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 
17 package com.android.quickstep;
18 
19 import static android.view.Display.DEFAULT_DISPLAY;
20 
21 import static com.android.launcher3.config.FeatureFlags.ENABLE_OVERVIEW_ACTIONS;
22 import static com.android.launcher3.config.FeatureFlags.ENABLE_OVERVIEW_SELECTIONS;
23 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SYSTEM_SHORTCUT_FREE_FORM_TAP;
24 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SYSTEM_SHORTCUT_SPLIT_SCREEN_TAP;
25 import static com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch.TAP;
26 
27 import android.app.Activity;
28 import android.app.ActivityOptions;
29 import android.graphics.Bitmap;
30 import android.graphics.Color;
31 import android.graphics.Rect;
32 import android.os.Handler;
33 import android.os.Looper;
34 import android.view.View;
35 
36 import com.android.launcher3.BaseDraggingActivity;
37 import com.android.launcher3.DeviceProfile;
38 import com.android.launcher3.R;
39 import com.android.launcher3.logging.StatsLogManager.LauncherEvent;
40 import com.android.launcher3.model.WellbeingModel;
41 import com.android.launcher3.popup.SystemShortcut;
42 import com.android.launcher3.popup.SystemShortcut.AppInfo;
43 import com.android.launcher3.userevent.nano.LauncherLogProto;
44 import com.android.launcher3.util.Executors;
45 import com.android.launcher3.util.InstantAppResolver;
46 import com.android.quickstep.views.RecentsView;
47 import com.android.quickstep.views.TaskThumbnailView;
48 import com.android.quickstep.views.TaskView;
49 import com.android.systemui.shared.recents.model.Task;
50 import com.android.systemui.shared.recents.view.AppTransitionAnimationSpecCompat;
51 import com.android.systemui.shared.recents.view.AppTransitionAnimationSpecsFuture;
52 import com.android.systemui.shared.recents.view.RecentsTransition;
53 import com.android.systemui.shared.system.ActivityCompat;
54 import com.android.systemui.shared.system.ActivityManagerWrapper;
55 import com.android.systemui.shared.system.ActivityOptionsCompat;
56 import com.android.systemui.shared.system.WindowManagerWrapper;
57 
58 import java.util.Collections;
59 import java.util.List;
60 import java.util.function.Consumer;
61 
62 /**
63  * Represents a system shortcut that can be shown for a recent task.
64  */
65 public interface TaskShortcutFactory {
66 
getShortcut(BaseDraggingActivity activity, TaskView view)67     SystemShortcut getShortcut(BaseDraggingActivity activity, TaskView view);
68 
69     TaskShortcutFactory APP_INFO = (activity, view) -> new AppInfo(activity, view.getItemInfo());
70 
71     abstract class MultiWindowFactory implements TaskShortcutFactory {
72 
73         private final int mIconRes;
74         private final int mTextRes;
75         private final LauncherEvent mLauncherEvent;
76 
MultiWindowFactory(int iconRes, int textRes, LauncherEvent launcherEvent)77         MultiWindowFactory(int iconRes, int textRes, LauncherEvent launcherEvent) {
78             mIconRes = iconRes;
79             mTextRes = textRes;
80             mLauncherEvent = launcherEvent;
81         }
82 
isAvailable(BaseDraggingActivity activity, int displayId)83         protected abstract boolean isAvailable(BaseDraggingActivity activity, int displayId);
makeLaunchOptions(Activity activity)84         protected abstract ActivityOptions makeLaunchOptions(Activity activity);
onActivityStarted(BaseDraggingActivity activity)85         protected abstract boolean onActivityStarted(BaseDraggingActivity activity);
86 
87         @Override
getShortcut(BaseDraggingActivity activity, TaskView taskView)88         public SystemShortcut getShortcut(BaseDraggingActivity activity, TaskView taskView) {
89             final Task task  = taskView.getTask();
90             if (!task.isDockable) {
91                 return null;
92             }
93             if (!isAvailable(activity, task.key.displayId)) {
94                 return null;
95             }
96             return new MultiWindowSystemShortcut(mIconRes, mTextRes, activity, taskView, this,
97                     mLauncherEvent);
98         }
99     }
100 
101     class MultiWindowSystemShortcut extends SystemShortcut {
102 
103         private Handler mHandler;
104 
105         private final RecentsView mRecentsView;
106         private final TaskThumbnailView mThumbnailView;
107         private final TaskView mTaskView;
108         private final MultiWindowFactory mFactory;
109         private final LauncherEvent mLauncherEvent;
110 
MultiWindowSystemShortcut(int iconRes, int textRes, BaseDraggingActivity activity, TaskView taskView, MultiWindowFactory factory, LauncherEvent launcherEvent)111         public MultiWindowSystemShortcut(int iconRes, int textRes, BaseDraggingActivity activity,
112                 TaskView taskView, MultiWindowFactory factory, LauncherEvent launcherEvent) {
113             super(iconRes, textRes, activity, taskView.getItemInfo());
114             mLauncherEvent = launcherEvent;
115             mHandler = new Handler(Looper.getMainLooper());
116             mTaskView = taskView;
117             mRecentsView = activity.getOverviewPanel();
118             mThumbnailView = taskView.getThumbnail();
119             mFactory = factory;
120         }
121 
122         @Override
onClick(View view)123         public void onClick(View view) {
124             Task.TaskKey taskKey = mTaskView.getTask().key;
125             final int taskId = taskKey.id;
126 
127             final View.OnLayoutChangeListener onLayoutChangeListener =
128                     new View.OnLayoutChangeListener() {
129                         @Override
130                         public void onLayoutChange(View v, int l, int t, int r, int b,
131                                 int oldL, int oldT, int oldR, int oldB) {
132                             mTaskView.getRootView().removeOnLayoutChangeListener(this);
133                             mRecentsView.clearIgnoreResetTask(taskId);
134 
135                             // Start animating in the side pages once launcher has been resized
136                             mRecentsView.dismissTask(mTaskView, false, false);
137                         }
138                     };
139 
140             final DeviceProfile.OnDeviceProfileChangeListener onDeviceProfileChangeListener =
141                     new DeviceProfile.OnDeviceProfileChangeListener() {
142                         @Override
143                         public void onDeviceProfileChanged(DeviceProfile dp) {
144                             mTarget.removeOnDeviceProfileChangeListener(this);
145                             if (dp.isMultiWindowMode) {
146                                 mTaskView.getRootView().addOnLayoutChangeListener(
147                                         onLayoutChangeListener);
148                             }
149                         }
150                     };
151 
152             dismissTaskMenuView(mTarget);
153 
154             ActivityOptions options = mFactory.makeLaunchOptions(mTarget);
155             if (options != null
156                     && ActivityManagerWrapper.getInstance().startActivityFromRecents(taskId,
157                             options)) {
158                 if (!mFactory.onActivityStarted(mTarget)) {
159                     return;
160                 }
161                 // Add a device profile change listener to kick off animating the side tasks
162                 // once we enter multiwindow mode and relayout
163                 mTarget.addOnDeviceProfileChangeListener(onDeviceProfileChangeListener);
164 
165                 final Runnable animStartedListener = () -> {
166                     // Hide the task view and wait for the window to be resized
167                     // TODO: Consider animating in launcher and do an in-place start activity
168                     //       afterwards
169                     mRecentsView.setIgnoreResetTask(taskId);
170                     mTaskView.setAlpha(0f);
171                 };
172 
173                 final int[] position = new int[2];
174                 mThumbnailView.getLocationOnScreen(position);
175                 final int width = (int) (mThumbnailView.getWidth() * mTaskView.getScaleX());
176                 final int height = (int) (mThumbnailView.getHeight() * mTaskView.getScaleY());
177                 final Rect taskBounds = new Rect(position[0], position[1],
178                         position[0] + width, position[1] + height);
179 
180                 // Take the thumbnail of the task without a scrim and apply it back after
181                 float alpha = mThumbnailView.getDimAlpha();
182                 mThumbnailView.setDimAlpha(0);
183                 Bitmap thumbnail = RecentsTransition.drawViewIntoHardwareBitmap(
184                         taskBounds.width(), taskBounds.height(), mThumbnailView, 1f,
185                         Color.BLACK);
186                 mThumbnailView.setDimAlpha(alpha);
187 
188                 AppTransitionAnimationSpecsFuture future =
189                         new AppTransitionAnimationSpecsFuture(mHandler) {
190                     @Override
191                     public List<AppTransitionAnimationSpecCompat> composeSpecs() {
192                         return Collections.singletonList(new AppTransitionAnimationSpecCompat(
193                                 taskId, thumbnail, taskBounds));
194                     }
195                 };
196                 WindowManagerWrapper.getInstance().overridePendingAppTransitionMultiThumbFuture(
197                         future, animStartedListener, mHandler, true /* scaleUp */,
198                         taskKey.displayId);
199                 mTarget.getStatsLogManager().logger().withItemInfo(mTaskView.getItemInfo())
200                         .log(mLauncherEvent);
201             }
202         }
203     }
204 
205     TaskShortcutFactory SPLIT_SCREEN = new MultiWindowFactory(R.drawable.ic_split_screen,
206             R.string.recent_task_option_split_screen, LAUNCHER_SYSTEM_SHORTCUT_SPLIT_SCREEN_TAP) {
207 
208         @Override
209         protected boolean isAvailable(BaseDraggingActivity activity, int displayId) {
210             // Don't show menu-item if already in multi-window and the task is from
211             // the secondary display.
212             // TODO(b/118266305): Temporarily disable splitscreen for secondary display while new
213             // implementation is enabled
214             return !activity.getDeviceProfile().isMultiWindowMode
215                     && (displayId == -1 || displayId == DEFAULT_DISPLAY);
216         }
217 
218         @Override
219         protected ActivityOptions makeLaunchOptions(Activity activity) {
220             final ActivityCompat act = new ActivityCompat(activity);
221             final int navBarPosition = WindowManagerWrapper.getInstance().getNavBarPosition(
222                     act.getDisplayId());
223             if (navBarPosition == WindowManagerWrapper.NAV_BAR_POS_INVALID) {
224                 return null;
225             }
226             boolean dockTopOrLeft = navBarPosition != WindowManagerWrapper.NAV_BAR_POS_LEFT;
227             return ActivityOptionsCompat.makeSplitScreenOptions(dockTopOrLeft);
228         }
229 
230         @Override
231         protected boolean onActivityStarted(BaseDraggingActivity activity) {
232             SystemUiProxy.INSTANCE.get(activity).onSplitScreenInvoked();
233             activity.getUserEventDispatcher().logActionOnControl(TAP,
234                     LauncherLogProto.ControlType.SPLIT_SCREEN_TARGET);
235             return true;
236         }
237     };
238 
239     TaskShortcutFactory FREE_FORM = new MultiWindowFactory(R.drawable.ic_split_screen,
240             R.string.recent_task_option_freeform, LAUNCHER_SYSTEM_SHORTCUT_FREE_FORM_TAP) {
241 
242         @Override
243         protected boolean isAvailable(BaseDraggingActivity activity, int displayId) {
244             return ActivityManagerWrapper.getInstance().supportsFreeformMultiWindow(activity);
245         }
246 
247         @Override
248         protected ActivityOptions makeLaunchOptions(Activity activity) {
249             ActivityOptions activityOptions = ActivityOptionsCompat.makeFreeformOptions();
250             // Arbitrary bounds only because freeform is in dev mode right now
251             Rect r = new Rect(50, 50, 200, 200);
252             activityOptions.setLaunchBounds(r);
253             return activityOptions;
254         }
255 
256         @Override
257         protected boolean onActivityStarted(BaseDraggingActivity activity) {
258             activity.returnToHomescreen();
259             return true;
260         }
261     };
262 
263     TaskShortcutFactory PIN = (activity, tv) -> {
264         if (!SystemUiProxy.INSTANCE.get(activity).isActive()) {
265             return null;
266         }
267         if (!ActivityManagerWrapper.getInstance().isScreenPinningEnabled()) {
268             return null;
269         }
270         if (ActivityManagerWrapper.getInstance().isLockToAppActive()) {
271             // We shouldn't be able to pin while an app is locked.
272             return null;
273         }
274         return new PinSystemShortcut(activity, tv);
275     };
276 
277     class PinSystemShortcut extends SystemShortcut {
278 
279         private static final String TAG = "PinSystemShortcut";
280 
281         private final TaskView mTaskView;
282 
PinSystemShortcut(BaseDraggingActivity target, TaskView tv)283         public PinSystemShortcut(BaseDraggingActivity target, TaskView tv) {
284             super(R.drawable.ic_pin, R.string.recent_task_option_pin, target, tv.getItemInfo());
285             mTaskView = tv;
286         }
287 
288         @Override
onClick(View view)289         public void onClick(View view) {
290             Consumer<Boolean> resultCallback = success -> {
291                 if (success) {
292                     SystemUiProxy.INSTANCE.get(mTarget).startScreenPinning(
293                             mTaskView.getTask().key.id);
294                 } else {
295                     mTaskView.notifyTaskLaunchFailed(TAG);
296                 }
297             };
298             mTaskView.launchTask(true, resultCallback, Executors.MAIN_EXECUTOR.getHandler());
299             dismissTaskMenuView(mTarget);
300             mTarget.getStatsLogManager().logger().withItemInfo(mTaskView.getItemInfo())
301                     .log(LauncherEvent.LAUNCHER_SYSTEM_SHORTCUT_PIN_TAP);
302         }
303     }
304 
305     TaskShortcutFactory INSTALL = (activity, view) ->
306             InstantAppResolver.newInstance(activity).isInstantApp(activity,
307                  view.getTask().getTopComponent().getPackageName())
308                     ? new SystemShortcut.Install(activity, view.getItemInfo()) : null;
309 
310     TaskShortcutFactory WELLBEING = (activity, view) ->
311             WellbeingModel.SHORTCUT_FACTORY.getShortcut(activity, view.getItemInfo());
312 
313     TaskShortcutFactory SCREENSHOT = (activity, tv) -> {
314         if (ENABLE_OVERVIEW_ACTIONS.get()) {
315             return tv.getThumbnail().getTaskOverlay()
316                 .getScreenshotShortcut(activity, tv.getItemInfo());
317         }
318         return null;
319     };
320 
321     TaskShortcutFactory MODAL = (activity, tv) -> {
322         if (ENABLE_OVERVIEW_ACTIONS.get() && ENABLE_OVERVIEW_SELECTIONS.get()) {
323             return tv.getThumbnail().getTaskOverlay().getModalStateSystemShortcut(tv.getItemInfo());
324         }
325         return null;
326     };
327 }
328