diff options
96 files changed, 3017 insertions, 1456 deletions
diff --git a/Android.bp b/Android.bp index df9615631f..73d0fce260 100644 --- a/Android.bp +++ b/Android.bp @@ -25,7 +25,6 @@ java_defaults { 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 878aa6e5e9..4ff976d148 100644 --- a/aconfig/launcher.aconfig +++ b/aconfig/launcher.aconfig @@ -501,10 +501,13 @@ flag { } 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 @@ flag { 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 57bfb4acba..8c39585bfe 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 0000000000..7d912a24c4 --- /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 2420a4624e..4118500656 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 6367a01860..b221b22e80 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 18337d3991..e624be7e6e 100644 --- a/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java +++ b/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java @@ -108,6 +108,7 @@ import android.view.WindowManager; 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.animation.RemoteAnimationRunnerCompat; 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 class QuickstepTransitionManager implements OnDeviceProfileChangeListener 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 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener protected final Handler mHandler; private final float mClosingWindowTransY; + private final float mClosingFreeformWindowTransY; private final float mMaxShadowRadius; private final StartingWindowListener mStartingWindowListener = @@ -290,6 +295,8 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener 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 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener ? 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 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener 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 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener 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 57444648ea..fd0243a035 100644 --- a/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.java +++ b/quickstep/src/com/android/launcher3/statehandlers/DesktopVisibilityController.java @@ -488,6 +488,15 @@ public class DesktopVisibilityController { } }); } + + 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 23a5a27984..3b7ad3e618 100644 --- a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java +++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchController.java @@ -139,18 +139,42 @@ public final class KeyboardQuickSwitchController implements @NonNull Set<Integer> taskIdsToExclude, boolean wasOpenedFromTaskbar) { if (mQuickSwitchViewController != null) { - if (!mQuickSwitchViewController.isCloseAnimationRunning() - && mQuickSwitchViewController.wasOpenedFromTaskbar() == wasOpenedFromTaskbar) { + if (!mQuickSwitchViewController.isCloseAnimationRunning()) { + if (mQuickSwitchViewController.wasOpenedFromTaskbar() == wasOpenedFromTaskbar) { + return; + } + + // 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); } - - // Allow the KQS to be reopened during the close animation to make it more responsive. - // Similarly, if KQS was opened in different mode (from taskbar vs. keyboard event), - // close it so it can be reopened in the correct mode. - // TODO(b/368119679) Consider updating list of shown tasks in place, or at least reopen - // the view in the same vertical location. - closeQuickSwitchView(false); } + mOverlayContext = mControllers.taskbarOverlayController.requestWindow(); if (Flags.taskbarOverflow()) { mOverlayContext.getDragLayer().addTouchController(this); @@ -186,13 +210,7 @@ public final class KeyboardQuickSwitchController implements 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 @@ public final class KeyboardQuickSwitchController implements } 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 05d34b5cb7..1967dfd980 100644 --- a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java +++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchView.java @@ -201,6 +201,8 @@ public class KeyboardQuickSwitchView extends ConstraintLayout { 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 @@ public class KeyboardQuickSwitchView extends ConstraintLayout { 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 390112e676..985cc26d8e 100644 --- a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java +++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java @@ -128,6 +128,23 @@ public class KeyboardQuickSwitchViewController { /* 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 @@ public class KeyboardQuickSwitchViewController { 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 4a94be71bb..c5be13dbe1 100644 --- a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java +++ b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java @@ -33,6 +33,7 @@ import androidx.annotation.Nullable; 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 @@ public class LauncherTaskbarUIController extends TaskbarUIController { private final DeviceProfile.OnDeviceProfileChangeListener mOnDeviceProfileChangeListener = dp -> { onStashedInAppChanged(dp); + adjustHotseatForBubbleBar(); if (mControllers != null && mControllers.taskbarViewController != null) { mControllers.taskbarViewController.onRotationChanged(dp); } @@ -152,8 +154,9 @@ public class LauncherTaskbarUIController extends TaskbarUIController { @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 @@ public class LauncherTaskbarUIController extends TaskbarUIController { } 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 @@ public class LauncherTaskbarUIController extends TaskbarUIController { } } + 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 @@ public class LauncherTaskbarUIController extends TaskbarUIController { } @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 0000000000..c0c2a024b2 --- /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 d7e5c61215..8149f8139c 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java @@ -582,7 +582,9 @@ public class TaskbarActivityContext extends BaseTaskbarContext { 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 8ab2ffadcf..bdc7f92c25 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarAutohideSuspendController.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarAutohideSuspendController.java @@ -47,6 +47,8 @@ public class TaskbarAutohideSuspendController implements 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 @@ public class TaskbarAutohideSuspendController implements 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 @@ public class TaskbarAutohideSuspendController implements "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 5a63ca69aa..db707247ad 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarControllers.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarControllers.java @@ -221,10 +221,13 @@ public class TaskbarControllers { 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 e16c76dd81..8b521128a7 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayer.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarDragLayer.java @@ -262,6 +262,7 @@ public class TaskbarDragLayer extends BaseDragLayer<TaskbarActivityContext> { @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 2845ceee7a..925e10bdce 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.Canvas; 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 @@ public class TaskbarDragLayerController implements TaskbarControllers.LoggableTa } 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 fa0473910b..dce377d5d7 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java @@ -17,6 +17,7 @@ package com.android.launcher3.taskbar; 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 @@ public class TaskbarLauncherStateController { 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 @@ public class TaskbarLauncherStateController { 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 @@ public class TaskbarLauncherStateController { } 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 @@ public class TaskbarLauncherStateController { 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 @@ public class TaskbarLauncherStateController { + 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 @@ public class TaskbarLauncherStateController { 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 @@ public class TaskbarLauncherStateController { @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 @@ public class TaskbarLauncherStateController { * * @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 @@ public class TaskbarLauncherStateController { mSkipNextRecentsAnimEnd = false; return; } - updateStateForUserFinishedToApp(finishedToApp); + updateStateForUserFinishedToApp(finishedToApp, launcherIsVisible); } } /** + * @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) { + 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 @@ public class TaskbarLauncherStateController { 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 0f9ede9f56..d4764c7634 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarNavButtonController.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarNavButtonController.java @@ -281,6 +281,10 @@ public class TaskbarNavButtonController implements TaskbarControllers.LoggableTa } 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 712478e440..8775766b25 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.app.animation.Interpolators; import com.android.launcher3.R; import com.android.launcher3.Reorderable; import com.android.launcher3.Utilities; +import com.android.launcher3.icons.IconNormalizer; import com.android.launcher3.util.MultiTranslateDelegate; import com.android.launcher3.util.Themes; import com.android.systemui.shared.recents.model.Task; @@ -68,8 +69,15 @@ public class TaskbarOverflowView extends FrameLayout implements Reorderable { 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 @@ public class TaskbarOverflowView extends FrameLayout implements Reorderable { 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 @@ public class TaskbarOverflowView extends FrameLayout implements Reorderable { 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 70d4bb10f2..2e0bae5200 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarPopupController.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarPopupController.java @@ -201,8 +201,10 @@ public class TaskbarPopupController implements TaskbarControllers.LoggableTaskba 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 @@ public class TaskbarPopupController implements TaskbarControllers.LoggableTaskba * 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 @@ public class TaskbarPopupController implements TaskbarControllers.LoggableTaskba } /** + * 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 c1dd216a9c..67be8dac05 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.INSTANT; 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 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba * 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 f7f5cf6fe2..8b636dd9b6 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java @@ -195,6 +195,16 @@ public class TaskbarUIController implements BubbleBarController.BubbleBarLocatio 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 cebabffa75..bb4f07a72f 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java @@ -120,7 +120,7 @@ public class TaskbarViewController implements TaskbarControllers.LoggableTaskbar 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 @@ public class TaskbarViewController implements TaskbarControllers.LoggableTaskbar */ 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 c5f8aa091d..7e3b362df5 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.Context 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 @@ constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 } 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 1fb835ab33..344f1634fd 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.Context 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 @@ import com.android.launcher3.views.IconButtonView /** 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 @@ constructor( } 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 2946242ac7..374db6ae6f 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.ApplicationInfo 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 @@ open class SystemApiWrapper @Inject constructor(@ApplicationContext context: Con 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 @@ open class SystemApiWrapper @Inject constructor(@ApplicationContext context: Con 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 3a39cf28b1..8ad00bfd88 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 class QuickstepAtomicAnimationFactory extends 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 @@ public class QuickstepAtomicAnimationFactory extends // 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 @@ public class QuickstepAtomicAnimationFactory extends // 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 31aa489bd6..95e7737906 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.jank.Cuj; 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 @@ public abstract class AbsSwipeUpHandler< 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 5190ec8b7a..cfbcf0ad47 100644 --- a/quickstep/src/com/android/quickstep/GestureState.java +++ b/quickstep/src/com/android/quickstep/GestureState.java @@ -274,7 +274,7 @@ public class GestureState implements RecentsAnimationCallbacks.RecentsAnimationL * @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 0000000000..bea3150bcb --- /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 1124aac457..6719ab74dc 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.AbstractFloatingView.TYPE_REBIND_SAFE; 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.R; 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 @@ public class LauncherBackAnimationController { 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 dcb01087ee..60fcff86f3 100644 --- a/quickstep/src/com/android/quickstep/RecentsAnimationController.java +++ b/quickstep/src/com/android/quickstep/RecentsAnimationController.java @@ -53,6 +53,8 @@ public class RecentsAnimationController { 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 @@ public class RecentsAnimationController { } @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 @@ public class RecentsAnimationController { // 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 @@ public class RecentsAnimationController { 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 80d6137344..7065f37ebf 100644 --- a/quickstep/src/com/android/quickstep/TopTaskTracker.java +++ b/quickstep/src/com/android/quickstep/TopTaskTracker.java @@ -23,6 +23,8 @@ import static android.content.Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS; import static android.view.Display.DEFAULT_DISPLAY; import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT; +import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_TYPE_A; +import static com.android.wm.shell.Flags.enableFlexibleSplit; import static com.android.wm.shell.shared.GroupedTaskInfo.TYPE_SPLIT; import android.app.ActivityManager.RunningTaskInfo; @@ -228,7 +230,8 @@ public class TopTaskTracker extends ISplitScreenListener.Stub 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 0242fb6c48..d38eaf33b9 100644 --- a/quickstep/src/com/android/quickstep/TouchInteractionService.java +++ b/quickstep/src/com/android/quickstep/TouchInteractionService.java @@ -24,7 +24,6 @@ import static android.view.MotionEvent.ACTION_UP; 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.launcher3.util.window.WindowManagerProxy.MIN_TABLET_WI 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.Choreographer; 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.launcher3.util.TraceHelper; 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 @@ public class TouchInteractionService extends Service { + "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 @@ public class TouchInteractionService extends Service { 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 @@ public class TouchInteractionService extends Service { // 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 @@ public class TouchInteractionService extends Service { 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 @@ public class TouchInteractionService extends Service { 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 @@ public class TouchInteractionService extends Service { ? 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 4f9d8371dc..c1d3f6e18e 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 @@ public class RecentsWindowSwipeHandler extends AbsSwipeUpHandler<RecentsWindowMa 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 997a842dc2..12ca25702e 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.celllayout.CellLayoutLayoutParams; 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 @@ public class StaggeredWorkspaceAnim { 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 class StaggeredWorkspaceAnim { 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 @@ public class StaggeredWorkspaceAnim { .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 9d74bfb2ad..6ab3e286a5 100644 --- a/quickstep/src/com/android/quickstep/views/RecentsView.java +++ b/quickstep/src/com/android/quickstep/views/RecentsView.java @@ -610,6 +610,8 @@ public abstract class RecentsView< private int mKeyboardTaskFocusSnapAnimationDuration; private int mKeyboardTaskFocusIndex = INVALID_PAGE; + private int[] mDismissPrimaryTranslations; + /** * TODO: Call reloadIdNeeded in onTaskStackChanged. */ @@ -847,6 +849,8 @@ public abstract class RecentsView< 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 @@ public abstract class RecentsView< 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 @@ public abstract class RecentsView< 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 @@ public abstract class RecentsView< } 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 @@ public abstract class RecentsView< 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 @@ public abstract class RecentsView< Math.abs(i - dismissedIndex), scrollDiff, anim, - splitTimings); + splitTimings, i); needsCurveUpdates = true; } } else if (child instanceof TaskView taskView) { @@ -3913,10 +3933,12 @@ public abstract class RecentsView< 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 @@ public abstract class RecentsView< 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 @@ public abstract class RecentsView< updateCurrentTaskActionsVisibility(); onDismissAnimationEnds(); mPendingAnimation = null; + mDismissPrimaryTranslations = null; } }); } @@ -4191,7 +4214,8 @@ public abstract class RecentsView< 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 @@ public abstract class RecentsView< ) ); + if (view instanceof TaskView) { + mDismissPrimaryTranslations[index] = scrollDiffPerPage; + } if (mEnableDrawingLiveTile && view instanceof TaskView && ((TaskView) view).isRunningTask()) { pendingAnimation.addOnFrameCallback(() -> { @@ -5808,6 +5835,14 @@ public abstract class RecentsView< // 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 @@ public abstract class RecentsView< * 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 @@ public abstract class RecentsView< 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 71f4ef4ee6..5e438bd8e1 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.annotations.EnableFlags 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 @@ class TaskbarStashControllerTest { @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 d113a38ac8..2bb2eb3097 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 622f0d6aa5..7c577264d3 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 5427732c4d..1ce1c55ec4 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 5dc1b47ece..cf090ad92e 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 06f0eee50f..a891e392b8 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 4ccdd53f61..8bd25ddd55 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 123e2b8dcf..c280307a43 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 6d3579b1a9..1c70d6c6aa 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 ef5c88aa33..817cc40abc 100644 --- a/src/com/android/launcher3/BubbleTextView.java +++ b/src/com/android/launcher3/BubbleTextView.java @@ -127,6 +127,8 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver, 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 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver, 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 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver, 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 78535a197e..09225e7924 100644 --- a/src/com/android/launcher3/DeviceProfile.java +++ b/src/com/android/launcher3/DeviceProfile.java @@ -1833,7 +1833,8 @@ public class DeviceProfile { 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 @@ public class DeviceProfile { 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 6468f749ac..b2ccba431c 100644 --- a/src/com/android/launcher3/Hotseat.java +++ b/src/com/android/launcher3/Hotseat.java @@ -187,22 +187,20 @@ public class Hotseat extends CellLayout implements Insettable { 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 @@ public class Hotseat extends CellLayout implements Insettable { } 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 305941e07a..74dd971114 100644 --- a/src/com/android/launcher3/Launcher.java +++ b/src/com/android/launcher3/Launcher.java @@ -782,6 +782,11 @@ public class Launcher extends StatefulActivity<LauncherState> 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 3936fe6564..01d0a740d7 100644 --- a/src/com/android/launcher3/LauncherAppState.java +++ b/src/com/android/launcher3/LauncherAppState.java @@ -188,9 +188,7 @@ public class LauncherAppState implements SafeCloseable { 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 df75470763..a5b8168c5f 100644 --- a/src/com/android/launcher3/LauncherFiles.java +++ b/src/com/android/launcher3/LauncherFiles.java @@ -24,7 +24,7 @@ public class LauncherFiles { 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 @@ public class LauncherFiles { 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 b3cb948a3d..f4d3146ed5 100644 --- a/src/com/android/launcher3/SecondaryDropTarget.java +++ b/src/com/android/launcher3/SecondaryDropTarget.java @@ -303,10 +303,11 @@ public class SecondaryDropTarget extends ButtonDropTarget implements OnAlarmList .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 6168e41464..ea5eb8f284 100644 --- a/src/com/android/launcher3/SessionCommitReceiver.java +++ b/src/com/android/launcher3/SessionCommitReceiver.java @@ -73,8 +73,9 @@ public class SessionCommitReceiver extends BroadcastReceiver { || 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 1b58987500..c938482284 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.logging.StatsLogManager.LauncherEvent.LAUNCH 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 @@ public class ActivityAllAppsContainerView<T extends Context & ActivityContext> 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 @@ public class ActivityAllAppsContainerView<T extends Context & ActivityContext> 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 e1c1b39493..609edd2c99 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.AnimatorListenerAdapter; 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.pm.UserCache; 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 @@ public class PrivateProfileManager extends UserProfileManager { } /** 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 @@ public class PrivateProfileManager extends UserProfileManager { * 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 @@ public class PrivateProfileManager extends UserProfileManager { executeLock(); } addPrivateSpaceDecorator(updatedState); + Trace.endSection(); } /** Returns whether or not Private Space Settings Page is available. */ @@ -308,31 +296,12 @@ public class PrivateProfileManager extends UserProfileManager { } } - @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 @@ public class PrivateProfileManager extends UserProfileManager { /** 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 @@ public class PrivateProfileManager extends UserProfileManager { if (mPSHeader == null) { return; } + Trace.beginSection("PrivateProfileManager#updateView"); Log.d(TAG, "bindPrivateSpaceHeaderViewElements: " + "Updating view with state: " + getCurrentState()); mPSHeader.setAlpha(1); @@ -451,6 +423,7 @@ public class PrivateProfileManager extends UserProfileManager { } } mPSHeader.invalidate(); + Trace.endSection(); } /** Sets the enablement of the profile when header or button is clicked. */ @@ -855,6 +828,7 @@ public class PrivateProfileManager extends UserProfileManager { 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 @@ public class PrivateProfileManager extends UserProfileManager { 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 93b6b29b34..765c29cccc 100644 --- a/src/com/android/launcher3/allapps/UserProfileManager.java +++ b/src/com/android/launcher3/allapps/UserProfileManager.java @@ -18,6 +18,7 @@ package com.android.launcher3.allapps; 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 androidx.annotation.IntDef; 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 @@ public abstract class UserProfileManager { } /** 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 3d0c1d063d..6ebab5a82d 100644 --- a/src/com/android/launcher3/allapps/WorkProfileManager.java +++ b/src/com/android/launcher3/allapps/WorkProfileManager.java @@ -74,7 +74,7 @@ public class WorkProfileManager extends UserProfileManager */ 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 0000000000..5015e5474a --- /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 617cac7f59..bfa00bda9e 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 @@ public class GridSizeMigrationDBController { // 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 @@ public class GridSizeMigrationDBController { } } - 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 c856d4bb46..3f52d8a73f 100644 --- a/src/com/android/launcher3/model/GridSizeMigrationLogic.kt +++ b/src/com/android/launcher3/model/GridSizeMigrationLogic.kt @@ -21,15 +21,20 @@ import android.graphics.Point 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 @@ class GridSizeMigrationLogic { // 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 @@ class GridSizeMigrationLogic { // 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 @@ class GridSizeMigrationLogic { } } + 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 a830c969cf..83eace83c6 100644 --- a/src/com/android/launcher3/model/LoaderTask.java +++ b/src/com/android/launcher3/model/LoaderTask.java @@ -246,7 +246,7 @@ public class LoaderTask implements Runnable { 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 @@ public class LoaderTask implements Runnable { 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 @@ public class LoaderTask implements Runnable { } 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 @@ public class LoaderTask implements Runnable { 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 @@ public class LoaderTask implements Runnable { // Take a break waitForIdle(); - logASplit("step 3 complete"); + logASplit("step 3 loading all shortcuts complete"); verifyNotStopped(); // fourth step @@ -345,11 +345,11 @@ public class LoaderTask implements Runnable { 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 @@ public class LoaderTask implements Runnable { 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 @@ public class LoaderTask implements Runnable { 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 @@ public class LoaderTask implements Runnable { } finally { Trace.endSection(); } - logASplit("loadWorkspace"); + logASplit("loadWorkspace finished"); mBgDataModel.isFirstPagePinnedItemEnabled = FeatureFlags.QSB_ON_FIRST_SCREEN && (!enableSmartspaceRemovalToggle() || LauncherPrefs.getPrefs( @@ -440,7 +440,7 @@ public class LoaderTask implements Runnable { } 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 @@ public class LoaderTask implements Runnable { 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 @@ public class LoaderTask implements Runnable { 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 ba4908ca8a..7ba2dad553 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_UNSUSPEND 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 @@ class ModelLauncherCallbacks(private var taskExecutor: Consumer<ModelUpdateTask> } } - @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 856c294a3a..b9c928c50b 100644 --- a/src/com/android/launcher3/pm/InstallSessionTracker.java +++ b/src/com/android/launcher3/pm/InstallSessionTracker.java @@ -33,7 +33,6 @@ import androidx.annotation.Nullable; 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 @@ public class InstallSessionTracker extends PackageInstaller.SessionCallback { } 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 @@ public class InstallSessionTracker extends PackageInstaller.SessionCallback { 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 69f4469e02..e8619610a8 100644 --- a/src/com/android/launcher3/pm/UserCache.java +++ b/src/com/android/launcher3/pm/UserCache.java @@ -26,6 +26,7 @@ import android.os.UserHandle; 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 @@ public class UserCache implements SafeCloseable { updateCache(); } - @WorkerThread + @AnyThread private void onUsersChanged(Intent intent) { MODEL_EXECUTOR.execute(this::updateCache); UserHandle user = intent.getParcelableExtra(Intent.EXTRA_USER); @@ -122,7 +123,7 @@ public class UserCache implements SafeCloseable { } @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 3c68e462a8..6f1d0dddaf 100644 --- a/src/com/android/launcher3/provider/LauncherDbUtils.kt +++ b/src/com/android/launcher3/provider/LauncherDbUtils.kt @@ -131,6 +131,11 @@ object LauncherDbUtils { } } + @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 82229f8f21..e4c50f08b1 100644 --- a/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPool.kt +++ b/src/com/android/launcher3/recyclerview/AllAppsRecyclerViewPool.kt @@ -18,18 +18,23 @@ package com.android.launcher3.recyclerview 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 @@ const val EXTRA_ICONS_COUNT = 2 * [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 @@ class AllAppsRecyclerViewPool<T> : RecycledViewPool() { /** * 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 @@ class AllAppsRecyclerViewPool<T> : RecycledViewPool() { 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 + } + 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.add( - adapter.createViewHolder(activeRv, BaseAllAppsAdapter.VIEW_TYPE_ICON) - ) } 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 @@ class AllAppsRecyclerViewPool<T> : RecycledViewPool() { * 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 5851f625f2..5068b48fc8 100644 --- a/src/com/android/launcher3/settings/SettingsActivity.java +++ b/src/com/android/launcher3/settings/SettingsActivity.java @@ -22,6 +22,7 @@ import static androidx.preference.PreferenceFragmentCompat.ARG_PREFERENCE_ROOT; 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 androidx.recyclerview.widget.RecyclerView; 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 @@ public class SettingsActivity extends FragmentActivity } 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 35fc9f5d80..467a7ec54f 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.Context; 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 com.android.launcher3.dagger.LauncherAppSingleton; import java.util.Collections; import java.util.List; import java.util.Map; -import java.util.Objects; import javax.inject.Inject; @@ -92,32 +85,22 @@ public class ApiWrapper { /** * 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 febe6af2d4..a245761fb7 100644 --- a/src/com/android/launcher3/util/DaggerSingletonObject.java +++ b/src/com/android/launcher3/util/DaggerSingletonObject.java @@ -18,8 +18,8 @@ package com.android.launcher3.util; 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 class DaggerSingletonObject<T> { } 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 f457e4e369..44a7c6fa74 100644 --- a/src/com/android/launcher3/util/SplitConfigurationOptions.java +++ b/src/com/android/launcher3/util/SplitConfigurationOptions.java @@ -73,7 +73,27 @@ public final class SplitConfigurationOptions { */ 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 1c0d94cbf4..fda5175794 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.Flags.enableWidgetTapToAdd; 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 @@ public abstract class BaseWidgetSheet extends AbstractSlideInView<BaseActivity> } @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 @@ public abstract class BaseWidgetSheet extends AbstractSlideInView<BaseActivity> 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 e190dc3749..2c07fd9ad1 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 class RoundedCornerEnforcement { 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 35a227523b..e4fecc5ec8 100644 --- a/tests/Android.bp +++ b/tests/Android.bp @@ -63,7 +63,6 @@ filegroup { "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 @@ filegroup { "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 1eacfaec0f..73b67d0105 100644 --- a/tests/assets/ReorderWidgets/push_reorder_case +++ b/tests/assets/ReorderWidgets/push_reorder_case @@ -39,6 +39,6 @@ arguments: 2 2 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 0000000000..92558773a7 --- /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 ce00b28c51..9b4bd71608 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.android.launcher3.util.PackageUserKey; 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 @@ public class IconCacheTest { 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 c9ea421039..09752b860e 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.LauncherSettings.Favorites.addTableToDb 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 @@ private const val ICON_RESOURCE = "iconResource" @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 @@ class DatabaseHelperTest { */ @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 @@ class DatabaseHelperTest { */ @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 7933331cff..eee6191f15 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 @@ class GridSizeMigrationTest { @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 b4945d7129..63359ec1cd 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 @@ public class LoaderCursorTest { @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 d0c168a8d1..c30b730807 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 @@ public class RestoreDbTaskTest { private Cursor mMockCursor; private LauncherPrefs mPrefs; private LauncherRestoreEventLogger mMockRestoreEventLogger; + private SQLiteDatabase mDb; @Before public void setup() { @@ -107,57 +108,60 @@ public class RestoreDbTaskTest { @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 @@ public class RestoreDbTaskTest { 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 @@ public class RestoreDbTaskTest { 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 @@ public class RestoreDbTaskTest { 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 @@ public class RestoreDbTaskTest { 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 @@ public class RestoreDbTaskTest { } 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 0000000000..3afb0b503f --- /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 7484bce091..ac5fda26a4 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.model.WidgetItem 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 @@ class GeneratedPreviewTest { @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 @@ class GeneratedPreviewTest { 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 @@ class GeneratedPreviewTest { 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 c82e84c18a..13e23c9753 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 @@ class RoundedCornerEnforcementTest { } @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 @@ class RoundedCornerEnforcementTest { } @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 @@ class RoundedCornerEnforcementTest { } @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 2b1fddce62..8e4db5c6cf 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 @@ import java.util.function.Supplier; /** * 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(); + /** + * @deprecated call {@link #performInitialization} instead + */ + @Deprecated + public static void initialize(AbstractLauncherUiTest test) throws Exception { + test.performInitialization(); + } - @Rule - public LimitDevicesRule mlimitDevicesRule = new LimitDevicesRule(); + @Override + protected void performInitialization() { + reinitializeLauncherData(); + mDevice.pressHome(); + // Check that we switched to home. + mLauncher.getWorkspace(); - 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", + waitForLauncherCondition("Launcher didn't start", Objects::nonNull); + waitForState("Launcher internal state didn't switch to Home", () -> LauncherState.NORMAL); - test.waitForResumed("Launcher internal state is still Background"); - // Check that we switched to home. - test.mLauncher.getWorkspace(); - AbstractLauncherUiTest.checkDetectedLeaks(test.mLauncher, true); - } + waitForResumed("Launcher internal state is still Background"); - 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")); + checkDetectedLeaks(mLauncher, true); } + @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 @@ public abstract class AbstractLauncherUiTest<LAUNCHER_TYPE extends Launcher> { : 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 @@ public abstract class AbstractLauncherUiTest<LAUNCHER_TYPE extends Launcher> { }, 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 @@ public abstract class AbstractLauncherUiTest<LAUNCHER_TYPE extends Launcher> { 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 0000000000..84498533c3 --- /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/TaplAddConfigWidgetTest.java b/tests/src/com/android/launcher3/ui/widget/AddConfigWidgetTest.java index 7845222b6e..bb645d714f 100644 --- a/tests/src/com/android/launcher3/ui/widget/TaplAddConfigWidgetTest.java +++ b/tests/src/com/android/launcher3/ui/widget/AddConfigWidgetTest.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; @@ -24,23 +24,34 @@ 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 androidx.test.runner.AndroidJUnit4; +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.AbstractLauncherUiTest; 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; @@ -52,7 +63,7 @@ import org.junit.runner.RunWith; */ @LargeTest @RunWith(AndroidJUnit4.class) -public class TaplAddConfigWidgetTest extends AbstractLauncherUiTest<Launcher> { +public class AddConfigWidgetTest extends BaseLauncherActivityTest<Launcher> { @Rule public ShellCommandRule mGrantWidgetRule = ShellCommandRule.grantWidgetBind(); @@ -62,12 +73,10 @@ public class TaplAddConfigWidgetTest extends AbstractLauncherUiTest<Launcher> { private int mWidgetId; - @Override @Before public void setUp() throws Exception { - super.setUp(); mWidgetInfo = TestViewHelpers.findWidgetProvider(true /* hasConfigureScreen */); - mAppWidgetManager = AppWidgetManager.getInstance(mTargetContext); + mAppWidgetManager = AppWidgetManager.getInstance(targetContext()); } @Test @@ -82,80 +91,86 @@ public class TaplAddConfigWidgetTest extends AbstractLauncherUiTest<Launcher> { runTest(false); } - /** * @param acceptConfig accept the config activity */ private void runTest(boolean acceptConfig) throws Throwable { - commitTransactionAndLoadHome(new FavoriteItemsTransaction(mTargetContext)); + new FavoriteItemsTransaction(targetContext()).commit(); + loadLauncherSync(); - // Drag widget to homescreen + // Add widget to homescreen WidgetConfigStartupMonitor monitor = new WidgetConfigStartupMonitor(); - mLauncher.getWorkspace() - .openAllWidgets() - .getWidget(mWidgetInfo.getLabel()) - .dragToWorkspace(true, false); + 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); - setResultAndWaitForAnimation(acceptConfig); if (acceptConfig) { - Wait.atMost("", new WidgetSearchCondition(), mLauncher); + 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, - mLauncher); + Wait.atMost("", () -> mAppWidgetManager.getAppWidgetInfo(mWidgetId) == null); } } - private static void setResult(boolean success) { + private 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"); - } + uiDevice.waitForIdle(); } /** * 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(); - } + private class WidgetSearchCondition implements ItemOperator { @Override public boolean evaluate(ItemInfo info, View view) { - return info instanceof LauncherAppWidgetInfo - && ((LauncherAppWidgetInfo) info).providerName.getClassName().equals( - mWidgetInfo.provider.getClassName()) - && ((LauncherAppWidgetInfo) info).appWidgetId == mWidgetId; + return info instanceof LauncherAppWidgetInfo lawi + && lawi.providerName.equals(mWidgetInfo.provider) + && lawi.appWidgetId == mWidgetId; } } /** * Broadcast receiver for receiving widget config activity status. */ - private class WidgetConfigStartupMonitor extends BlockingBroadcastReceiver { + private static class WidgetConfigStartupMonitor extends BlockingBroadcastReceiver { - public WidgetConfigStartupMonitor() { + WidgetConfigStartupMonitor() { super(WidgetConfigActivity.class.getName()); } diff --git a/tests/src/com/android/launcher3/ui/widget/TaplBindWidgetTest.java b/tests/src/com/android/launcher3/ui/widget/BindWidgetTest.java index 4cdbd96641..8846d6593a 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_PROVID 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.PackageInstaller.SessionParams; 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.R; 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 org.junit.runner.RunWith; 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 @@ import java.util.function.Consumer; */ @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 @@ public class TaplBindWidgetTest extends AbstractLauncherUiTest<Launcher> { 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 @@ public class TaplBindWidgetTest extends AbstractLauncherUiTest<Launcher> { } if (mSessionId > -1) { - mTargetContext.getPackageManager().getPackageInstaller().abandonSession(mSessionId); + targetContext().getPackageManager().getPackageInstaller().abandonSession(mSessionId); } } @@ -122,13 +121,12 @@ public class TaplBindWidgetTest extends AbstractLauncherUiTest<Launcher> { 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 @@ public class TaplBindWidgetTest extends AbstractLauncherUiTest<Launcher> { // 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 @@ public class TaplBindWidgetTest extends AbstractLauncherUiTest<Launcher> { 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 @@ public class TaplBindWidgetTest extends AbstractLauncherUiTest<Launcher> { // 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 @@ public class TaplBindWidgetTest extends AbstractLauncherUiTest<Launcher> { } 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 @@ public class TaplBindWidgetTest extends AbstractLauncherUiTest<Launcher> { 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 index fe3b2eea6c..2fb7987f05 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.ItemInfo; 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.Test; 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 @@ public class TaplRequestPinItemTest extends AbstractLauncherUiTest<Launcher> { @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 @@ public class TaplRequestPinItemTest extends AbstractLauncherUiTest<Launcher> { 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 @@ public class TaplRequestPinItemTest extends AbstractLauncherUiTest<Launcher> { 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 @@ public class TaplRequestPinItemTest extends AbstractLauncherUiTest<Launcher> { 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/TaplWidgetPickerTest.java b/tests/src/com/android/launcher3/ui/widget/WidgetPickerTest.java index 19c585085b..caad1d98b7 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 static org.junit.Assert.assertTrue; 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 @@ public class TaplWidgetPickerTest extends AbstractLauncherUiTest<Launcher> { @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)); - - widgets.flingBackward(); - executeOnLauncher(launcher -> assertTrue("Flinging backward didn't scroll widgets", - getWidgetsScroll(launcher) < flingForwardY)); + executeOnLauncher(AbstractFloatingView::closeAllOpenViews); + uiDevice.waitForIdle(); - 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 cfc0a6b286..c623513f07 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.ContentProviderClient; 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 com.android.launcher3.util.TestUtil; import org.junit.Test; -import java.util.ArrayDeque; -import java.util.Queue; - /** * Tests for theme icon support in Launcher * @@ -137,27 +130,10 @@ public class ThemeIconsTest extends BaseLauncherActivityTest<Launcher> { } 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 @@ public class ThemeIconsTest extends BaseLauncherActivityTest<Launcher> { 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 476e497807..61fa7d57ea 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.InputDevice 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 androidx.test.uiautomator.UiDevice 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 @@ open class BaseLauncherActivityTest<LAUNCHER_TYPE : Launcher> { ) .also { currentScenario = it } + @JvmField val uiDevice = UiDevice.getInstance(getInstrumentation()) + @After fun closeCurrentActivity() { currentScenario?.close() @@ -117,9 +124,10 @@ open class BaseLauncherActivityTest<LAUNCHER_TYPE : Launcher> { @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 @@ open class BaseLauncherActivityTest<LAUNCHER_TYPE : Launcher> { /* 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 0000000000..20881d19fb --- /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 7bdc040afc..3b8530984c 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 androidx.test.uiautomator.UiDevice; 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 @@ public class FailureWatcher extends TestWatcher { @Override protected void succeeded(Description description) { super.succeeded(description); - AbstractLauncherUiTest.checkDetectedLeaks(mLauncher); + BaseLauncherTaplTest.checkDetectedLeaks(mLauncher); } @Override |