1 /*
2  * Copyright (C) 2018 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.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
19 import static com.android.launcher3.anim.Interpolators.ACCEL_2;
20 import static com.android.launcher3.anim.Interpolators.INSTANT;
21 import static com.android.launcher3.anim.Interpolators.LINEAR;
22 import static com.android.launcher3.config.FeatureFlags.ENABLE_OVERVIEW_ACTIONS;
23 import static com.android.quickstep.BaseSwipeUpHandlerV2.RECENTS_ATTACH_DURATION;
24 import static com.android.quickstep.SysUINavigationMode.getMode;
25 import static com.android.quickstep.SysUINavigationMode.hideShelfInTwoButtonLandscape;
26 import static com.android.quickstep.SysUINavigationMode.removeShelfFromOverview;
27 import static com.android.quickstep.util.RecentsAtomicAnimationFactory.INDEX_RECENTS_FADE_ANIM;
28 import static com.android.quickstep.util.RecentsAtomicAnimationFactory.INDEX_RECENTS_TRANSLATE_X_ANIM;
29 import static com.android.quickstep.views.RecentsView.ADJACENT_PAGE_OFFSET;
30 import static com.android.quickstep.views.RecentsView.FULLSCREEN_PROGRESS;
31 
32 import android.animation.Animator;
33 import android.annotation.TargetApi;
34 import android.content.Context;
35 import android.content.res.Resources;
36 import android.graphics.Rect;
37 import android.os.Build;
38 import android.view.MotionEvent;
39 import android.view.animation.Interpolator;
40 
41 import androidx.annotation.Nullable;
42 import androidx.annotation.UiThread;
43 
44 import com.android.launcher3.DeviceProfile;
45 import com.android.launcher3.R;
46 import com.android.launcher3.anim.AnimatorPlaybackController;
47 import com.android.launcher3.anim.PendingAnimation;
48 import com.android.launcher3.statehandlers.DepthController;
49 import com.android.launcher3.statemanager.BaseState;
50 import com.android.launcher3.statemanager.StatefulActivity;
51 import com.android.launcher3.touch.PagedOrientationHandler;
52 import com.android.launcher3.util.WindowBounds;
53 import com.android.quickstep.SysUINavigationMode.Mode;
54 import com.android.quickstep.util.ActivityInitListener;
55 import com.android.quickstep.util.ShelfPeekAnim;
56 import com.android.quickstep.util.SplitScreenBounds;
57 import com.android.quickstep.views.RecentsView;
58 import com.android.systemui.shared.recents.model.ThumbnailData;
59 import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
60 
61 import java.util.function.Consumer;
62 import java.util.function.Predicate;
63 
64 /**
65  * Utility class which abstracts out the logical differences between Launcher and RecentsActivity.
66  */
67 @TargetApi(Build.VERSION_CODES.P)
68 public abstract class BaseActivityInterface<STATE_TYPE extends BaseState<STATE_TYPE>,
69         ACTIVITY_TYPE extends StatefulActivity<STATE_TYPE>> {
70 
71     public final boolean rotationSupportedByActivity;
72 
73     private final STATE_TYPE mOverviewState, mBackgroundState;
74 
BaseActivityInterface(boolean rotationSupportedByActivity, STATE_TYPE overviewState, STATE_TYPE backgroundState)75     protected BaseActivityInterface(boolean rotationSupportedByActivity,
76             STATE_TYPE overviewState, STATE_TYPE backgroundState) {
77         this.rotationSupportedByActivity = rotationSupportedByActivity;
78         mOverviewState = overviewState;
79         mBackgroundState = backgroundState;
80     }
81 
onTransitionCancelled(boolean activityVisible)82     public void onTransitionCancelled(boolean activityVisible) {
83         ACTIVITY_TYPE activity = getCreatedActivity();
84         if (activity == null) {
85             return;
86         }
87         STATE_TYPE startState = activity.getStateManager().getRestState();
88         activity.getStateManager().goToState(startState, activityVisible);
89     }
90 
getSwipeUpDestinationAndLength( DeviceProfile dp, Context context, Rect outRect, PagedOrientationHandler orientationHandler)91     public abstract int getSwipeUpDestinationAndLength(
92             DeviceProfile dp, Context context, Rect outRect,
93             PagedOrientationHandler orientationHandler);
94 
onSwipeUpToRecentsComplete()95     public void onSwipeUpToRecentsComplete() {
96         // Re apply state in case we did something funky during the transition.
97         ACTIVITY_TYPE activity = getCreatedActivity();
98         if (activity == null) {
99             return;
100         }
101         activity.getStateManager().reapplyState();
102     }
103 
onSwipeUpToHomeComplete(RecentsAnimationDeviceState deviceState)104     public abstract void onSwipeUpToHomeComplete(RecentsAnimationDeviceState deviceState);
105 
onAssistantVisibilityChanged(float visibility)106     public abstract void onAssistantVisibilityChanged(float visibility);
107 
prepareRecentsUI(RecentsAnimationDeviceState deviceState, boolean activityVisible, Consumer<AnimatorPlaybackController> callback)108     public abstract AnimationFactory prepareRecentsUI(RecentsAnimationDeviceState deviceState,
109             boolean activityVisible, Consumer<AnimatorPlaybackController> callback);
110 
createActivityInitListener( Predicate<Boolean> onInitListener)111     public abstract ActivityInitListener createActivityInitListener(
112             Predicate<Boolean> onInitListener);
113 
114     /**
115      * Sets a callback to be run when an activity launch happens while launcher is not yet resumed.
116      */
setOnDeferredActivityLaunchCallback(Runnable r)117     public void setOnDeferredActivityLaunchCallback(Runnable r) {}
118 
119     @Nullable
getCreatedActivity()120     public abstract ACTIVITY_TYPE getCreatedActivity();
121 
122     @Nullable
getDepthController()123     public DepthController getDepthController() {
124         return null;
125     }
126 
isResumed()127     public final boolean isResumed() {
128         ACTIVITY_TYPE activity = getCreatedActivity();
129         return activity != null && activity.hasBeenResumed();
130     }
131 
isStarted()132     public final boolean isStarted() {
133         ACTIVITY_TYPE activity = getCreatedActivity();
134         return activity != null && activity.isStarted();
135     }
136 
137     @UiThread
138     @Nullable
getVisibleRecentsView()139     public abstract <T extends RecentsView> T getVisibleRecentsView();
140 
141     @UiThread
switchToRecentsIfVisible(Runnable onCompleteCallback)142     public abstract boolean switchToRecentsIfVisible(Runnable onCompleteCallback);
143 
getOverviewWindowBounds( Rect homeBounds, RemoteAnimationTargetCompat target)144     public abstract Rect getOverviewWindowBounds(
145             Rect homeBounds, RemoteAnimationTargetCompat target);
146 
allowMinimizeSplitScreen()147     public abstract boolean allowMinimizeSplitScreen();
148 
deferStartingActivity(RecentsAnimationDeviceState deviceState, MotionEvent ev)149     public boolean deferStartingActivity(RecentsAnimationDeviceState deviceState, MotionEvent ev) {
150         return deviceState.isInDeferredGestureRegion(ev);
151     }
152 
onExitOverview(RecentsAnimationDeviceState deviceState, Runnable exitRunnable)153     public abstract void onExitOverview(RecentsAnimationDeviceState deviceState,
154             Runnable exitRunnable);
155 
156     /**
157      * Updates the prediction state to the overview state.
158      */
updateOverviewPredictionState()159     public void updateOverviewPredictionState() {
160         // By public overview predictions are not supported
161     }
162 
163     /**
164      * Used for containerType in {@link com.android.launcher3.logging.UserEventDispatcher}
165      */
getContainerType()166     public abstract int getContainerType();
167 
isInLiveTileMode()168     public abstract boolean isInLiveTileMode();
169 
onLaunchTaskFailed()170     public abstract void onLaunchTaskFailed();
171 
onLaunchTaskSuccess()172     public void onLaunchTaskSuccess() {
173         ACTIVITY_TYPE activity = getCreatedActivity();
174         if (activity == null) {
175             return;
176         }
177         activity.getStateManager().moveToRestState();
178     }
179 
closeOverlay()180     public void closeOverlay() { }
181 
switchRunningTaskViewToScreenshot(ThumbnailData thumbnailData, Runnable runnable)182     public void switchRunningTaskViewToScreenshot(ThumbnailData thumbnailData, Runnable runnable) {
183         ACTIVITY_TYPE activity = getCreatedActivity();
184         if (activity == null) {
185             return;
186         }
187         RecentsView recentsView = activity.getOverviewPanel();
188         if (recentsView == null) {
189             if (runnable != null) {
190                 runnable.run();
191             }
192             return;
193         }
194         recentsView.switchToScreenshot(thumbnailData, runnable);
195     }
196 
197     /**
198      * Calculates the taskView size for the provided device configuration
199      */
calculateTaskSize(Context context, DeviceProfile dp, Rect outRect, PagedOrientationHandler orientedState)200     public final void calculateTaskSize(Context context, DeviceProfile dp, Rect outRect,
201             PagedOrientationHandler orientedState) {
202         calculateTaskSize(context, dp, getExtraSpace(context, dp, orientedState),
203                 outRect, orientedState);
204     }
205 
getExtraSpace(Context context, DeviceProfile dp, PagedOrientationHandler orientedState)206     protected abstract float getExtraSpace(Context context, DeviceProfile dp,
207             PagedOrientationHandler orientedState);
208 
calculateTaskSize( Context context, DeviceProfile dp, float extraVerticalSpace, Rect outRect, PagedOrientationHandler orientationHandler)209     private void calculateTaskSize(
210             Context context, DeviceProfile dp, float extraVerticalSpace, Rect outRect,
211             PagedOrientationHandler orientationHandler) {
212         Resources res = context.getResources();
213         final boolean showLargeTaskSize = showOverviewActions(context) ||
214                 hideShelfInTwoButtonLandscape(context, orientationHandler);
215 
216         final int paddingResId;
217         if (dp.isMultiWindowMode) {
218             paddingResId = R.dimen.multi_window_task_card_horz_space;
219         } else if (dp.isVerticalBarLayout()) {
220             paddingResId = R.dimen.landscape_task_card_horz_space;
221         } else if (showLargeTaskSize) {
222             paddingResId = R.dimen.portrait_task_card_horz_space_big_overview;
223         } else {
224             paddingResId = R.dimen.portrait_task_card_horz_space;
225         }
226         float paddingHorz = res.getDimension(paddingResId);
227         float paddingVert = showLargeTaskSize
228                 ? 0 : res.getDimension(R.dimen.task_card_vert_space);
229 
230         calculateTaskSizeInternal(context, dp, extraVerticalSpace, paddingHorz, paddingVert,
231                 res.getDimension(R.dimen.task_thumbnail_top_margin), outRect);
232     }
233 
calculateTaskSizeInternal(Context context, DeviceProfile dp, float extraVerticalSpace, float paddingHorz, float paddingVert, float topIconMargin, Rect outRect)234     private void calculateTaskSizeInternal(Context context, DeviceProfile dp,
235             float extraVerticalSpace, float paddingHorz, float paddingVert, float topIconMargin,
236             Rect outRect) {
237         float taskWidth, taskHeight;
238         Rect insets = dp.getInsets();
239         if (dp.isMultiWindowMode) {
240             WindowBounds bounds = SplitScreenBounds.INSTANCE.getSecondaryWindowBounds(context);
241             taskWidth = bounds.availableSize.x;
242             taskHeight = bounds.availableSize.y;
243         } else {
244             taskWidth = dp.availableWidthPx;
245             taskHeight = dp.availableHeightPx;
246         }
247 
248         // Note this should be same as dp.availableWidthPx and dp.availableHeightPx unless
249         // we override the insets ourselves.
250         int launcherVisibleWidth = dp.widthPx - insets.left - insets.right;
251         int launcherVisibleHeight = dp.heightPx - insets.top - insets.bottom;
252 
253         float availableHeight = launcherVisibleHeight
254                 - topIconMargin - extraVerticalSpace - paddingVert;
255         float availableWidth = launcherVisibleWidth - paddingHorz;
256 
257         float scale = Math.min(availableWidth / taskWidth, availableHeight / taskHeight);
258         float outWidth = scale * taskWidth;
259         float outHeight = scale * taskHeight;
260 
261         // Center in the visible space
262         float x = insets.left + (launcherVisibleWidth - outWidth) / 2;
263         float y = insets.top + Math.max(topIconMargin,
264                 (launcherVisibleHeight - extraVerticalSpace - outHeight) / 2);
265         outRect.set(Math.round(x), Math.round(y),
266                 Math.round(x) + Math.round(outWidth), Math.round(y) + Math.round(outHeight));
267     }
268 
269     /**
270      * Calculates the modal taskView size for the provided device configuration
271      */
calculateModalTaskSize(Context context, DeviceProfile dp, Rect outRect)272     public final void calculateModalTaskSize(Context context, DeviceProfile dp, Rect outRect) {
273         float paddingHorz = context.getResources().getDimension(dp.isMultiWindowMode
274                 ? R.dimen.multi_window_task_card_horz_space
275                 : dp.isVerticalBarLayout()
276                         ? R.dimen.landscape_task_card_horz_space
277                         : R.dimen.portrait_modal_task_card_horz_space);
278         float extraVerticalSpace = getOverviewActionsHeight(context);
279         float paddingVert = 0;
280         float topIconMargin = 0;
281         calculateTaskSizeInternal(context, dp, extraVerticalSpace, paddingHorz, paddingVert,
282                 topIconMargin, outRect);
283     }
284 
285     /** Gets the space that the overview actions will take, including margins. */
getOverviewActionsHeight(Context context)286     public final float getOverviewActionsHeight(Context context) {
287         Resources res = context.getResources();
288         float actionsBottomMargin = 0;
289         if (getMode(context) == Mode.THREE_BUTTONS) {
290             actionsBottomMargin = res.getDimensionPixelSize(
291                     R.dimen.overview_actions_bottom_margin_three_button);
292         } else {
293             actionsBottomMargin = res.getDimensionPixelSize(
294                     R.dimen.overview_actions_bottom_margin_gesture);
295         }
296         float overviewActionsHeight = actionsBottomMargin
297                 + res.getDimensionPixelSize(R.dimen.overview_actions_height);
298         return overviewActionsHeight;
299     }
300 
301     public interface AnimationFactory {
302 
createActivityInterface(long transitionLength)303         void createActivityInterface(long transitionLength);
304 
onTransitionCancelled()305         default void onTransitionCancelled() { }
306 
setShelfState(ShelfPeekAnim.ShelfAnimState animState, Interpolator interpolator, long duration)307         default void setShelfState(ShelfPeekAnim.ShelfAnimState animState,
308                 Interpolator interpolator, long duration) { }
309 
310         /**
311          * @param attached Whether to show RecentsView alongside the app window. If false, recents
312          *                 will be hidden by some property we can animate, e.g. alpha.
313          * @param animate Whether to animate recents to/from its new attached state.
314          */
setRecentsAttachedToAppWindow(boolean attached, boolean animate)315         default void setRecentsAttachedToAppWindow(boolean attached, boolean animate) { }
316     }
317 
318     class DefaultAnimationFactory implements AnimationFactory {
319 
320         protected final ACTIVITY_TYPE mActivity;
321         private final STATE_TYPE mStartState;
322         private final Consumer<AnimatorPlaybackController> mCallback;
323 
324         private boolean mIsAttachedToWindow;
325 
DefaultAnimationFactory(Consumer<AnimatorPlaybackController> callback)326         DefaultAnimationFactory(Consumer<AnimatorPlaybackController> callback) {
327             mCallback = callback;
328 
329             mActivity = getCreatedActivity();
330             mStartState = mActivity.getStateManager().getState();
331         }
332 
initUI()333         protected ACTIVITY_TYPE initUI() {
334             STATE_TYPE resetState = mStartState;
335             if (mStartState.shouldDisableRestore()) {
336                 resetState = mActivity.getStateManager().getRestState();
337             }
338             mActivity.getStateManager().setRestState(resetState);
339             mActivity.getStateManager().goToState(mBackgroundState, false);
340             return mActivity;
341         }
342 
343         @Override
createActivityInterface(long transitionLength)344         public void createActivityInterface(long transitionLength) {
345             PendingAnimation pa = new PendingAnimation(transitionLength * 2);
346             createBackgroundToOverviewAnim(mActivity, pa);
347             AnimatorPlaybackController controller = pa.createPlaybackController();
348             mActivity.getStateManager().setCurrentUserControlledAnimation(controller);
349 
350             // Since we are changing the start position of the UI, reapply the state, at the end
351             controller.setEndAction(() -> mActivity.getStateManager().goToState(
352                     controller.getInterpolatedProgress() > 0.5 ? mOverviewState : mBackgroundState,
353                     false));
354             mCallback.accept(controller);
355 
356             // Creating the activity controller animation sometimes reapplies the launcher state
357             // (because we set the animation as the current state animation), so we reapply the
358             // attached state here as well to ensure recents is shown/hidden appropriately.
359             if (SysUINavigationMode.getMode(mActivity) == Mode.NO_BUTTON) {
360                 setRecentsAttachedToAppWindow(mIsAttachedToWindow, false);
361             }
362         }
363 
364         @Override
onTransitionCancelled()365         public void onTransitionCancelled() {
366             mActivity.getStateManager().goToState(mStartState, false /* animate */);
367         }
368 
369         @Override
setRecentsAttachedToAppWindow(boolean attached, boolean animate)370         public void setRecentsAttachedToAppWindow(boolean attached, boolean animate) {
371             if (mIsAttachedToWindow == attached && animate) {
372                 return;
373             }
374             mIsAttachedToWindow = attached;
375             RecentsView recentsView = mActivity.getOverviewPanel();
376             Animator fadeAnim = mActivity.getStateManager()
377                     .createStateElementAnimation(INDEX_RECENTS_FADE_ANIM, attached ? 1 : 0);
378 
379             float fromTranslation = attached ? 1 : 0;
380             float toTranslation = attached ? 0 : 1;
381             mActivity.getStateManager()
382                     .cancelStateElementAnimation(INDEX_RECENTS_TRANSLATE_X_ANIM);
383             if (!recentsView.isShown() && animate) {
384                 ADJACENT_PAGE_OFFSET.set(recentsView, fromTranslation);
385             } else {
386                 fromTranslation = ADJACENT_PAGE_OFFSET.get(recentsView);
387             }
388             if (!animate) {
389                 ADJACENT_PAGE_OFFSET.set(recentsView, toTranslation);
390             } else {
391                 mActivity.getStateManager().createStateElementAnimation(
392                         INDEX_RECENTS_TRANSLATE_X_ANIM,
393                         fromTranslation, toTranslation).start();
394             }
395 
396             fadeAnim.setInterpolator(attached ? INSTANT : ACCEL_2);
397             fadeAnim.setDuration(animate ? RECENTS_ATTACH_DURATION : 0).start();
398         }
399 
createBackgroundToOverviewAnim(ACTIVITY_TYPE activity, PendingAnimation pa)400         protected void createBackgroundToOverviewAnim(ACTIVITY_TYPE activity, PendingAnimation pa) {
401             //  Scale down recents from being full screen to being in overview.
402             RecentsView recentsView = activity.getOverviewPanel();
403             pa.addFloat(recentsView, SCALE_PROPERTY,
404                     recentsView.getMaxScaleForFullScreen(), 1, LINEAR);
405             pa.addFloat(recentsView, FULLSCREEN_PROGRESS, 1, 0, LINEAR);
406         }
407     }
408 
showOverviewActions(Context context)409     protected static boolean showOverviewActions(Context context) {
410         return ENABLE_OVERVIEW_ACTIONS.get() && removeShelfFromOverview(context);
411     }
412 }
413