content: /*
   * Copyright (C) 2019 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 android.view.View.TRANSLATION_Y;
  
  import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
  import static com.android.launcher3.LauncherAppTransitionManagerImpl.INDEX_RECENTS_FADE_ANIM;
  import static com.android.launcher3.LauncherAppTransitionManagerImpl.INDEX_RECENTS_TRANSLATE_X_ANIM;
  import static com.android.launcher3.LauncherAppTransitionManagerImpl.INDEX_SHELF_ANIM;
  import static com.android.launcher3.LauncherState.BACKGROUND_APP;
  import static com.android.launcher3.LauncherState.NORMAL;
  import static com.android.launcher3.LauncherState.OVERVIEW;
  import static com.android.launcher3.anim.Interpolators.ACCEL_2;
  import static com.android.launcher3.anim.Interpolators.ACCEL_DEACCEL;
  import static com.android.launcher3.anim.Interpolators.INSTANT;
  import static com.android.launcher3.anim.Interpolators.LINEAR;
  import static com.android.quickstep.WindowTransformSwipeHandler.RECENTS_ATTACH_DURATION;
  
  import android.animation.Animator;
  import android.animation.AnimatorSet;
  import android.animation.ObjectAnimator;
  import android.animation.TimeInterpolator;
  import android.content.Context;
  import android.graphics.Rect;
  import android.graphics.RectF;
  import android.graphics.Region;
  import android.os.UserHandle;
  import android.view.MotionEvent;
  import android.view.View;
  import android.view.animation.Interpolator;
  
  import androidx.annotation.NonNull;
  import androidx.annotation.Nullable;
  import androidx.annotation.UiThread;
  
  import com.android.launcher3.DeviceProfile;
  import com.android.launcher3.Launcher;
  import com.android.launcher3.LauncherAppState;
  import com.android.launcher3.LauncherInitListenerEx;
  import com.android.launcher3.LauncherState;
  import com.android.launcher3.QuickstepAppTransitionManagerImpl;
  import com.android.launcher3.allapps.DiscoveryBounce;
  import com.android.launcher3.anim.AnimatorPlaybackController;
  import com.android.launcher3.userevent.nano.LauncherLogProto;
  import com.android.launcher3.views.FloatingIconView;
  import com.android.quickstep.SysUINavigationMode.Mode;
  import com.android.quickstep.util.LayoutUtils;
  import com.android.quickstep.util.ShelfPeekAnim;
  import com.android.quickstep.util.ShelfPeekAnim.ShelfAnimState;
  import com.android.quickstep.util.StaggeredWorkspaceAnim;
  import com.android.quickstep.views.LauncherRecentsView;
  import com.android.quickstep.views.RecentsView;
  import com.android.quickstep.views.TaskView;
  import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
  
  import java.util.function.BiPredicate;
  import java.util.function.Consumer;
  
  /**
   * {@link ActivityControlHelper} for the in-launcher recents.
   */
  public final class LauncherActivityControllerHelper implements ActivityControlHelper<Launcher> {
  
      private Runnable mAdjustInterpolatorsRunnable;
  
      @Override
      public int getSwipeUpDestinationAndLength(DeviceProfile dp, Context context, Rect outRect) {
          LayoutUtils.calculateLauncherTaskSize(context, dp, outRect);
          if (dp.isVerticalBarLayout() && SysUINavigationMode.getMode(context) != Mode.NO_BUTTON) {
              Rect targetInsets = dp.getInsets();
              int hotseatInset = dp.isSeascape() ? targetInsets.left : targetInsets.right;
              return dp.hotseatBarSizePx + hotseatInset;
          } else {
              return LayoutUtils.getShelfTrackingDistance(context, dp);
          }
      }
  
      @Override
      public void onTransitionCancelled(Launcher activity, boolean activityVisible) {
          LauncherState startState = activity.getStateManager().getRestState();
          activity.getStateManager().goToState(startState, activityVisible);
      }
  
      @Override
      public void onSwipeUpToRecentsComplete(Launcher activity) {
          // Re apply state in case we did something funky during the transition.
          activity.getStateManager().reapplyState();
          DiscoveryBounce.showForOverviewIfNeeded(activity);
      }
  
      @Override
      public void onSwipeUpToHomeComplete(Launcher activity) {
          // Ensure recents is at the correct position for NORMAL state. For example, when we detach
          // recents, we assume the first task is invisible, making translation off by one task.
          activity.getStateManager().reapplyState();
      }
  
      @Override
      public void onAssistantVisibilityChanged(float visibility) {
          Launcher launcher = getCreatedActivity();
          if (launcher != null) {
              launcher.onAssistantVisibilityChanged(visibility);
          }
      }
  
      @NonNull
      @Override
      public HomeAnimationFactory prepareHomeUI(Launcher activity) {
          final DeviceProfile dp = activity.getDeviceProfile();
          final RecentsView recentsView = activity.getOverviewPanel();
          final TaskView runningTaskView = recentsView.getRunningTaskView();
          final View workspaceView;
          if (runningTaskView != null && runningTaskView.getTask().key.getComponent() != null) {
              workspaceView = activity.getWorkspace().getFirstMatchForAppClose(
                      runningTaskView.getTask().key.getComponent().getPackageName(),
                      UserHandle.of(runningTaskView.getTask().key.userId));
          } else {
              workspaceView = null;
          }
          final RectF iconLocation = new RectF();
          boolean canUseWorkspaceView = workspaceView != null && workspaceView.isAttachedToWindow();
          FloatingIconView floatingIconView = canUseWorkspaceView
                  ? FloatingIconView.getFloatingIconView(activity, workspaceView,
                          true /* hideOriginal */, iconLocation, false /* isOpening */)
                  : null;
  
          return new HomeAnimationFactory() {
              @Nullable
              @Override
              public View getFloatingView() {
                  return floatingIconView;
              }
  
              @NonNull
              @Override
              public RectF getWindowTargetRect() {
                  if (canUseWorkspaceView) {
                      return iconLocation;
                  } else {
                      return HomeAnimationFactory.getDefaultWindowTargetRect(dp);
                  }
              }
  
              @NonNull
              @Override
              public AnimatorPlaybackController createActivityAnimationToHome() {
                  // Return an empty APC here since we have an non-user controlled animation to home.
                  long accuracy = 2 * Math.max(dp.widthPx, dp.heightPx);
                  return activity.getStateManager().createAnimationToNewWorkspace(NORMAL, accuracy,
                          0 /* animComponents */);
              }
  
              @Override
              public void playAtomicAnimation(float velocity) {
                  new StaggeredWorkspaceAnim(activity, velocity, true /* animateOverviewScrim */)
                          .start();
              }
          };
      }
  
      @Override
      public AnimationFactory prepareRecentsUI(Launcher activity, boolean activityVisible,
              boolean animateActivity, Consumer<AnimatorPlaybackController> callback) {
          final LauncherState startState = activity.getStateManager().getState();
  
          LauncherState resetState = startState;
          if (startState.disableRestore) {
              resetState = activity.getStateManager().getRestState();
          }
          activity.getStateManager().setRestState(resetState);
  
          final LauncherState fromState = animateActivity ? BACKGROUND_APP : OVERVIEW;
          activity.getStateManager().goToState(fromState, false);
          // Since all apps is not visible, we can safely reset the scroll position.
          // This ensures then the next swipe up to all-apps starts from scroll 0.
          activity.getAppsView().reset(false /* animate */);
  
          return new AnimationFactory() {
              private final ShelfPeekAnim mShelfAnim =
                      ((QuickstepAppTransitionManagerImpl) activity.getAppTransitionManager())
                              .getShelfPeekAnim();
              private boolean mIsAttachedToWindow;
  
              @Override
              public void createActivityController(long transitionLength) {
                  createActivityControllerInternal(activity, fromState, transitionLength, callback);
                  // Creating the activity controller animation sometimes reapplies the launcher state
                  // (because we set the animation as the current state animation), so we reapply the
                  // attached state here as well to ensure recents is shown/hidden appropriately.
                  if (SysUINavigationMode.getMode(activity) == Mode.NO_BUTTON) {
                      setRecentsAttachedToAppWindow(mIsAttachedToWindow, false);
                  }
              }
  
              @Override
              public void adjustActivityControllerInterpolators() {
                  if (mAdjustInterpolatorsRunnable != null) {
                      mAdjustInterpolatorsRunnable.run();
                  }
              }
  
              @Override
              public void onTransitionCancelled() {
                  activity.getStateManager().goToState(startState, false /* animate */);
              }
  
              @Override
              public void setShelfState(ShelfAnimState shelfState, Interpolator interpolator,
                      long duration) {
                  mShelfAnim.setShelfState(shelfState, interpolator, duration);
              }
  
              @Override
              public void setRecentsAttachedToAppWindow(boolean attached, boolean animate) {
                  if (mIsAttachedToWindow == attached && animate) {
                      return;
                  }
                  mIsAttachedToWindow = attached;
                  LauncherRecentsView recentsView = activity.getOverviewPanel();
                  Animator fadeAnim = activity.getStateManager()
                          .createStateElementAnimation(
                          INDEX_RECENTS_FADE_ANIM, attached ? 1 : 0);
  
                  int runningTaskIndex = recentsView.getRunningTaskIndex();
                  if (runningTaskIndex == recentsView.getTaskViewStartIndex()) {
                      // If we are on the first task (we haven't quick switched), translate recents in
                      // from the side. Calculate the start translation based on current scale/scroll.
                      float currScale = recentsView.getScaleX();
                      float scrollOffsetX = recentsView.getScrollOffset();
                      float offscreenX = recentsView.getOffscreenTranslationX(currScale);
  
                      float fromTranslationX = attached ? offscreenX - scrollOffsetX : 0;
                      float toTranslationX = attached ? 0 : offscreenX - scrollOffsetX;
                      activity.getStateManager()
                              .cancelStateElementAnimation(INDEX_RECENTS_TRANSLATE_X_ANIM);
  
                      if (!recentsView.isShown() && animate) {
                          recentsView.setTranslationX(fromTranslationX);
                      } else {
                          fromTranslationX = recentsView.getTranslationX();
                      }
  
                      if (!animate) {
                          recentsView.setTranslationX(toTranslationX);
                      } else {
                          activity.getStateManager().createStateElementAnimation(
                                  INDEX_RECENTS_TRANSLATE_X_ANIM,
                                  fromTranslationX, toTranslationX).start();
                      }
  
                      fadeAnim.setInterpolator(attached ? INSTANT : ACCEL_2);
                  } else {
                      fadeAnim.setInterpolator(ACCEL_DEACCEL);
                  }
                  fadeAnim.setDuration(animate ? RECENTS_ATTACH_DURATION : 0).start();
              }
          };
      }
  
      private void createActivityControllerInternal(Launcher activity, LauncherState fromState,
              long transitionLength, Consumer<AnimatorPlaybackController> callback) {
          LauncherState endState = OVERVIEW;
          if (fromState == endState) {
              return;
          }
  
          AnimatorSet anim = new AnimatorSet();
          if (!activity.getDeviceProfile().isVerticalBarLayout()
                  && SysUINavigationMode.getMode(activity) != Mode.NO_BUTTON) {
              // Don't animate the shelf when the mode is NO_BUTTON, because we update it atomically.
              anim.play(activity.getStateManager().createStateElementAnimation(
                      INDEX_SHELF_ANIM,
                      fromState.getVerticalProgress(activity),
                      endState.getVerticalProgress(activity)));
          }
          playScaleDownAnim(anim, activity, fromState, endState);
  
          anim.setDuration(transitionLength * 2);
          anim.setInterpolator(LINEAR);
          AnimatorPlaybackController controller =
                  AnimatorPlaybackController.wrap(anim, transitionLength * 2);
          activity.getStateManager().setCurrentUserControlledAnimation(controller);
  
          // Since we are changing the start position of the UI, reapply the state, at the end
          controller.setEndAction(() -> {
              activity.getStateManager().goToState(
                      controller.getInterpolatedProgress() > 0.5 ? endState : fromState, false);
          });
          callback.accept(controller);
      }
  
      /**
       * Scale down recents from the center task being full screen to being in overview.
       */
      private void playScaleDownAnim(AnimatorSet anim, Launcher launcher, LauncherState fromState,
              LauncherState endState) {
          RecentsView recentsView = launcher.getOverviewPanel();
          if (recentsView.getCurrentPageTaskView() == null) {
              return;
          }
  
          LauncherState.ScaleAndTranslation fromScaleAndTranslation
                  = fromState.getOverviewScaleAndTranslation(launcher);
          LauncherState.ScaleAndTranslation endScaleAndTranslation
                  = endState.getOverviewScaleAndTranslation(launcher);
          float fromTranslationY = fromScaleAndTranslation.translationY;
          float endTranslationY = endScaleAndTranslation.translationY;
          float fromFullscreenProgress = fromState.getOverviewFullscreenProgress();
          float endFullscreenProgress = endState.getOverviewFullscreenProgress();
  
          Animator scale = ObjectAnimator.ofFloat(recentsView, SCALE_PROPERTY,
                  fromScaleAndTranslation.scale, endScaleAndTranslation.scale);
          Animator translateY = ObjectAnimator.ofFloat(recentsView, TRANSLATION_Y,
                  fromTranslationY, endTranslationY);
          Animator applyFullscreenProgress = ObjectAnimator.ofFloat(recentsView,
                  RecentsView.FULLSCREEN_PROGRESS, fromFullscreenProgress, endFullscreenProgress);
          anim.playTogether(scale, translateY, applyFullscreenProgress);
  
          mAdjustInterpolatorsRunnable = () -> {
              // Adjust the translateY interpolator to account for the running task's top inset.
              // When progress <= 1, this is handled by each task view as they set their fullscreen
              // progress. However, once we go to progress > 1, fullscreen progress stays at 0, so
              // recents as a whole needs to translate further to keep up with the app window.
              TaskView runningTaskView = recentsView.getRunningTaskView();
              if (runningTaskView == null) {
                  runningTaskView = recentsView.getCurrentPageTaskView();
                  if (runningTaskView == null) {
                      // There are no task views in LockTask mode when Overview is enabled.
                      return;
                  }
              }
              TimeInterpolator oldInterpolator = translateY.getInterpolator();
              Rect fallbackInsets = launcher.getDeviceProfile().getInsets();
              float extraTranslationY = runningTaskView.getThumbnail().getInsets(fallbackInsets).top;
              float normalizedTranslationY = extraTranslationY / (fromTranslationY - endTranslationY);
              translateY.setInterpolator(t -> {
                  float newT = oldInterpolator.getInterpolation(t);
                  return newT <= 1f ? newT : newT + normalizedTranslationY * (newT - 1);
              });
          };
      }
  
      @Override
      public ActivityInitListener createActivityInitListener(
              BiPredicate<Launcher, Boolean> onInitListener) {
          return new LauncherInitListenerEx(onInitListener);
      }
  
      @Nullable
      @Override
      public Launcher getCreatedActivity() {
          LauncherAppState app = LauncherAppState.getInstanceNoCreate();
          if (app == null) {
              return null;
          }
          return (Launcher) app.getModel().getCallback();
      }
  
      @Nullable
      @UiThread
      private Launcher getVisibleLauncher() {
          Launcher launcher = getCreatedActivity();
          return (launcher != null) && launcher.isStarted() && launcher.hasWindowFocus() ?
                  launcher : null;
      }
  
      @Nullable
      @Override
      public RecentsView getVisibleRecentsView() {
          Launcher launcher = getVisibleLauncher();
          return launcher != null && launcher.getStateManager().getState().overviewUi
                  ? launcher.getOverviewPanel() : null;
      }
  
      @Override
      public boolean switchToRecentsIfVisible(Runnable onCompleteCallback) {
          Launcher launcher = getVisibleLauncher();
          if (launcher == null) {
              return false;
          }
  
          launcher.getUserEventDispatcher().logActionCommand(
                  LauncherLogProto.Action.Command.RECENTS_BUTTON,
                  getContainerType(),
                  LauncherLogProto.ContainerType.TASKSWITCHER);
          launcher.getStateManager().goToState(OVERVIEW,
                  launcher.getStateManager().shouldAnimateStateChange(), onCompleteCallback);
          return true;
      }
  
      @Override
      public boolean deferStartingActivity(Region activeNavBarRegion, MotionEvent ev) {
          return activeNavBarRegion.contains((int) ev.getX(), (int) ev.getY());
      }
  
      @Override
      public Rect getOverviewWindowBounds(Rect homeBounds, RemoteAnimationTargetCompat target) {
          return homeBounds;
      }
  
      @Override
      public boolean shouldMinimizeSplitScreen() {
          return true;
      }
  
      @Override
      public int getContainerType() {
          final Launcher launcher = getVisibleLauncher();
          return launcher != null ? launcher.getStateManager().getState().containerType
                  : LauncherLogProto.ContainerType.APP;
      }
  
      @Override
      public boolean isInLiveTileMode() {
          Launcher launcher = getCreatedActivity();
          return launcher != null && launcher.getStateManager().getState() == OVERVIEW &&
                  launcher.isStarted();
      }
  
      @Override
      public void onLaunchTaskFailed(Launcher launcher) {
          launcher.getStateManager().goToState(OVERVIEW);
      }
  
      @Override
      public void onLaunchTaskSuccess(Launcher launcher) {
          launcher.getStateManager().moveToRestState();
      }
  }
  