1 /*
2  * Copyright (C) 2020 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.EXAGGERATED_EASE;
19 import static com.android.app.animation.Interpolators.LINEAR;
20 import static com.android.launcher3.Flags.enableScalingRevealHomeAnimation;
21 import static com.android.launcher3.LauncherState.NORMAL;
22 import static com.android.launcher3.Utilities.mapBoundToRange;
23 import static com.android.launcher3.model.data.ItemInfo.NO_MATCHING_ID;
24 import static com.android.launcher3.views.FloatingIconView.SHAPE_PROGRESS_DURATION;
25 import static com.android.launcher3.views.FloatingIconView.getFloatingIconView;
26 
27 import android.animation.AnimatorSet;
28 import android.content.Context;
29 import android.graphics.Rect;
30 import android.graphics.RectF;
31 import android.os.IBinder;
32 import android.os.UserHandle;
33 import android.util.Size;
34 import android.view.RemoteAnimationTarget;
35 import android.view.View;
36 
37 import androidx.annotation.NonNull;
38 import androidx.annotation.Nullable;
39 
40 import com.android.launcher3.LauncherState;
41 import com.android.launcher3.anim.AnimatorPlaybackController;
42 import com.android.launcher3.model.data.ItemInfo;
43 import com.android.launcher3.states.StateAnimationConfig;
44 import com.android.launcher3.uioverrides.QuickstepLauncher;
45 import com.android.launcher3.util.ObjectWrapper;
46 import com.android.launcher3.views.ClipIconView;
47 import com.android.launcher3.views.FloatingIconView;
48 import com.android.launcher3.views.FloatingView;
49 import com.android.launcher3.widget.LauncherAppWidgetHostView;
50 import com.android.quickstep.util.RectFSpringAnim;
51 import com.android.quickstep.util.ScalingWorkspaceRevealAnim;
52 import com.android.quickstep.util.StaggeredWorkspaceAnim;
53 import com.android.quickstep.util.TaskViewSimulator;
54 import com.android.quickstep.views.FloatingWidgetView;
55 import com.android.quickstep.views.RecentsView;
56 import com.android.quickstep.views.TaskView;
57 import com.android.systemui.shared.system.InputConsumerController;
58 
59 import java.util.Collections;
60 import java.util.List;
61 
62 /**
63  * Temporary class to allow easier refactoring
64  */
65 public class LauncherSwipeHandlerV2 extends
66         AbsSwipeUpHandler<QuickstepLauncher, RecentsView, LauncherState> {
67 
LauncherSwipeHandlerV2(Context context, RecentsAnimationDeviceState deviceState, TaskAnimationManager taskAnimationManager, GestureState gestureState, long touchTimeMs, boolean continuingLastGesture, InputConsumerController inputConsumer)68     public LauncherSwipeHandlerV2(Context context, RecentsAnimationDeviceState deviceState,
69             TaskAnimationManager taskAnimationManager, GestureState gestureState, long touchTimeMs,
70             boolean continuingLastGesture, InputConsumerController inputConsumer) {
71         super(context, deviceState, taskAnimationManager, gestureState, touchTimeMs,
72                 continuingLastGesture, inputConsumer);
73     }
74 
75 
76     @Override
createHomeAnimationFactory( List<IBinder> launchCookies, long duration, boolean isTargetTranslucent, boolean appCanEnterPip, RemoteAnimationTarget runningTaskTarget, @Nullable TaskView targetTaskView)77     protected HomeAnimationFactory createHomeAnimationFactory(
78             List<IBinder> launchCookies,
79             long duration,
80             boolean isTargetTranslucent,
81             boolean appCanEnterPip,
82             RemoteAnimationTarget runningTaskTarget,
83             @Nullable TaskView targetTaskView) {
84         if (mContainer == null) {
85             mStateCallback.addChangeListener(STATE_LAUNCHER_PRESENT | STATE_HANDLER_INVALIDATED,
86                     isPresent -> mRecentsView.startHome());
87             return new HomeAnimationFactory() {
88                 @Override
89                 public AnimatorPlaybackController createActivityAnimationToHome() {
90                     return AnimatorPlaybackController.wrap(new AnimatorSet(), duration);
91                 }
92             };
93         }
94 
95         TaskView sourceTaskView = mRecentsView == null && targetTaskView == null
96                 ? null
97                 : targetTaskView == null
98                         ? mRecentsView.getRunningTaskView()
99                         : targetTaskView;
100         final View workspaceView = findWorkspaceView(
101                 targetTaskView == null ? launchCookies : Collections.emptyList(),
102                 sourceTaskView);
103         boolean canUseWorkspaceView = workspaceView != null
104                 && workspaceView.isAttachedToWindow()
105                 && workspaceView.getHeight() > 0
106                 && (mContainer.getDesktopVisibilityController() == null
107                 || !mContainer.getDesktopVisibilityController().areDesktopTasksVisible());
108 
109         mContainer.getRootView().setForceHideBackArrow(true);
110         if (!TaskAnimationManager.ENABLE_SHELL_TRANSITIONS) {
111             mContainer.setHintUserWillBeActive();
112         }
113 
114         if (!canUseWorkspaceView || appCanEnterPip || mIsSwipeForSplit) {
115             return new LauncherHomeAnimationFactory() {
116 
117                 @Nullable
118                 @Override
119                 public TaskView getTargetTaskView() {
120                     return targetTaskView;
121                 }
122             };
123         }
124         if (workspaceView instanceof LauncherAppWidgetHostView) {
125             return createWidgetHomeAnimationFactory((LauncherAppWidgetHostView) workspaceView,
126                     isTargetTranslucent, runningTaskTarget);
127         }
128         return createIconHomeAnimationFactory(workspaceView, targetTaskView);
129     }
130 
131     private HomeAnimationFactory createIconHomeAnimationFactory(
132             View workspaceView, @Nullable TaskView targetTaskView) {
133         RectF iconLocation = new RectF();
134         FloatingIconView floatingIconView = getFloatingIconView(mContainer, workspaceView, null,
135                 mContainer.getTaskbarUIController() == null
136                         ? null
137                         : mContainer.getTaskbarUIController().findMatchingView(workspaceView),
138                 true /* hideOriginal */, iconLocation, false /* isOpening */);
139 
140         // We want the window alpha to be 0 once this threshold is met, so that the
141         // FolderIconView can be seen morphing into the icon shape.
142         float windowAlphaThreshold = 1f - SHAPE_PROGRESS_DURATION;
143 
144         return new FloatingViewHomeAnimationFactory(floatingIconView) {
145             @Nullable
146             private RectF mTargetRect;
147             @Nullable
148             private RectFSpringAnim mSiblingAnimation;
149 
150             @Nullable
151             @Override
152             protected View getViewIgnoredInWorkspaceRevealAnimation() {
153                 return workspaceView;
154             }
155 
156             @Override
157             public boolean isInHotseat() {
158                 return workspaceView.getTag() instanceof ItemInfo
159                         && ((ItemInfo) workspaceView.getTag()).isInHotseat();
160             }
161 
162             @NonNull
163             @Override
164             public RectF getWindowTargetRect() {
165                 if (enableScalingRevealHomeAnimation()) {
166                     if (mTargetRect == null) {
167                         mTargetRect = new RectF(iconLocation);
168                     }
169                     return mTargetRect;
170                 } else {
171                     return iconLocation;
172                 }
173             }
174 
175             @Override
176             protected void playScalingRevealAnimation() {
177                 if (mContainer != null) {
178                     new ScalingWorkspaceRevealAnim(mContainer, mSiblingAnimation,
179                             getWindowTargetRect()).start();
180                 }
181             }
182 
183             @Override
184             public void setAnimation(RectFSpringAnim anim) {
185                 super.setAnimation(anim);
186                 mSiblingAnimation = anim;
187                 mSiblingAnimation.addAnimatorListener(floatingIconView);
188                 floatingIconView.setOnTargetChangeListener(
189                         mSiblingAnimation::onTargetPositionChanged);
190                 floatingIconView.setFastFinishRunnable(mSiblingAnimation::end);
191             }
192 
193             @Override
194             public void update(
195                     RectF currentRect,
196                     float progress,
197                     float radius,
198                     int overlayAlpha) {
199                 floatingIconView.update(1f /* alpha */, currentRect, progress, windowAlphaThreshold,
200                         radius, false, overlayAlpha);
201             }
202 
203             @Override
204             public boolean isAnimationReady() {
205                 return floatingIconView.isLaidOut();
206             }
207 
208             @Override
209             public void setTaskViewArtist(ClipIconView.TaskViewArtist taskViewArtist) {
210                 super.setTaskViewArtist(taskViewArtist);
211                 floatingIconView.setOverlayArtist(taskViewArtist);
212             }
213 
214             @Override
215             public boolean isAnimatingIntoIcon() {
216                 return true;
217             }
218 
219             @Nullable
220             @Override
221             public TaskView getTargetTaskView() {
222                 return targetTaskView;
223             }
224         };
225     }
226 
227     private HomeAnimationFactory createWidgetHomeAnimationFactory(
228             LauncherAppWidgetHostView hostView, boolean isTargetTranslucent,
229             RemoteAnimationTarget runningTaskTarget) {
230         final float floatingWidgetAlpha = isTargetTranslucent ? 0 : 1;
231         RectF backgroundLocation = new RectF();
232         Rect crop = new Rect();
233         // We can assume there is only one remote target here because staged split never animates
234         // into the app icon, only into the homescreen
235         RemoteTargetGluer.RemoteTargetHandle remoteTargetHandle = mRemoteTargetHandles[0];
236         TaskViewSimulator tvs = remoteTargetHandle.getTaskViewSimulator();
237         // This is to set up the inverse matrix in the simulator
238         tvs.apply(remoteTargetHandle.getTransformParams());
239         tvs.getCurrentCropRect().roundOut(crop);
240         Size windowSize = new Size(crop.width(), crop.height());
241         int fallbackBackgroundColor =
242                 FloatingWidgetView.getDefaultBackgroundColor(mContext, runningTaskTarget);
243         FloatingWidgetView floatingWidgetView = FloatingWidgetView.getFloatingWidgetView(mContainer,
244                 hostView, backgroundLocation, windowSize, tvs.getCurrentCornerRadius(),
245                 isTargetTranslucent, fallbackBackgroundColor);
246 
247         return new FloatingViewHomeAnimationFactory(floatingWidgetView) {
248 
249             @Override
250             @Nullable
251             protected View getViewIgnoredInWorkspaceRevealAnimation() {
252                 return hostView;
253             }
254 
255             @Override
256             public RectF getWindowTargetRect() {
257                 super.getWindowTargetRect();
258                 return backgroundLocation;
259             }
260 
261             @Override
262             public float getEndRadius(RectF cropRectF) {
263                 return floatingWidgetView.getInitialCornerRadius();
264             }
265 
266             @Override
267             public void setAnimation(RectFSpringAnim anim) {
268                 super.setAnimation(anim);
269 
270                 anim.addAnimatorListener(floatingWidgetView);
271                 floatingWidgetView.setOnTargetChangeListener(anim::onTargetPositionChanged);
272                 floatingWidgetView.setFastFinishRunnable(anim::end);
273             }
274 
275             @Override
276             public void update(RectF currentRect, float progress, float radius, int overlayAlpha) {
277                 super.update(currentRect, progress, radius, overlayAlpha);
278                 final float fallbackBackgroundAlpha =
279                         1 - mapBoundToRange(progress, 0.8f, 1, 0, 1, EXAGGERATED_EASE);
280                 final float foregroundAlpha =
281                         mapBoundToRange(progress, 0.5f, 1, 0, 1, EXAGGERATED_EASE);
282                 floatingWidgetView.update(currentRect, floatingWidgetAlpha, foregroundAlpha,
283                         fallbackBackgroundAlpha, 1 - progress);
284             }
285 
286             @Override
287             protected float getWindowAlpha(float progress) {
288                 return 1 - mapBoundToRange(progress, 0, 0.5f, 0, 1, LINEAR);
289             }
290         };
291     }
292 
293     /**
294      * Returns the associated view on the workspace matching one of the launch cookies, or the app
295      * associated with the running task.
296      */
297     @Nullable
298     private View findWorkspaceView(List<IBinder> launchCookies, TaskView sourceTaskView) {
299         if (mIsSwipingPipToHome) {
300             // Disable if swiping to PIP
301             return null;
302         }
303         if (sourceTaskView == null || sourceTaskView.getFirstTask().key.getComponent() == null) {
304             // Disable if it's an invalid task
305             return null;
306         }
307 
308         // Find the associated item info for the launch cookie (if available), note that predicted
309         // apps actually have an id of -1, so use another default id here
310         int launchCookieItemId = NO_MATCHING_ID;
311         for (IBinder cookie : launchCookies) {
312             Integer itemId = ObjectWrapper.unwrap(cookie);
313             if (itemId != null) {
314                 launchCookieItemId = itemId;
315                 break;
316             }
317         }
318 
319         return mContainer.getFirstMatchForAppClose(launchCookieItemId,
320                 sourceTaskView.getFirstTask().key.getComponent().getPackageName(),
321                 UserHandle.of(sourceTaskView.getFirstTask().key.userId),
322                 false /* supportsAllAppsState */);
323     }
324 
325     @Override
326     protected void finishRecentsControllerToHome(Runnable callback) {
327         mRecentsView.cleanupRemoteTargets();
328         mRecentsAnimationController.finish(
329                 true /* toRecents */, callback, true /* sendUserLeaveHint */);
330     }
331 
332     private class FloatingViewHomeAnimationFactory extends LauncherHomeAnimationFactory {
333 
334         private final FloatingView mFloatingView;
335 
336         FloatingViewHomeAnimationFactory(FloatingView floatingView) {
337             mFloatingView = floatingView;
338         }
339 
340         @Override
341         public void onCancel() {
342             mFloatingView.fastFinish();
343         }
344     }
345 
346     private class LauncherHomeAnimationFactory extends HomeAnimationFactory {
347 
348         /**
349          * Returns a view which should be excluded from the Workspace animation, or null if there
350          * is no view to exclude.
351          */
352         @Nullable
353         protected View getViewIgnoredInWorkspaceRevealAnimation() {
354             return null;
355         }
356 
357         @NonNull
358         @Override
359         public AnimatorPlaybackController createActivityAnimationToHome() {
360             // Return an empty APC here since we have an non-user controlled animation
361             // to home.
362             long accuracy = 2 * Math.max(mDp.widthPx, mDp.heightPx);
363             return mContainer.getStateManager().createAnimationToNewWorkspace(
364                     NORMAL, accuracy, StateAnimationConfig.SKIP_ALL_ANIMATIONS);
365         }
366 
367         @Override
368         public void playAtomicAnimation(float velocity) {
369             if (enableScalingRevealHomeAnimation()) {
370                 playScalingRevealAnimation();
371             } else {
372                 new StaggeredWorkspaceAnim(mContainer, velocity, true /* animateOverviewScrim */,
373                         getViewIgnoredInWorkspaceRevealAnimation())
374                         .start();
375             }
376         }
377 
378         /**
379          * Extracted in a different method so subclasses that have a custom window animation with a
380          * target (icons, widgets) can pass the optional parameters.
381          */
382         protected void playScalingRevealAnimation() {
383             if (mContainer != null) {
384                 new ScalingWorkspaceRevealAnim(
385                         mContainer, null /* siblingAnimation */,
386                         null /* windowTargetRect */).start();
387             }
388         }
389     }
390 }
391