ad6fee6111
This CL does the following: - Adds new methods `isInDesktopMode()` and `isInDesktopModeAndNotInOverview()` to `DesktopVisibilityController`. - These two methods rely on the new multi-desks impl when the flags are enabled. - Makes the existing `areDesktopTasksVisible()` and `areDesktopTasksVisibleAndNotInOverview()` private, and migrates all the usages to the new methods. - The new methods uses the old methods if the flags are disabled. A companion NexusLauncher CL is at ag/31601363. Bug: 394182435 Test: m Flag: com.android.window.flags.enable_multiple_desktops_frontend Flag: com.android.window.flags.enable_multiple_desktops_backend Change-Id: I29ff38f984bfe2f68a120f84e1a75fa36b739258
429 lines
18 KiB
Java
429 lines
18 KiB
Java
/*
|
|
* 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 static com.android.app.animation.Interpolators.INSTANT;
|
|
import static com.android.app.animation.Interpolators.LINEAR;
|
|
import static com.android.launcher3.LauncherAnimUtils.VIEW_BACKGROUND_COLOR;
|
|
import static com.android.quickstep.GestureState.GestureEndTarget.LAST_TASK;
|
|
import static com.android.quickstep.GestureState.GestureEndTarget.RECENTS;
|
|
|
|
import android.animation.Animator;
|
|
import android.animation.ObjectAnimator;
|
|
import android.content.Context;
|
|
import android.content.res.Resources;
|
|
import android.graphics.Color;
|
|
import android.graphics.PointF;
|
|
import android.graphics.Rect;
|
|
import android.view.Gravity;
|
|
import android.view.MotionEvent;
|
|
import android.view.RemoteAnimationTarget;
|
|
import android.view.View;
|
|
|
|
import androidx.annotation.Nullable;
|
|
import androidx.annotation.UiThread;
|
|
|
|
import com.android.launcher3.DeviceProfile;
|
|
import com.android.launcher3.Flags;
|
|
import com.android.launcher3.R;
|
|
import com.android.launcher3.statehandlers.DesktopVisibilityController;
|
|
import com.android.launcher3.statemanager.BaseState;
|
|
import com.android.launcher3.statemanager.StatefulContainer;
|
|
import com.android.launcher3.taskbar.TaskbarUIController;
|
|
import com.android.launcher3.util.DisplayController;
|
|
import com.android.launcher3.util.WindowBounds;
|
|
import com.android.launcher3.views.ScrimView;
|
|
import com.android.quickstep.orientation.RecentsPagedOrientationHandler;
|
|
import com.android.quickstep.util.AnimatorControllerWithResistance;
|
|
import com.android.quickstep.util.ContextInitListener;
|
|
import com.android.quickstep.views.RecentsView;
|
|
import com.android.quickstep.views.RecentsViewContainer;
|
|
import com.android.systemui.shared.recents.model.ThumbnailData;
|
|
|
|
import java.util.HashMap;
|
|
import java.util.List;
|
|
import java.util.function.Consumer;
|
|
import java.util.function.Predicate;
|
|
|
|
public abstract class BaseContainerInterface<STATE_TYPE extends BaseState<STATE_TYPE>,
|
|
CONTAINER_TYPE extends RecentsViewContainer & StatefulContainer<STATE_TYPE>> {
|
|
|
|
public boolean rotationSupportedByActivity = false;
|
|
protected final STATE_TYPE mBackgroundState;
|
|
|
|
protected BaseContainerInterface(STATE_TYPE backgroundState) {
|
|
mBackgroundState = backgroundState;
|
|
}
|
|
|
|
@UiThread
|
|
@Nullable
|
|
public abstract <T extends RecentsView<?,?>> T getVisibleRecentsView();
|
|
|
|
@UiThread
|
|
public abstract boolean switchToRecentsIfVisible(Animator.AnimatorListener animatorListener);
|
|
|
|
@Nullable
|
|
public abstract CONTAINER_TYPE getCreatedContainer();
|
|
|
|
@Nullable
|
|
protected Runnable mOnInitBackgroundStateUICallback = null;
|
|
|
|
public abstract boolean isInLiveTileMode();
|
|
|
|
public abstract void onAssistantVisibilityChanged(float assistantVisibility);
|
|
|
|
public abstract boolean isResumed();
|
|
|
|
public abstract boolean isStarted();
|
|
public abstract boolean deferStartingActivity(RecentsAnimationDeviceState deviceState,
|
|
MotionEvent ev);
|
|
|
|
/**
|
|
* Returns the color of the scrim behind overview when at rest in this state.
|
|
* Return {@link Color#TRANSPARENT} for no scrim.
|
|
*/
|
|
protected abstract int getOverviewScrimColorForState(CONTAINER_TYPE container,
|
|
STATE_TYPE state);
|
|
|
|
public abstract int getSwipeUpDestinationAndLength(
|
|
DeviceProfile dp, Context context, Rect outRect,
|
|
RecentsPagedOrientationHandler orientationHandler);
|
|
|
|
@Nullable
|
|
public abstract TaskbarUIController getTaskbarController();
|
|
|
|
public interface AnimationFactory {
|
|
|
|
void createContainerInterface(long transitionLength);
|
|
|
|
/**
|
|
* @param attached Whether to show RecentsView alongside the app window. If false, recents
|
|
* will be hidden by some property we can animate, e.g. alpha.
|
|
* @param animate Whether to animate recents to/from its new attached state.
|
|
* @param updateRunningTaskAlpha Whether to update the running task's attached alpha
|
|
*/
|
|
default void setRecentsAttachedToAppWindow(
|
|
boolean attached, boolean animate, boolean updateRunningTaskAlpha) { }
|
|
|
|
default boolean isRecentsAttachedToAppWindow() {
|
|
return false;
|
|
}
|
|
|
|
default boolean hasRecentsEverAttachedToAppWindow() {
|
|
return false;
|
|
}
|
|
|
|
/** Called when the gesture ends and we know what state it is going towards */
|
|
default void setEndTarget(GestureState.GestureEndTarget endTarget) { }
|
|
}
|
|
|
|
public abstract BaseContainerInterface.AnimationFactory prepareRecentsUI(
|
|
boolean activityVisible,
|
|
Consumer<AnimatorControllerWithResistance> callback);
|
|
|
|
public abstract ContextInitListener createActivityInitListener(
|
|
Predicate<Boolean> onInitListener);
|
|
/**
|
|
* Returns the expected STATE_TYPE from the provided GestureEndTarget.
|
|
*/
|
|
public abstract STATE_TYPE stateFromGestureEndTarget(GestureState.GestureEndTarget endTarget);
|
|
|
|
public abstract void switchRunningTaskViewToScreenshot(HashMap<Integer,
|
|
ThumbnailData> thumbnailDatas, Runnable runnable);
|
|
|
|
public abstract void closeOverlay();
|
|
|
|
public abstract Rect getOverviewWindowBounds(
|
|
Rect homeBounds, RemoteAnimationTarget target);
|
|
|
|
public abstract void onLaunchTaskFailed();
|
|
|
|
public abstract void onExitOverview(Runnable exitRunnable);
|
|
|
|
/** Called when the animation to home has fully settled. */
|
|
public void onSwipeUpToHomeComplete() {}
|
|
|
|
/**
|
|
* Sets a callback to be run when an activity launch happens while launcher is not yet resumed.
|
|
*/
|
|
public void setOnDeferredActivityLaunchCallback(Runnable r) {}
|
|
/**
|
|
* @return Whether the gesture in progress should be cancelled.
|
|
*/
|
|
public boolean shouldCancelCurrentGesture() {
|
|
return false;
|
|
}
|
|
|
|
public void runOnInitBackgroundStateUI(Runnable callback) {
|
|
StatefulContainer container = getCreatedContainer();
|
|
if (container != null
|
|
&& container.getStateManager().getState() == mBackgroundState) {
|
|
callback.run();
|
|
onInitBackgroundStateUI();
|
|
return;
|
|
}
|
|
mOnInitBackgroundStateUICallback = callback;
|
|
}
|
|
|
|
/**
|
|
* Called when the gesture ends and the animation starts towards the given target. Used to add
|
|
* an optional additional animation with the same duration.
|
|
*/
|
|
public @Nullable Animator getParallelAnimationToLauncher(
|
|
GestureState.GestureEndTarget endTarget, long duration,
|
|
RecentsAnimationCallbacks callbacks) {
|
|
if (endTarget == RECENTS) {
|
|
CONTAINER_TYPE container = getCreatedContainer();
|
|
if (container == null) {
|
|
return null;
|
|
}
|
|
RecentsView recentsView = container.getOverviewPanel();
|
|
STATE_TYPE state = stateFromGestureEndTarget(endTarget);
|
|
ScrimView scrimView = container.getScrimView();
|
|
ObjectAnimator anim = ObjectAnimator.ofArgb(scrimView, VIEW_BACKGROUND_COLOR,
|
|
getOverviewScrimColorForState(container, state));
|
|
anim.setDuration(duration);
|
|
anim.setInterpolator(recentsView == null || !recentsView.isKeyboardTaskFocusPending()
|
|
? LINEAR : INSTANT);
|
|
return anim;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Called when the animation to the target has finished, but right before updating the state.
|
|
* @return A View that needs to draw before ending the recents animation to LAST_TASK.
|
|
* (This is a hack to ensure Taskbar draws its background first to avoid flickering.)
|
|
*/
|
|
public @Nullable View onSettledOnEndTarget(GestureState.GestureEndTarget endTarget) {
|
|
TaskbarUIController taskbarUIController = getTaskbarController();
|
|
if (taskbarUIController != null) {
|
|
taskbarUIController.setSystemGestureInProgress(false);
|
|
return taskbarUIController.getRootView();
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Called when the current gesture transition is cancelled.
|
|
* @param activityVisible Whether the user can see the changes we make here, so try to animate.
|
|
* @param endTarget If the gesture ended before we got cancelled, where we were headed.
|
|
*/
|
|
public void onTransitionCancelled(boolean activityVisible,
|
|
@Nullable GestureState.GestureEndTarget endTarget) {
|
|
RecentsViewContainer container = getCreatedContainer();
|
|
if (container == null) {
|
|
return;
|
|
}
|
|
RecentsView recentsView = container.getOverviewPanel();
|
|
BaseState startState = recentsView.getStateManager().getRestState();
|
|
if (endTarget != null) {
|
|
// We were on our way to this state when we got canceled, end there instead.
|
|
startState = stateFromGestureEndTarget(endTarget);
|
|
final var context = recentsView.getContext();
|
|
if (DesktopVisibilityController.INSTANCE.get(context)
|
|
.isInDesktopModeAndNotInOverview(context.getDisplayId())
|
|
&& endTarget == LAST_TASK) {
|
|
// When we are cancelling the transition and going back to last task, move to
|
|
// rest state instead when desktop tasks are visible.
|
|
// If a fullscreen task is visible, launcher goes to normal state when the
|
|
// activity is stopped. This does not happen when desktop tasks are visible
|
|
// on top of launcher. Force the launcher state to rest state here.
|
|
startState = recentsView.getStateManager().getRestState();
|
|
// Do not animate the transition
|
|
activityVisible = false;
|
|
}
|
|
}
|
|
recentsView.getStateManager().goToState(startState, activityVisible);
|
|
}
|
|
|
|
public final void calculateTaskSize(Context context, DeviceProfile dp, Rect outRect,
|
|
RecentsPagedOrientationHandler orientationHandler) {
|
|
if (dp.isTablet) {
|
|
calculateLargeTileSize(context, dp, outRect);
|
|
} else {
|
|
Resources res = context.getResources();
|
|
float maxScale = res.getFloat(R.dimen.overview_max_scale);
|
|
int taskMargin = dp.overviewTaskMarginPx;
|
|
// In fake orientation, OverviewActions is hidden and we only leave a margin there.
|
|
int overviewActionsClaimedSpace = orientationHandler.isLayoutNaturalToLauncher()
|
|
? dp.getOverviewActionsClaimedSpace() : dp.overviewActionsTopMarginPx;
|
|
calculateTaskSizeInternal(
|
|
context,
|
|
dp,
|
|
dp.overviewTaskThumbnailTopMarginPx,
|
|
overviewActionsClaimedSpace,
|
|
res.getDimensionPixelSize(R.dimen.overview_minimum_next_prev_size) + taskMargin,
|
|
maxScale,
|
|
Gravity.CENTER,
|
|
outRect,
|
|
orientationHandler);
|
|
}
|
|
}
|
|
|
|
private void calculateLargeTileSize(Context context, DeviceProfile dp, Rect outRect) {
|
|
Resources res = context.getResources();
|
|
float maxScale = res.getFloat(R.dimen.overview_max_scale);
|
|
Rect gridRect = new Rect();
|
|
calculateGridSize(dp, context, gridRect);
|
|
calculateTaskSizeInternal(context, dp, gridRect, maxScale, Gravity.CENTER, outRect);
|
|
}
|
|
|
|
private void calculateTaskSizeInternal(Context context, DeviceProfile dp, int claimedSpaceAbove,
|
|
int claimedSpaceBelow, int minimumHorizontalPadding, float maxScale, int gravity,
|
|
Rect outRect, RecentsPagedOrientationHandler orientationHandler) {
|
|
Rect potentialTaskRect = new Rect(0, 0, dp.widthPx, dp.heightPx);
|
|
|
|
Rect insets;
|
|
if (orientationHandler.isLayoutNaturalToLauncher()) {
|
|
insets = dp.getInsets();
|
|
} else {
|
|
Rect portraitInsets = dp.getInsets();
|
|
DisplayController displayController = DisplayController.INSTANCE.get(context);
|
|
@Nullable List<WindowBounds> windowBounds =
|
|
displayController.getInfo().getCurrentBounds();
|
|
Rect deviceRotationInsets = windowBounds != null
|
|
? windowBounds.get(orientationHandler.getRotation()).insets
|
|
: new Rect();
|
|
// Obtain the landscape/seascape insets, and rotate it to portrait perspective.
|
|
orientationHandler.rotateInsets(deviceRotationInsets, outRect);
|
|
// Then combine with portrait's insets to leave space for status bar/nav bar in
|
|
// either orientations.
|
|
outRect.set(
|
|
Math.max(outRect.left, portraitInsets.left),
|
|
Math.max(outRect.top, portraitInsets.top),
|
|
Math.max(outRect.right, portraitInsets.right),
|
|
Math.max(outRect.bottom, portraitInsets.bottom)
|
|
);
|
|
insets = outRect;
|
|
}
|
|
potentialTaskRect.inset(insets);
|
|
|
|
outRect.set(
|
|
minimumHorizontalPadding,
|
|
claimedSpaceAbove,
|
|
minimumHorizontalPadding,
|
|
claimedSpaceBelow);
|
|
// Rotate the paddings to portrait perspective,
|
|
orientationHandler.rotateInsets(outRect, outRect);
|
|
potentialTaskRect.inset(outRect);
|
|
|
|
calculateTaskSizeInternal(context, dp, potentialTaskRect, maxScale, gravity, outRect);
|
|
}
|
|
|
|
private void calculateTaskSizeInternal(Context context, DeviceProfile dp,
|
|
Rect potentialTaskRect, float targetScale, int gravity, Rect outRect) {
|
|
PointF taskDimension = getTaskDimension(context, dp);
|
|
|
|
float scale = Math.min(
|
|
potentialTaskRect.width() / taskDimension.x,
|
|
potentialTaskRect.height() / taskDimension.y);
|
|
scale = Math.min(scale, targetScale);
|
|
int outWidth = Math.round(scale * taskDimension.x);
|
|
int outHeight = Math.round(scale * taskDimension.y);
|
|
|
|
Gravity.apply(gravity, outWidth, outHeight, potentialTaskRect, outRect);
|
|
}
|
|
|
|
private static PointF getTaskDimension(Context context, DeviceProfile dp) {
|
|
PointF dimension = new PointF();
|
|
getTaskDimension(context, dp, dimension);
|
|
return dimension;
|
|
}
|
|
|
|
/**
|
|
* Gets the dimension of the task in the current system state.
|
|
*/
|
|
public static void getTaskDimension(Context context, DeviceProfile dp, PointF out) {
|
|
out.x = dp.widthPx;
|
|
out.y = dp.heightPx;
|
|
if (dp.isTablet && !DisplayController.isTransientTaskbar(context)) {
|
|
out.y -= dp.taskbarHeight;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Calculates the overview grid size for the provided device configuration.
|
|
*/
|
|
public final void calculateGridSize(DeviceProfile dp, Context context, Rect outRect) {
|
|
Rect insets = dp.getInsets();
|
|
int topMargin = dp.overviewTaskThumbnailTopMarginPx;
|
|
int bottomMargin = dp.getOverviewActionsClaimedSpace();
|
|
int sideMargin = dp.overviewGridSideMargin;
|
|
|
|
outRect.set(0, 0, dp.widthPx, dp.heightPx);
|
|
outRect.inset(Math.max(insets.left, sideMargin), insets.top + topMargin,
|
|
Math.max(insets.right, sideMargin), Math.max(insets.bottom, bottomMargin));
|
|
}
|
|
|
|
/**
|
|
* Calculates the overview grid non-focused task size for the provided device configuration.
|
|
*/
|
|
public final void calculateGridTaskSize(Context context, DeviceProfile dp, Rect outRect,
|
|
RecentsPagedOrientationHandler orientationHandler) {
|
|
Resources res = context.getResources();
|
|
Rect potentialTaskRect = new Rect();
|
|
calculateLargeTileSize(context, dp, potentialTaskRect);
|
|
|
|
float rowHeight = (potentialTaskRect.height() + dp.overviewTaskThumbnailTopMarginPx
|
|
- dp.overviewRowSpacing) / 2f;
|
|
|
|
PointF taskDimension = getTaskDimension(context, dp);
|
|
float scale = (rowHeight - dp.overviewTaskThumbnailTopMarginPx) / taskDimension.y;
|
|
int outWidth = Math.round(scale * taskDimension.x);
|
|
int outHeight = Math.round(scale * taskDimension.y);
|
|
|
|
int gravity = Gravity.TOP;
|
|
gravity |= orientationHandler.getRecentsRtlSetting(res) ? Gravity.RIGHT : Gravity.LEFT;
|
|
Gravity.apply(gravity, outWidth, outHeight, potentialTaskRect, outRect);
|
|
}
|
|
|
|
/**
|
|
* Calculates the modal taskView size for the provided device configuration
|
|
*/
|
|
public final void calculateModalTaskSize(Context context, DeviceProfile dp, Rect outRect,
|
|
RecentsPagedOrientationHandler orientationHandler) {
|
|
calculateTaskSize(context, dp, outRect, orientationHandler);
|
|
boolean isGridOnlyOverview = dp.isTablet && Flags.enableGridOnlyOverview();
|
|
int claimedSpaceBelow = isGridOnlyOverview
|
|
? dp.overviewActionsTopMarginPx + dp.overviewActionsHeight + dp.stashedTaskbarHeight
|
|
: (dp.heightPx - outRect.bottom - dp.getInsets().bottom);
|
|
int minimumHorizontalPadding = 0;
|
|
if (!isGridOnlyOverview) {
|
|
float maxScale = context.getResources().getFloat(R.dimen.overview_modal_max_scale);
|
|
minimumHorizontalPadding =
|
|
Math.round((dp.availableWidthPx - outRect.width() * maxScale) / 2);
|
|
}
|
|
calculateTaskSizeInternal(
|
|
context,
|
|
dp,
|
|
dp.overviewTaskMarginPx,
|
|
claimedSpaceBelow,
|
|
minimumHorizontalPadding,
|
|
1f /*maxScale*/,
|
|
Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM,
|
|
outRect,
|
|
orientationHandler);
|
|
}
|
|
|
|
protected void onInitBackgroundStateUI() {
|
|
if (mOnInitBackgroundStateUICallback != null) {
|
|
mOnInitBackgroundStateUICallback.run();
|
|
mOnInitBackgroundStateUICallback = null;
|
|
}
|
|
}
|
|
}
|