1 /*
2  * Copyright (C) 2022 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 
17 package com.android.quickstep.util;
18 
19 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
20 
21 import android.animation.Animator;
22 import android.animation.AnimatorListenerAdapter;
23 import android.annotation.NonNull;
24 import android.annotation.Nullable;
25 import android.app.PendingIntent;
26 import android.content.Intent;
27 import android.graphics.Bitmap;
28 import android.graphics.Canvas;
29 import android.graphics.Rect;
30 import android.graphics.RectF;
31 import android.graphics.drawable.BitmapDrawable;
32 import android.graphics.drawable.Drawable;
33 import android.os.UserHandle;
34 import android.view.View;
35 
36 import com.android.internal.jank.Cuj;
37 import com.android.launcher3.DeviceProfile;
38 import com.android.launcher3.LauncherAppState;
39 import com.android.launcher3.R;
40 import com.android.launcher3.anim.PendingAnimation;
41 import com.android.launcher3.config.FeatureFlags;
42 import com.android.launcher3.icons.BitmapInfo;
43 import com.android.launcher3.icons.IconCache;
44 import com.android.launcher3.model.data.AppPairInfo;
45 import com.android.launcher3.model.data.ItemInfo;
46 import com.android.launcher3.model.data.PackageItemInfo;
47 import com.android.launcher3.model.data.WorkspaceItemInfo;
48 import com.android.launcher3.uioverrides.QuickstepLauncher;
49 import com.android.quickstep.views.FloatingTaskView;
50 import com.android.quickstep.views.RecentsView;
51 import com.android.systemui.shared.system.InteractionJankMonitorWrapper;
52 
53 /** Handles when the stage split lands on the home screen. */
54 public class SplitToWorkspaceController {
55 
56     private final QuickstepLauncher mLauncher;
57     private final SplitSelectStateController mController;
58 
59     private final int mHalfDividerSize;
60     private final IconCache mIconCache;
61 
SplitToWorkspaceController(QuickstepLauncher launcher, SplitSelectStateController controller)62     public SplitToWorkspaceController(QuickstepLauncher launcher,
63             SplitSelectStateController controller) {
64         mLauncher = launcher;
65         mController = controller;
66         mIconCache = LauncherAppState.getInstance(launcher).getIconCache();
67         mHalfDividerSize = mLauncher.getResources().getDimensionPixelSize(
68                 R.dimen.multi_window_task_divider_size) / 2;
69     }
70 
71     /**
72      * Handles widget selection from staged split.
73      * @param view Original widget view
74      * @param pendingIntent Provided by widget via InteractionHandler
75      * @return {@code true} if we can attempt launch the widget into split, {@code false} otherwise
76      *         to allow launcher to handle the click
77      */
handleSecondWidgetSelectionForSplit(View view, PendingIntent pendingIntent, Intent remoteResponseIntent)78     public boolean handleSecondWidgetSelectionForSplit(View view, PendingIntent pendingIntent,
79             Intent remoteResponseIntent) {
80         if (shouldIgnoreSecondSplitLaunch()) {
81             return false;
82         }
83 
84         int width = view.getWidth();
85         int height = view.getHeight();
86         MODEL_EXECUTOR.execute(() -> {
87             PackageItemInfo infoInOut = new PackageItemInfo(pendingIntent.getCreatorPackage(),
88                     pendingIntent.getCreatorUserHandle());
89             mIconCache.getTitleAndIconForApp(infoInOut, false);
90             Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
91 
92             view.post(() -> {
93                 mController.setSecondWidget(pendingIntent, remoteResponseIntent);
94                 // Convert original widgetView into bitmap to use for animation
95                 Canvas canvas = new Canvas(bitmap);
96                 view.draw(canvas);
97                 startWorkspaceAnimation(view, bitmap,
98                         new BitmapDrawable(mLauncher.getResources(), infoInOut.bitmap.icon));
99             });
100         });
101 
102         return true;
103     }
104 
105     /**
106      * Handles second app selection from stage split. If the item can't be opened in split or
107      * it's not in stage split state, we pass it onto Launcher's default item click handler.
108      */
handleSecondAppSelectionForSplit(View view)109     public boolean handleSecondAppSelectionForSplit(View view) {
110         if (shouldIgnoreSecondSplitLaunch()) {
111             return false;
112         }
113         Object tag = view.getTag();
114         Intent intent;
115         UserHandle user;
116         BitmapInfo bitmapInfo;
117         if (tag instanceof WorkspaceItemInfo) {
118             final WorkspaceItemInfo workspaceItemInfo = (WorkspaceItemInfo) tag;
119             intent = workspaceItemInfo.intent;
120             user = workspaceItemInfo.user;
121             bitmapInfo = workspaceItemInfo.bitmap;
122         } else if (tag instanceof com.android.launcher3.model.data.AppInfo) {
123             final com.android.launcher3.model.data.AppInfo appInfo =
124                     (com.android.launcher3.model.data.AppInfo) tag;
125             intent = appInfo.intent;
126             user = appInfo.user;
127             bitmapInfo = appInfo.bitmap;
128         } else if (tag instanceof AppPairInfo) {
129             // Prompt the user to select something else by wiggling the instructions view
130             mController.getSplitInstructionsView().goBoing();
131             return true;
132         } else {
133             // Use Launcher's default click handler
134             return false;
135         }
136 
137         mController.setSecondTask(intent, user, (ItemInfo) tag);
138 
139         startWorkspaceAnimation(view, null /*bitmap*/, bitmapInfo.newIcon(mLauncher));
140         return true;
141     }
142 
startWorkspaceAnimation(@onNull View view, @Nullable Bitmap bitmap, @Nullable Drawable icon)143     private void startWorkspaceAnimation(@NonNull View view, @Nullable Bitmap bitmap,
144             @Nullable Drawable icon) {
145         DeviceProfile dp = mLauncher.getDeviceProfile();
146         boolean isTablet = dp.isTablet;
147         SplitAnimationTimings timings = AnimUtils.getDeviceSplitToConfirmTimings(isTablet);
148         PendingAnimation pendingAnimation = new PendingAnimation(timings.getDuration());
149 
150         Rect firstTaskStartingBounds = new Rect();
151         Rect firstTaskEndingBounds = new Rect();
152         RectF secondTaskStartingBounds = new RectF();
153         Rect secondTaskEndingBounds = new Rect();
154 
155         RecentsView recentsView = mLauncher.getOverviewPanel();
156         recentsView.getPagedOrientationHandler().getFinalSplitPlaceholderBounds(mHalfDividerSize,
157                 dp, mController.getActiveSplitStagePosition(), firstTaskEndingBounds,
158                 secondTaskEndingBounds);
159 
160         FloatingTaskView firstFloatingTaskView = mController.getFirstFloatingTaskView();
161         firstFloatingTaskView.getBoundsOnScreen(firstTaskStartingBounds);
162         firstFloatingTaskView.addConfirmAnimation(pendingAnimation,
163                 new RectF(firstTaskStartingBounds), firstTaskEndingBounds,
164                 false /* fadeWithThumbnail */, true /* isStagedTask */);
165 
166         View backingScrim = recentsView.getSplitSelectController().getSplitAnimationController()
167                 .addScrimBehindAnim(pendingAnimation, mLauncher, view.getContext());
168 
169         FloatingTaskView secondFloatingTaskView = FloatingTaskView.getFloatingTaskView(mLauncher,
170                 view, bitmap, icon, secondTaskStartingBounds);
171         secondFloatingTaskView.setAlpha(1);
172         secondFloatingTaskView.addConfirmAnimation(pendingAnimation, secondTaskStartingBounds,
173                 secondTaskEndingBounds, true /* fadeWithThumbnail */, false /* isStagedTask */);
174 
175         pendingAnimation.addListener(new AnimatorListenerAdapter() {
176             private boolean mIsCancelled = false;
177 
178             @Override
179             public void onAnimationStart(Animator animation) {
180                 mController.launchSplitTasks(aBoolean -> cleanUp());
181             }
182 
183             @Override
184             public void onAnimationCancel(Animator animation) {
185                 mIsCancelled = true;
186                 cleanUp();
187             }
188 
189             @Override
190             public void onAnimationEnd(Animator animation) {
191                 if (!mIsCancelled) {
192                     InteractionJankMonitorWrapper.end(Cuj.CUJ_SPLIT_SCREEN_ENTER);
193                 }
194             }
195 
196             private void cleanUp() {
197                 mLauncher.getDragLayer().removeView(firstFloatingTaskView);
198                 mLauncher.getDragLayer().removeView(secondFloatingTaskView);
199                 mLauncher.getDragLayer().removeView(backingScrim);
200                 mController.getSplitAnimationController().removeSplitInstructionsView(mLauncher);
201                 mController.resetState();
202             }
203         });
204         pendingAnimation.buildAnim().start();
205     }
206 
shouldIgnoreSecondSplitLaunch()207     private boolean shouldIgnoreSecondSplitLaunch() {
208         return !FeatureFlags.enableSplitContextually() || !mController.isSplitSelectActive();
209     }
210 }
211