1 /*
2  * Copyright (C) 2024 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.quickstep;
17 
18 import static com.android.app.animation.Interpolators.INSTANT;
19 import static com.android.app.animation.Interpolators.LINEAR;
20 import static com.android.launcher3.LauncherAnimUtils.VIEW_BACKGROUND_COLOR;
21 import static com.android.quickstep.GestureState.GestureEndTarget.LAST_TASK;
22 import static com.android.quickstep.GestureState.GestureEndTarget.RECENTS;
23 
24 import android.animation.Animator;
25 import android.animation.ObjectAnimator;
26 import android.content.Context;
27 import android.content.res.Resources;
28 import android.graphics.Color;
29 import android.graphics.PointF;
30 import android.graphics.Rect;
31 import android.view.Gravity;
32 import android.view.MotionEvent;
33 import android.view.RemoteAnimationTarget;
34 import android.view.View;
35 
36 import androidx.annotation.Nullable;
37 
38 import com.android.launcher3.DeviceProfile;
39 import com.android.launcher3.Flags;
40 import com.android.launcher3.R;
41 import com.android.launcher3.statehandlers.DesktopVisibilityController;
42 import com.android.launcher3.statemanager.BaseState;
43 import com.android.launcher3.taskbar.TaskbarUIController;
44 import com.android.launcher3.util.DisplayController;
45 import com.android.launcher3.views.ScrimView;
46 import com.android.quickstep.orientation.RecentsPagedOrientationHandler;
47 import com.android.quickstep.util.ActivityInitListener;
48 import com.android.quickstep.util.AnimatorControllerWithResistance;
49 import com.android.quickstep.views.RecentsView;
50 import com.android.quickstep.views.RecentsViewContainer;
51 import com.android.systemui.shared.recents.model.ThumbnailData;
52 
53 import java.util.HashMap;
54 import java.util.function.Consumer;
55 import java.util.function.Predicate;
56 
57 public abstract class BaseContainerInterface<STATE_TYPE extends BaseState<STATE_TYPE>,
58         CONTAINER_TYPE extends RecentsViewContainer> {
59 
60     public boolean rotationSupportedByActivity = false;
61 
62     @Nullable
getCreatedContainer()63     public abstract CONTAINER_TYPE getCreatedContainer();
64 
isInLiveTileMode()65     public abstract boolean isInLiveTileMode();
66 
onAssistantVisibilityChanged(float assistantVisibility)67     public abstract void onAssistantVisibilityChanged(float assistantVisibility);
68 
allowMinimizeSplitScreen()69     public abstract boolean allowMinimizeSplitScreen();
70 
isResumed()71     public abstract boolean isResumed();
72 
isStarted()73     public abstract boolean isStarted();
deferStartingActivity(RecentsAnimationDeviceState deviceState, MotionEvent ev)74     public abstract boolean deferStartingActivity(RecentsAnimationDeviceState deviceState,
75             MotionEvent ev);
76 
77     /** @return whether to allow going to All Apps from Overview. */
allowAllAppsFromOverview()78     public abstract boolean allowAllAppsFromOverview();
79 
80     /**
81      * Returns the color of the scrim behind overview when at rest in this state.
82      * Return {@link Color#TRANSPARENT} for no scrim.
83      */
getOverviewScrimColorForState(CONTAINER_TYPE container, STATE_TYPE state)84     protected abstract int getOverviewScrimColorForState(CONTAINER_TYPE container,
85             STATE_TYPE state);
86 
getSwipeUpDestinationAndLength( DeviceProfile dp, Context context, Rect outRect, RecentsPagedOrientationHandler orientationHandler)87     public abstract int getSwipeUpDestinationAndLength(
88             DeviceProfile dp, Context context, Rect outRect,
89             RecentsPagedOrientationHandler orientationHandler);
90 
91     @Nullable
getTaskbarController()92     public abstract TaskbarUIController getTaskbarController();
93 
prepareRecentsUI( RecentsAnimationDeviceState deviceState, boolean activityVisible, Consumer<AnimatorControllerWithResistance> callback)94     public abstract BaseActivityInterface.AnimationFactory prepareRecentsUI(
95             RecentsAnimationDeviceState deviceState, boolean activityVisible,
96             Consumer<AnimatorControllerWithResistance> callback);
97 
createActivityInitListener( Predicate<Boolean> onInitListener)98     public abstract ActivityInitListener createActivityInitListener(
99             Predicate<Boolean> onInitListener);
100     /**
101      * Returns the expected STATE_TYPE from the provided GestureEndTarget.
102      */
stateFromGestureEndTarget(GestureState.GestureEndTarget endTarget)103     public abstract STATE_TYPE stateFromGestureEndTarget(GestureState.GestureEndTarget endTarget);
104 
switchRunningTaskViewToScreenshot(HashMap<Integer, ThumbnailData> thumbnailDatas, Runnable runnable)105     public abstract void switchRunningTaskViewToScreenshot(HashMap<Integer,
106             ThumbnailData> thumbnailDatas, Runnable runnable);
107 
closeOverlay()108     public abstract void closeOverlay();
109 
getOverviewWindowBounds( Rect homeBounds, RemoteAnimationTarget target)110     public abstract Rect getOverviewWindowBounds(
111             Rect homeBounds, RemoteAnimationTarget target);
112 
onLaunchTaskFailed()113     public abstract void onLaunchTaskFailed();
114 
onExitOverview(RotationTouchHelper deviceState, Runnable exitRunnable)115     public abstract void onExitOverview(RotationTouchHelper deviceState,
116             Runnable exitRunnable);
117 
118     /** Called when the animation to home has fully settled. */
onSwipeUpToHomeComplete(RecentsAnimationDeviceState deviceState)119     public void onSwipeUpToHomeComplete(RecentsAnimationDeviceState deviceState) {}
120 
121     /**
122      * Sets a callback to be run when an activity launch happens while launcher is not yet resumed.
123      */
setOnDeferredActivityLaunchCallback(Runnable r)124     public void setOnDeferredActivityLaunchCallback(Runnable r) {}
125     /**
126      * @return Whether the gesture in progress should be cancelled.
127      */
shouldCancelCurrentGesture()128     public boolean shouldCancelCurrentGesture() {
129         return false;
130     }
131 
132     @Nullable
getDesktopVisibilityController()133     public DesktopVisibilityController getDesktopVisibilityController() {
134         return null;
135     }
136 
137     /**
138      * Called when the gesture ends and the animation starts towards the given target. Used to add
139      * an optional additional animation with the same duration.
140      */
getParallelAnimationToLauncher( GestureState.GestureEndTarget endTarget, long duration, RecentsAnimationCallbacks callbacks)141     public @Nullable Animator getParallelAnimationToLauncher(
142             GestureState.GestureEndTarget endTarget, long duration,
143             RecentsAnimationCallbacks callbacks) {
144         if (endTarget == RECENTS) {
145             CONTAINER_TYPE container = getCreatedContainer();
146             if (container == null) {
147                 return null;
148             }
149             RecentsView recentsView = container.getOverviewPanel();
150             STATE_TYPE state = stateFromGestureEndTarget(endTarget);
151             ScrimView scrimView = container.getScrimView();
152             ObjectAnimator anim = ObjectAnimator.ofArgb(scrimView, VIEW_BACKGROUND_COLOR,
153                     getOverviewScrimColorForState(container, state));
154             anim.setDuration(duration);
155             anim.setInterpolator(recentsView == null || !recentsView.isKeyboardTaskFocusPending()
156                     ? LINEAR : INSTANT);
157             return anim;
158         }
159         return null;
160     }
161 
162     /**
163      * Called when the animation to the target has finished, but right before updating the state.
164      * @return A View that needs to draw before ending the recents animation to LAST_TASK.
165      * (This is a hack to ensure Taskbar draws its background first to avoid flickering.)
166      */
onSettledOnEndTarget(GestureState.GestureEndTarget endTarget)167     public @Nullable View onSettledOnEndTarget(GestureState.GestureEndTarget endTarget) {
168         TaskbarUIController taskbarUIController = getTaskbarController();
169         if (taskbarUIController != null) {
170             taskbarUIController.setSystemGestureInProgress(false);
171             return taskbarUIController.getRootView();
172         }
173         return null;
174     }
175 
176     /**
177      * Called when the current gesture transition is cancelled.
178      * @param activityVisible Whether the user can see the changes we make here, so try to animate.
179      * @param endTarget If the gesture ended before we got cancelled, where we were headed.
180      */
onTransitionCancelled(boolean activityVisible, @Nullable GestureState.GestureEndTarget endTarget)181     public void onTransitionCancelled(boolean activityVisible,
182             @Nullable GestureState.GestureEndTarget endTarget) {
183         RecentsViewContainer container = getCreatedContainer();
184         if (container == null) {
185             return;
186         }
187         RecentsView recentsView = container.getOverviewPanel();
188         BaseState startState = recentsView.getStateManager().getRestState();
189         if (endTarget != null) {
190             // We were on our way to this state when we got canceled, end there instead.
191             startState = stateFromGestureEndTarget(endTarget);
192             DesktopVisibilityController controller = getDesktopVisibilityController();
193             if (controller != null && controller.areDesktopTasksVisible()
194                     && endTarget == LAST_TASK) {
195                 // When we are cancelling the transition and going back to last task, move to
196                 // rest state instead when desktop tasks are visible.
197                 // If a fullscreen task is visible, launcher goes to normal state when the
198                 // activity is stopped. This does not happen when desktop tasks are visible
199                 // on top of launcher. Force the launcher state to rest state here.
200                 startState = recentsView.getStateManager().getRestState();
201                 // Do not animate the transition
202                 activityVisible = false;
203             }
204         }
205         recentsView.getStateManager().goToState(startState, activityVisible);
206     }
207 
calculateTaskSize(Context context, DeviceProfile dp, Rect outRect, RecentsPagedOrientationHandler orientationHandler)208     public final void calculateTaskSize(Context context, DeviceProfile dp, Rect outRect,
209             RecentsPagedOrientationHandler orientationHandler) {
210         if (dp.isTablet) {
211             if (Flags.enableGridOnlyOverview()) {
212                 calculateGridTaskSize(context, dp, outRect, orientationHandler);
213             } else {
214                 calculateFocusTaskSize(context, dp, outRect);
215             }
216         } else {
217             Resources res = context.getResources();
218             float maxScale = res.getFloat(R.dimen.overview_max_scale);
219             int taskMargin = dp.overviewTaskMarginPx;
220             // In fake orientation, OverviewActions is hidden and we only leave a margin there.
221             int overviewActionsClaimedSpace = orientationHandler.isLayoutNaturalToLauncher()
222                     ? dp.getOverviewActionsClaimedSpace() : dp.overviewActionsTopMarginPx;
223             calculateTaskSizeInternal(
224                     context,
225                     dp,
226                     dp.overviewTaskThumbnailTopMarginPx,
227                     overviewActionsClaimedSpace,
228                     res.getDimensionPixelSize(R.dimen.overview_minimum_next_prev_size) + taskMargin,
229                     maxScale,
230                     Gravity.CENTER,
231                     outRect,
232                     orientationHandler);
233         }
234     }
235 
236     /**
237      * Calculates the taskView size for carousel during app to overview animation on tablets.
238      */
calculateCarouselTaskSize(Context context, DeviceProfile dp, Rect outRect, RecentsPagedOrientationHandler orientationHandler)239     public final void calculateCarouselTaskSize(Context context, DeviceProfile dp, Rect outRect,
240             RecentsPagedOrientationHandler orientationHandler) {
241         if (dp.isTablet && dp.isGestureMode) {
242             Resources res = context.getResources();
243             float minScale = res.getFloat(R.dimen.overview_carousel_min_scale);
244             Rect gridRect = new Rect();
245             calculateGridSize(dp, context, gridRect);
246             calculateTaskSizeInternal(context, dp, gridRect, minScale, Gravity.CENTER | Gravity.TOP,
247                     outRect);
248         } else {
249             calculateTaskSize(context, dp, outRect, orientationHandler);
250         }
251     }
252 
calculateFocusTaskSize(Context context, DeviceProfile dp, Rect outRect)253     private void calculateFocusTaskSize(Context context, DeviceProfile dp, Rect outRect) {
254         Resources res = context.getResources();
255         float maxScale = res.getFloat(R.dimen.overview_max_scale);
256         Rect gridRect = new Rect();
257         calculateGridSize(dp, context, gridRect);
258         calculateTaskSizeInternal(context, dp, gridRect, maxScale, Gravity.CENTER, outRect);
259     }
260 
calculateTaskSizeInternal(Context context, DeviceProfile dp, int claimedSpaceAbove, int claimedSpaceBelow, int minimumHorizontalPadding, float maxScale, int gravity, Rect outRect, RecentsPagedOrientationHandler orientationHandler)261     private void calculateTaskSizeInternal(Context context, DeviceProfile dp, int claimedSpaceAbove,
262             int claimedSpaceBelow, int minimumHorizontalPadding, float maxScale, int gravity,
263             Rect outRect, RecentsPagedOrientationHandler orientationHandler) {
264         Rect potentialTaskRect = new Rect(0, 0, dp.widthPx, dp.heightPx);
265 
266         Rect insets;
267         if (orientationHandler.isLayoutNaturalToLauncher()) {
268             insets = dp.getInsets();
269         } else {
270             Rect portraitInsets = dp.getInsets();
271             DisplayController displayController = DisplayController.INSTANCE.get(context);
272             Rect deviceRotationInsets = displayController.getInfo().getCurrentBounds().get(
273                     orientationHandler.getRotation()).insets;
274             // Obtain the landscape/seascape insets, and rotate it to portrait perspective.
275             orientationHandler.rotateInsets(deviceRotationInsets, outRect);
276             // Then combine with portrait's insets to leave space for status bar/nav bar in
277             // either orientations.
278             outRect.set(
279                     Math.max(outRect.left, portraitInsets.left),
280                     Math.max(outRect.top, portraitInsets.top),
281                     Math.max(outRect.right, portraitInsets.right),
282                     Math.max(outRect.bottom, portraitInsets.bottom)
283             );
284             insets = outRect;
285         }
286         potentialTaskRect.inset(insets);
287 
288         outRect.set(
289                 minimumHorizontalPadding,
290                 claimedSpaceAbove,
291                 minimumHorizontalPadding,
292                 claimedSpaceBelow);
293         // Rotate the paddings to portrait perspective,
294         orientationHandler.rotateInsets(outRect, outRect);
295         potentialTaskRect.inset(outRect);
296 
297         calculateTaskSizeInternal(context, dp, potentialTaskRect, maxScale, gravity, outRect);
298     }
299 
calculateTaskSizeInternal(Context context, DeviceProfile dp, Rect potentialTaskRect, float targetScale, int gravity, Rect outRect)300     private void calculateTaskSizeInternal(Context context, DeviceProfile dp,
301             Rect potentialTaskRect, float targetScale, int gravity, Rect outRect) {
302         PointF taskDimension = getTaskDimension(context, dp);
303 
304         float scale = Math.min(
305                 potentialTaskRect.width() / taskDimension.x,
306                 potentialTaskRect.height() / taskDimension.y);
307         scale = Math.min(scale, targetScale);
308         int outWidth = Math.round(scale * taskDimension.x);
309         int outHeight = Math.round(scale * taskDimension.y);
310 
311         Gravity.apply(gravity, outWidth, outHeight, potentialTaskRect, outRect);
312     }
313 
getTaskDimension(Context context, DeviceProfile dp)314     private static PointF getTaskDimension(Context context, DeviceProfile dp) {
315         PointF dimension = new PointF();
316         getTaskDimension(context, dp, dimension);
317         return dimension;
318     }
319 
320     /**
321      * Gets the dimension of the task in the current system state.
322      */
getTaskDimension(Context context, DeviceProfile dp, PointF out)323     public static void getTaskDimension(Context context, DeviceProfile dp, PointF out) {
324         out.x = dp.widthPx;
325         out.y = dp.heightPx;
326         if (dp.isTablet && !DisplayController.isTransientTaskbar(context)) {
327             out.y -= dp.taskbarHeight;
328         }
329     }
330 
331     /**
332      * Calculates the overview grid size for the provided device configuration.
333      */
calculateGridSize(DeviceProfile dp, Context context, Rect outRect)334     public final void calculateGridSize(DeviceProfile dp, Context context, Rect outRect) {
335         Rect insets = dp.getInsets();
336         int topMargin = dp.overviewTaskThumbnailTopMarginPx;
337         int bottomMargin = dp.getOverviewActionsClaimedSpace();
338         if (dp.isTaskbarPresent && Flags.enableGridOnlyOverview()) {
339             topMargin += context.getResources().getDimensionPixelSize(
340                     R.dimen.overview_top_margin_grid_only);
341             bottomMargin += context.getResources().getDimensionPixelSize(
342                     R.dimen.overview_bottom_margin_grid_only);
343         }
344         int sideMargin = dp.overviewGridSideMargin;
345 
346         outRect.set(0, 0, dp.widthPx, dp.heightPx);
347         outRect.inset(Math.max(insets.left, sideMargin), insets.top + topMargin,
348                 Math.max(insets.right, sideMargin), Math.max(insets.bottom, bottomMargin));
349     }
350 
351     /**
352      * Calculates the overview grid non-focused task size for the provided device configuration.
353      */
calculateGridTaskSize(Context context, DeviceProfile dp, Rect outRect, RecentsPagedOrientationHandler orientationHandler)354     public final void calculateGridTaskSize(Context context, DeviceProfile dp, Rect outRect,
355             RecentsPagedOrientationHandler orientationHandler) {
356         Resources res = context.getResources();
357         Rect potentialTaskRect = new Rect();
358         if (Flags.enableGridOnlyOverview()) {
359             calculateGridSize(dp, context, potentialTaskRect);
360         } else {
361             calculateFocusTaskSize(context, dp, potentialTaskRect);
362         }
363 
364         float rowHeight = (potentialTaskRect.height() + dp.overviewTaskThumbnailTopMarginPx
365                 - dp.overviewRowSpacing) / 2f;
366 
367         PointF taskDimension = getTaskDimension(context, dp);
368         float scale = (rowHeight - dp.overviewTaskThumbnailTopMarginPx) / taskDimension.y;
369         int outWidth = Math.round(scale * taskDimension.x);
370         int outHeight = Math.round(scale * taskDimension.y);
371 
372         int gravity = Gravity.TOP;
373         gravity |= orientationHandler.getRecentsRtlSetting(res) ? Gravity.RIGHT : Gravity.LEFT;
374         Gravity.apply(gravity, outWidth, outHeight, potentialTaskRect, outRect);
375     }
376 
377     /**
378      * Calculates the modal taskView size for the provided device configuration
379      */
calculateModalTaskSize(Context context, DeviceProfile dp, Rect outRect, RecentsPagedOrientationHandler orientationHandler)380     public final void calculateModalTaskSize(Context context, DeviceProfile dp, Rect outRect,
381             RecentsPagedOrientationHandler orientationHandler) {
382         calculateTaskSize(context, dp, outRect, orientationHandler);
383         boolean isGridOnlyOverview = dp.isTablet && Flags.enableGridOnlyOverview();
384         int claimedSpaceBelow = isGridOnlyOverview
385                 ? dp.overviewActionsTopMarginPx + dp.overviewActionsHeight + dp.stashedTaskbarHeight
386                 : (dp.heightPx - outRect.bottom - dp.getInsets().bottom);
387         int minimumHorizontalPadding = 0;
388         if (!isGridOnlyOverview) {
389             float maxScale = context.getResources().getFloat(R.dimen.overview_modal_max_scale);
390             minimumHorizontalPadding =
391                     Math.round((dp.availableWidthPx - outRect.width() * maxScale) / 2);
392         }
393         calculateTaskSizeInternal(
394                 context,
395                 dp,
396                 dp.overviewTaskMarginPx,
397                 claimedSpaceBelow,
398                 minimumHorizontalPadding,
399                 1f /*maxScale*/,
400                 Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM,
401                 outRect,
402                 orientationHandler);
403     }
404 }
405