Merge "Keep nav button container and back button stable during SUW" into main
diff --git a/Android.bp b/Android.bp
index df96156..73d0fce 100644
--- a/Android.bp
+++ b/Android.bp
@@ -25,7 +25,6 @@
name: "launcher-non-platform-apis-defaults",
static_libs: [
"android.os.flags-aconfig-java",
- "android.multiuser.flags-aconfig-java",
"android.appwidget.flags-aconfig-java",
"com.android.window.flags.window-aconfig-java",
],
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 6367a01..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,11 +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_item_icon_size_default">22dp</dimen>
- <dimen name="taskbar_overflow_item_icon_size_scaled_down">15dp</dimen>
- <dimen name="taskbar_overflow_item_icon_stroke_width_default">2dp</dimen>
- <dimen name="taskbar_overflow_leave_behind_size_default">18dp</dimen>
- <dimen name="taskbar_overflow_leave_behind_size_scaled_down">15dp</dimen>
<!-- Transient taskbar -->
<dimen name="transient_taskbar_padding">12dp</dimen>
@@ -430,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/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/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/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
index d7e5c61..8149f81 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
@@ -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/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 712478e..8775766 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarOverflowView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarOverflowView.java
@@ -41,6 +41,7 @@
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;
@@ -68,8 +69,15 @@
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
@@ -208,9 +216,24 @@
icon.mIconSize = iconSize;
icon.mPadding = padding;
- final float radius = iconSize / 2f - padding;
- final float size = radius + icon.mItemIconStrokeWidth;
- icon.mItemIconCenterOffsetDefault = radius - size / 2 - icon.mItemIconStrokeWidth;
+ 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;
@@ -222,22 +245,6 @@
mItemBackgroundColor = getContext().getColor(R.color.taskbar_background);
mLeaveBehindColor = Themes.getAttrColor(getContext(), android.R.attr.textColorTertiary);
- mItemIconSizeDefault = getResources().getDimension(
- R.dimen.taskbar_overflow_item_icon_size_default);
- mItemIconSizeScaledDown = getResources().getDimension(
- R.dimen.taskbar_overflow_item_icon_size_scaled_down);
- mItemIconSize = mItemIconSizeDefault;
-
- mItemIconStrokeWidthDefault = getResources().getDimension(
- R.dimen.taskbar_overflow_item_icon_stroke_width_default);
- mItemIconStrokeWidth = mItemIconStrokeWidthDefault;
-
- mLeaveBehindSizeDefault = getResources().getDimension(
- R.dimen.taskbar_overflow_leave_behind_size_default);
- mLeaveBehindSizeScaledDown = getResources().getDimension(
- R.dimen.taskbar_overflow_leave_behind_size_scaled_down);
- mLeaveBehindSize = mLeaveBehindSizeScaledDown;
-
setWillNotDraw(false);
}
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/TaskbarViewController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
index cebabff..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);
@@ -796,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/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/SystemApiWrapper.kt b/quickstep/src/com/android/launcher3/uioverrides/SystemApiWrapper.kt
index 2946242..374db6a 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/SystemApiWrapper.kt
+++ b/quickstep/src/com/android/launcher3/uioverrides/SystemApiWrapper.kt
@@ -27,7 +27,6 @@
import android.content.pm.LauncherActivityInfo
import android.content.pm.LauncherApps
import android.content.pm.ShortcutInfo
-import android.multiuser.Flags.addLauncherUserConfig
import android.os.Bundle
import android.os.Flags.allowPrivateProfile
import android.os.IBinder
@@ -75,14 +74,16 @@
mContext.getSystemService(UserManager::class.java)!!.userProfiles?.forEach { user ->
mContext.getSystemService(LauncherApps::class.java)!!.getLauncherUserInfo(user)?.apply {
users[user] =
- if (addLauncherUserConfig())
- UserIconInfo(
- user,
- getUserIconType(userType),
- userSerialNumber.toLong(),
- userConfig,
- )
- else UserIconInfo(user, getUserIconType(userType), userSerialNumber.toLong())
+ UserIconInfo(
+ user,
+ when (userType) {
+ UserManager.USER_TYPE_PROFILE_MANAGED -> UserIconInfo.TYPE_WORK
+ UserManager.USER_TYPE_PROFILE_CLONE -> UserIconInfo.TYPE_CLONED
+ UserManager.USER_TYPE_PROFILE_PRIVATE -> UserIconInfo.TYPE_PRIVATE
+ else -> UserIconInfo.TYPE_MAIN
+ },
+ userSerialNumber.toLong(),
+ )
}
}
return users
@@ -191,13 +192,4 @@
override fun getApplicationInfoHash(appInfo: ApplicationInfo): String =
(appInfo.sourceDir?.hashCode() ?: 0).toString() + " " + appInfo.longVersionCode
-
- fun getUserIconType(userType: String): Int {
- return when (userType) {
- UserManager.USER_TYPE_PROFILE_MANAGED -> UserIconInfo.TYPE_WORK
- UserManager.USER_TYPE_PROFILE_CLONE -> UserIconInfo.TYPE_CLONED
- UserManager.USER_TYPE_PROFILE_PRIVATE -> UserIconInfo.TYPE_PRIVATE
- else -> UserIconInfo.TYPE_MAIN
- }
- }
}
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 31aa489..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;
@@ -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/GestureState.java b/quickstep/src/com/android/quickstep/GestureState.java
index 5190ec8..cfbcf0a 100644
--- a/quickstep/src/com/android/quickstep/GestureState.java
+++ b/quickstep/src/com/android/quickstep/GestureState.java
@@ -274,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;
}
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/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/TopTaskTracker.java b/quickstep/src/com/android/quickstep/TopTaskTracker.java
index 80d6137..7065f37 100644
--- a/quickstep/src/com/android/quickstep/TopTaskTracker.java
+++ b/quickstep/src/com/android/quickstep/TopTaskTracker.java
@@ -23,6 +23,8 @@
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.app.ActivityManager.RunningTaskInfo;
@@ -228,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;
diff --git a/quickstep/src/com/android/quickstep/TouchInteractionService.java b/quickstep/src/com/android/quickstep/TouchInteractionService.java
index 0242fb6..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,312 +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();
-
- if (!com.android.wm.shell.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.
- 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();
@@ -1439,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
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/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 9d74bfb..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) {
@@ -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;
}
@@ -3835,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) {
@@ -3852,7 +3872,7 @@
Math.abs(i - dismissedIndex),
scrollDiff,
anim,
- splitTimings);
+ splitTimings, i);
needsCurveUpdates = true;
}
} else if (child instanceof TaskView taskView) {
@@ -3913,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++;
}
}
@@ -3935,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;
@@ -4153,6 +4175,7 @@
updateCurrentTaskActionsVisibility();
onDismissAnimationEnds();
mPendingAnimation = null;
+ mDismissPrimaryTranslations = null;
}
});
}
@@ -4191,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();
@@ -4225,6 +4249,9 @@
)
);
+ if (view instanceof TaskView) {
+ mDismissPrimaryTranslations[index] = scrollDiffPerPage;
+ }
if (mEnableDrawingLiveTile && view instanceof TaskView
&& ((TaskView) view).isRunningTask()) {
pendingAnimation.addOnFrameCallback(() -> {
@@ -5808,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);
@@ -5844,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?
@@ -5884,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/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_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/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 4ccdd53..8bd25dd 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" />
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 ef5c88a..817cc40 100644
--- a/src/com/android/launcher3/BubbleTextView.java
+++ b/src/com/android/launcher3/BubbleTextView.java
@@ -127,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;
@@ -730,16 +732,21 @@
Paint.FontMetrics fm = getPaint().getFontMetrics();
Rect tmpRect = new Rect();
getDrawingRect(tmpRect);
+ CharSequence text = getText();
- if (mIcon == null) {
- appTitleBounds = new RectF(0, 0, tmpRect.right,
- (int) Math.ceil(fm.bottom - fm.top));
- } else {
+ 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 = new RectF(tmpRect.left, textStart, tmpRect.right,
- textStart + (int) Math.ceil(fm.bottom - fm.top));
+ appTitleBounds.offset(0, textStart);
}
canvas.drawRoundRect(appTitleBounds, appTitleBounds.height() / 2,
@@ -851,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()
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/Launcher.java b/src/com/android/launcher3/Launcher.java
index 305941e..74dd971 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -782,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
);
diff --git a/src/com/android/launcher3/LauncherAppState.java b/src/com/android/launcher3/LauncherAppState.java
index 3936fe6..01d0a74 100644
--- a/src/com/android/launcher3/LauncherAppState.java
+++ b/src/com/android/launcher3/LauncherAppState.java
@@ -188,9 +188,7 @@
mOnTerminateCallback.add(() ->
settingsCache.unregister(NOTIFICATION_BADGING_URI, notificationLister));
// Register an observer to notify Launcher about Private Space settings toggle.
- if (!android.multiuser.Flags.addLauncherUserConfig()) {
- registerPrivateSpaceHideWhenLockListener(settingsCache);
- }
+ registerPrivateSpaceHideWhenLockListener(settingsCache);
}
public LauncherAppState(Context context, @Nullable String iconCacheFileName) {
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/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/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 e1c1b39..609edd2 100644
--- a/src/com/android/launcher3/allapps/PrivateProfileManager.java
+++ b/src/com/android/launcher3/allapps/PrivateProfileManager.java
@@ -16,7 +16,6 @@
package com.android.launcher3.allapps;
-import static android.content.pm.LauncherUserInfo.PRIVATE_SPACE_ENTRYPOINT_HIDDEN;
import static android.view.View.GONE;
import static android.view.View.INVISIBLE;
import static android.view.View.VISIBLE;
@@ -44,9 +43,9 @@
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
-import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
+import android.os.Trace;
import android.os.UserHandle;
import android.os.UserManager;
import android.util.Log;
@@ -80,13 +79,11 @@
import com.android.launcher3.util.ApiWrapper;
import com.android.launcher3.util.Preconditions;
import com.android.launcher3.util.SettingsCache;
-import com.android.launcher3.util.UserIconInfo;
import com.android.launcher3.views.ActivityContext;
import com.android.launcher3.views.RecyclerViewFastScroller;
import java.util.ArrayList;
import java.util.List;
-import java.util.Objects;
import java.util.function.Predicate;
/**
@@ -213,20 +210,9 @@
}
/** Whether private profile should be hidden on Launcher. */
- @SuppressLint("NewApi")
public boolean isPrivateSpaceHidden() {
- UserHandle profileHandle = getProfileUser();
- if (android.multiuser.Flags.addLauncherUserConfig() && !Objects.isNull(profileHandle)) {
- UserIconInfo userInconInfo = UserCache.INSTANCE.get(mAllApps.getContext()).getUserInfo(
- profileHandle);
-
- return getCurrentState() == STATE_DISABLED && userInconInfo.getUserConfig().getBoolean(
- PRIVATE_SPACE_ENTRYPOINT_HIDDEN, false);
- }
-
- return getCurrentState() == STATE_DISABLED && SettingsCache.INSTANCE.get(
- mAllApps.getContext()).getValue(PRIVATE_SPACE_HIDE_WHEN_LOCKED_URI, 0);
-
+ return getCurrentState() == STATE_DISABLED && SettingsCache.INSTANCE
+ .get(mAllApps.getContext()).getValue(PRIVATE_SPACE_HIDE_WHEN_LOCKED_URI, 0);
}
/**
@@ -235,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);
@@ -254,6 +241,7 @@
executeLock();
}
addPrivateSpaceDecorator(updatedState);
+ Trace.endSection();
}
/** Returns whether or not Private Space Settings Page is available. */
@@ -308,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()}
*/
@@ -346,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) {
@@ -393,6 +364,7 @@
if (mPSHeader == null) {
return;
}
+ Trace.beginSection("PrivateProfileManager#updateView");
Log.d(TAG, "bindPrivateSpaceHeaderViewElements: " + "Updating view with state: "
+ getCurrentState());
mPSHeader.setAlpha(1);
@@ -451,6 +423,7 @@
}
}
mPSHeader.invalidate();
+ Trace.endSection();
}
/** Sets the enablement of the profile when header or button is clicked. */
@@ -855,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.
@@ -864,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/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/model/ModelLauncherCallbacks.kt b/src/com/android/launcher3/model/ModelLauncherCallbacks.kt
index ba4908c..7ba2dad 100644
--- a/src/com/android/launcher3/model/ModelLauncherCallbacks.kt
+++ b/src/com/android/launcher3/model/ModelLauncherCallbacks.kt
@@ -16,9 +16,7 @@
package com.android.launcher3.model
-import android.annotation.SuppressLint
import android.content.pm.LauncherApps
-import android.content.pm.LauncherUserInfo
import android.content.pm.PackageInstaller.SessionInfo
import android.content.pm.ShortcutInfo
import android.os.UserHandle
@@ -34,7 +32,6 @@
import com.android.launcher3.model.PackageUpdatedTask.OP_UPDATE
import com.android.launcher3.pm.InstallSessionTracker
import com.android.launcher3.pm.PackageInstallInfo
-import com.android.launcher3.pm.UserCache
import com.android.launcher3.util.PackageUserKey
import java.util.function.Consumer
@@ -137,17 +134,6 @@
}
}
- @SuppressLint("NewApi")
- override fun onUserConfigChanged(launcherUserInfo: LauncherUserInfo) {
- FileLog.d(TAG, "onUserConfigChanged for user ${launcherUserInfo.userType}")
- if (android.multiuser.Flags.addLauncherUserConfig()) {
- taskExecutor.accept { taskController, _, _ ->
- UserCache.INSTANCE.get(taskController.app.context).updateCache()
- taskController.app.model.forceReload()
- }
- }
- }
-
companion object {
private const val TAG = "LauncherAppsCallbackImpl"
}
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/pm/UserCache.java b/src/com/android/launcher3/pm/UserCache.java
index 69f4469..e861961 100644
--- a/src/com/android/launcher3/pm/UserCache.java
+++ b/src/com/android/launcher3/pm/UserCache.java
@@ -26,6 +26,7 @@
import android.os.UserManager;
import android.util.ArrayMap;
+import androidx.annotation.AnyThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
@@ -110,7 +111,7 @@
updateCache();
}
- @WorkerThread
+ @AnyThread
private void onUsersChanged(Intent intent) {
MODEL_EXECUTOR.execute(this::updateCache);
UserHandle user = intent.getParcelableExtra(Intent.EXTRA_USER);
@@ -122,7 +123,7 @@
}
@WorkerThread
- public void updateCache() {
+ private void updateCache() {
mUserToSerialMap = ApiWrapper.INSTANCE.get(mContext).queryAllUsers();
mUserToPreInstallAppMap = fetchPreInstallApps();
}
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/ApiWrapper.java b/src/com/android/launcher3/util/ApiWrapper.java
index 35fc9f5..467a7ec 100644
--- a/src/com/android/launcher3/util/ApiWrapper.java
+++ b/src/com/android/launcher3/util/ApiWrapper.java
@@ -16,11 +16,8 @@
package com.android.launcher3.util;
-import static android.multiuser.Flags.addLauncherUserConfig;
-
import static com.android.launcher3.LauncherConstants.ActivityCodes.REQUEST_HOME_ROLE;
-import android.annotation.SuppressLint;
import android.app.ActivityOptions;
import android.app.Person;
import android.app.role.RoleManager;
@@ -28,12 +25,9 @@
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.LauncherActivityInfo;
-import android.content.pm.LauncherApps;
-import android.content.pm.LauncherUserInfo;
import android.content.pm.ShortcutInfo;
import android.graphics.drawable.ColorDrawable;
import android.net.Uri;
-import android.os.Build;
import android.os.UserHandle;
import android.os.UserManager;
import android.util.ArrayMap;
@@ -51,7 +45,6 @@
import java.util.Collections;
import java.util.List;
import java.util.Map;
-import java.util.Objects;
import javax.inject.Inject;
@@ -92,32 +85,22 @@
/**
* Returns a map of all users on the device to their corresponding UI properties
*/
- @SuppressLint("NewApi")
public Map<UserHandle, UserIconInfo> queryAllUsers() {
UserManager um = mContext.getSystemService(UserManager.class);
Map<UserHandle, UserIconInfo> users = new ArrayMap<>();
List<UserHandle> usersActual = um.getUserProfiles();
if (usersActual != null) {
for (UserHandle user : usersActual) {
+ long serial = um.getSerialNumberForUser(user);
+
+ // Simple check to check if the provided user is work profile
+ // TODO: Migrate to a better platform API
NoopDrawable d = new NoopDrawable();
boolean isWork = (d != mContext.getPackageManager().getUserBadgedIcon(d, user));
- UserIconInfo info;
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
- LauncherUserInfo userInfo = Objects.requireNonNull(
- mContext.getSystemService(LauncherApps.class)).getLauncherUserInfo(
- user);
- long serial = Objects.requireNonNull(userInfo).getUserSerialNumber();
- info = addLauncherUserConfig() ? new UserIconInfo(user,
- isWork ? UserIconInfo.TYPE_WORK : UserIconInfo.TYPE_MAIN, serial,
- userInfo.getUserConfig()) : new UserIconInfo(user,
- isWork ? UserIconInfo.TYPE_WORK : UserIconInfo.TYPE_MAIN, serial);
- } else {
- long serial = um.getSerialNumberForUser(user);
- // Simple check to check if the provided user is work profile
- // TODO: Migrate to a better platform API
- info = new UserIconInfo(user,
- isWork ? UserIconInfo.TYPE_WORK : UserIconInfo.TYPE_MAIN, serial);
- }
+ UserIconInfo info = new UserIconInfo(
+ user,
+ isWork ? UserIconInfo.TYPE_WORK : UserIconInfo.TYPE_MAIN,
+ serial);
users.put(user, info);
}
}
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/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/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/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/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/ui/AbstractLauncherUiTest.java b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
index 2b1fddc..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,184 +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 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();
- }
-
/**
* Runs the callback on the UI thread and returns the result.
*/
@@ -542,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(
@@ -668,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