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.Surface.ROTATION_0;
20 
21 import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
22 import static com.android.quickstep.views.OverviewActionsView.DISABLED_NO_THUMBNAIL;
23 import static com.android.quickstep.views.OverviewActionsView.DISABLED_ROTATED;
24 
25 import android.annotation.SuppressLint;
26 import android.app.ActivityManager;
27 import android.content.Context;
28 import android.graphics.Insets;
29 import android.graphics.Matrix;
30 import android.graphics.Rect;
31 import android.os.Build;
32 import android.view.View;
33 import android.widget.Toast;
34 
35 import androidx.annotation.NonNull;
36 import androidx.annotation.RequiresApi;
37 
38 import com.android.launcher3.BaseActivity;
39 import com.android.launcher3.BaseDraggingActivity;
40 import com.android.launcher3.DeviceProfile;
41 import com.android.launcher3.R;
42 import com.android.launcher3.config.FeatureFlags;
43 import com.android.launcher3.model.data.ItemInfo;
44 import com.android.launcher3.model.data.WorkspaceItemInfo;
45 import com.android.launcher3.popup.SystemShortcut;
46 import com.android.launcher3.touch.PagedOrientationHandler;
47 import com.android.launcher3.util.ResourceBasedOverride;
48 import com.android.launcher3.util.SplitConfigurationOptions.SplitPositionOption;
49 import com.android.quickstep.TaskShortcutFactory.SplitSelectSystemShortcut;
50 import com.android.quickstep.util.RecentsOrientedState;
51 import com.android.quickstep.views.OverviewActionsView;
52 import com.android.quickstep.views.RecentsView;
53 import com.android.quickstep.views.TaskThumbnailView;
54 import com.android.quickstep.views.TaskView;
55 import com.android.quickstep.views.TaskView.TaskIdAttributeContainer;
56 import com.android.systemui.shared.recents.model.Task;
57 import com.android.systemui.shared.recents.model.ThumbnailData;
58 import com.android.systemui.shared.system.ActivityManagerWrapper;
59 
60 import java.util.ArrayList;
61 import java.util.List;
62 
63 /**
64  * Factory class to create and add an overlays on the TaskView
65  */
66 public class TaskOverlayFactory implements ResourceBasedOverride {
67 
getEnabledShortcuts(TaskView taskView, DeviceProfile deviceProfile, TaskIdAttributeContainer taskContainer)68     public static List<SystemShortcut> getEnabledShortcuts(TaskView taskView,
69             DeviceProfile deviceProfile, TaskIdAttributeContainer taskContainer) {
70         final ArrayList<SystemShortcut> shortcuts = new ArrayList<>();
71         final BaseDraggingActivity activity = BaseActivity.fromContext(taskView.getContext());
72         boolean hasMultipleTasks = taskView.getTaskIds()[1] != -1;
73         for (TaskShortcutFactory menuOption : MENU_OPTIONS) {
74             if (hasMultipleTasks && !menuOption.showForSplitscreen()) {
75                 continue;
76             }
77 
78             SystemShortcut shortcut = menuOption.getShortcut(activity, taskContainer);
79             if (shortcut == null) {
80                 continue;
81             }
82 
83             if (menuOption == TaskShortcutFactory.SPLIT_SCREEN &&
84                     FeatureFlags.ENABLE_SPLIT_SELECT.get()) {
85                 addSplitOptions(shortcuts, activity, taskView, deviceProfile);
86             } else {
87                 shortcuts.add(shortcut);
88             }
89         }
90         RecentsOrientedState orientedState = taskView.getRecentsView().getPagedViewOrientedState();
91         boolean canLauncherRotate = orientedState.isRecentsActivityRotationAllowed();
92         boolean isInLandscape = orientedState.getTouchRotation() != ROTATION_0;
93 
94         // Add overview actions to the menu when in in-place rotate landscape mode.
95         if (!canLauncherRotate && isInLandscape) {
96             // Add screenshot action to task menu.
97             SystemShortcut screenshotShortcut = TaskShortcutFactory.SCREENSHOT
98                     .getShortcut(activity, taskContainer);
99             if (screenshotShortcut != null) {
100                 shortcuts.add(screenshotShortcut);
101             }
102 
103             // Add modal action only if display orientation is the same as the device orientation.
104             if (orientedState.getDisplayRotation() == ROTATION_0) {
105                 SystemShortcut modalShortcut = TaskShortcutFactory.MODAL
106                         .getShortcut(activity, taskContainer);
107                 if (modalShortcut != null) {
108                     shortcuts.add(modalShortcut);
109                 }
110             }
111         }
112         return shortcuts;
113     }
114 
115 
116     /**
117      * Does NOT add split options in the following scenarios:
118      * * The taskView to add split options is already showing split screen tasks
119      * * There aren't at least 2 tasks in overview to show split options for
120      * * Device is in "Lock task mode"
121      * * The taskView to show split options for is the focused task AND we haven't started
122      *   scrolling in overview (if we haven't scrolled, there's a split overview action button so
123      *   we don't need this menu option)
124      */
addSplitOptions(List<SystemShortcut> outShortcuts, BaseDraggingActivity activity, TaskView taskView, DeviceProfile deviceProfile)125     private static void addSplitOptions(List<SystemShortcut> outShortcuts,
126             BaseDraggingActivity activity, TaskView taskView, DeviceProfile deviceProfile) {
127         RecentsView recentsView = taskView.getRecentsView();
128         PagedOrientationHandler orientationHandler = recentsView.getPagedOrientationHandler();
129         int[] taskViewTaskIds = taskView.getTaskIds();
130         boolean taskViewHasMultipleTasks = taskViewTaskIds[0] != -1 &&
131                 taskViewTaskIds[1] != -1;
132         boolean notEnoughTasksToSplit = recentsView.getTaskViewCount() < 2;
133         boolean isFocusedTask = deviceProfile.overviewShowAsGrid && taskView.isFocusedTask();
134         boolean isTaskInExpectedScrollPosition =
135                 recentsView.isTaskInExpectedScrollPosition(recentsView.indexOfChild(taskView));
136         ActivityManager activityManager =
137                 (ActivityManager) taskView.getContext().getSystemService(Context.ACTIVITY_SERVICE);
138         boolean isLockTaskMode = activityManager.isInLockTaskMode();
139 
140         if (taskViewHasMultipleTasks || notEnoughTasksToSplit || isLockTaskMode ||
141                 (isFocusedTask && isTaskInExpectedScrollPosition)) {
142             return;
143         }
144 
145         List<SplitPositionOption> positions =
146                 orientationHandler.getSplitPositionOptions(deviceProfile);
147         for (SplitPositionOption option : positions) {
148             outShortcuts.add(new SplitSelectSystemShortcut(activity, taskView, option));
149         }
150     }
151 
152     public TaskOverlay createOverlay(TaskThumbnailView thumbnailView) {
153         return new TaskOverlay(thumbnailView);
154     }
155 
156     /**
157      * Subclasses can attach any system listeners in this method, must be paired with
158      * {@link #removeListeners()}
159      */
160     public void initListeners() { }
161 
162     /**
163      * Subclasses should remove any system listeners in this method, must be paired with
164      * {@link #initListeners()}
165      */
166     public void removeListeners() { }
167 
168     /** Note that these will be shown in order from top to bottom, if available for the task. */
169     private static final TaskShortcutFactory[] MENU_OPTIONS = new TaskShortcutFactory[]{
170             TaskShortcutFactory.APP_INFO,
171             TaskShortcutFactory.SPLIT_SCREEN,
172             TaskShortcutFactory.PIN,
173             TaskShortcutFactory.INSTALL,
174             TaskShortcutFactory.FREE_FORM,
175             TaskShortcutFactory.WELLBEING
176     };
177 
178     /**
179      * Overlay on each task handling Overview Action Buttons.
180      */
181     public static class TaskOverlay<T extends OverviewActionsView> {
182 
183         protected final Context mApplicationContext;
184         protected final TaskThumbnailView mThumbnailView;
185 
186         private T mActionsView;
187         protected ImageActionsApi mImageApi;
188 
189         protected TaskOverlay(TaskThumbnailView taskThumbnailView) {
190             mApplicationContext = taskThumbnailView.getContext().getApplicationContext();
191             mThumbnailView = taskThumbnailView;
192             mImageApi = new ImageActionsApi(
193                 mApplicationContext, mThumbnailView::getThumbnail);
194         }
195 
196         protected T getActionsView() {
197             if (mActionsView == null) {
198                 mActionsView = BaseActivity.fromContext(mThumbnailView.getContext()).findViewById(
199                         R.id.overview_actions_view);
200             }
201             return mActionsView;
202         }
203 
204         /**
205          * Called when the current task is interactive for the user
206          */
207         public void initOverlay(Task task, ThumbnailData thumbnail, Matrix matrix,
208                 boolean rotated) {
209             getActionsView().updateDisabledFlags(DISABLED_NO_THUMBNAIL, thumbnail == null);
210 
211             if (thumbnail != null) {
212                 getActionsView().updateDisabledFlags(DISABLED_ROTATED, rotated);
213                 boolean isAllowedByPolicy = mThumbnailView.isRealSnapshot();
214                 getActionsView().setCallbacks(new OverlayUICallbacksImpl(isAllowedByPolicy, task));
215             }
216         }
217 
218         /**
219          * End rendering live tile in Overview.
220          *
221          * @param callback callback to run, after switching to screenshot
222          */
223         public void endLiveTileMode(@NonNull Runnable callback) {
224             if (ENABLE_QUICKSTEP_LIVE_TILE.get()) {
225                 RecentsView recentsView = mThumbnailView.getTaskView().getRecentsView();
226                 recentsView.switchToScreenshot(
227                         () -> recentsView.finishRecentsAnimation(true /* toRecents */,
228                                 false /* shouldPip */, callback));
229             } else {
callback.run()230                 callback.run();
231             }
232         }
233 
234         /**
235          * Called to save screenshot of the task thumbnail.
236          */
237         @SuppressLint("NewApi")
saveScreenshot(Task task)238         protected void saveScreenshot(Task task) {
239             if (mThumbnailView.isRealSnapshot()) {
240                 mImageApi.saveScreenshot(mThumbnailView.getThumbnail(),
241                         getTaskSnapshotBounds(), getTaskSnapshotInsets(), task.key);
242             } else {
243                 showBlockedByPolicyMessage();
244             }
245         }
246 
enterSplitSelect()247         private void enterSplitSelect() {
248             RecentsView overviewPanel = mThumbnailView.getTaskView().getRecentsView();
249             overviewPanel.initiateSplitSelect(mThumbnailView.getTaskView());
250         }
251 
252         /**
253          * Called when the overlay is no longer used.
254          */
reset()255         public void reset() {
256         }
257 
258         /**
259          * Called when the system wants to reset the modal visuals.
260          */
resetModalVisuals()261         public void resetModalVisuals() {
262         }
263 
264         /**
265          * Gets the modal state system shortcut.
266          */
getModalStateSystemShortcut(WorkspaceItemInfo itemInfo)267         public SystemShortcut getModalStateSystemShortcut(WorkspaceItemInfo itemInfo) {
268             return null;
269         }
270 
271         /**
272          * Sets full screen progress to the task overlay.
273          */
setFullscreenProgress(float progress)274         public void setFullscreenProgress(float progress) {
275         }
276 
277         /**
278          * Gets the system shortcut for the screenshot that will be added to the task menu.
279          */
getScreenshotShortcut(BaseDraggingActivity activity, ItemInfo iteminfo)280         public SystemShortcut getScreenshotShortcut(BaseDraggingActivity activity,
281                 ItemInfo iteminfo) {
282             return new ScreenshotSystemShortcut(activity, iteminfo);
283         }
284         /**
285          * Gets the task snapshot as it is displayed on the screen.
286          *
287          * @return the bounds of the snapshot in screen coordinates.
288          */
getTaskSnapshotBounds()289         public Rect getTaskSnapshotBounds() {
290             int[] location = new int[2];
291             mThumbnailView.getLocationOnScreen(location);
292 
293             return new Rect(location[0], location[1], mThumbnailView.getWidth() + location[0],
294                     mThumbnailView.getHeight() + location[1]);
295         }
296 
297         /**
298          * Gets the insets that the snapshot is drawn with.
299          *
300          * @return the insets in screen coordinates.
301          */
302         @RequiresApi(api = Build.VERSION_CODES.Q)
getTaskSnapshotInsets()303         public Insets getTaskSnapshotInsets() {
304             return mThumbnailView.getScaledInsets();
305         }
306 
307         /**
308          * Called when the device rotated.
309          */
updateOrientationState(RecentsOrientedState state)310         public void updateOrientationState(RecentsOrientedState state) {
311         }
312 
showBlockedByPolicyMessage()313         protected void showBlockedByPolicyMessage() {
314             Toast.makeText(
315                     mThumbnailView.getContext(),
316                     R.string.blocked_by_policy,
317                     Toast.LENGTH_LONG).show();
318         }
319 
320         private class ScreenshotSystemShortcut extends SystemShortcut {
321 
322             private final BaseDraggingActivity mActivity;
323 
ScreenshotSystemShortcut(BaseDraggingActivity activity, ItemInfo itemInfo)324             ScreenshotSystemShortcut(BaseDraggingActivity activity, ItemInfo itemInfo) {
325                 super(R.drawable.ic_screenshot, R.string.action_screenshot, activity, itemInfo);
326                 mActivity = activity;
327             }
328 
329             @Override
onClick(View view)330             public void onClick(View view) {
331                 saveScreenshot(mThumbnailView.getTaskView().getTask());
332                 dismissTaskMenuView(mActivity);
333             }
334         }
335 
336         protected class OverlayUICallbacksImpl implements OverlayUICallbacks {
337             protected final boolean mIsAllowedByPolicy;
338             protected final Task mTask;
339 
OverlayUICallbacksImpl(boolean isAllowedByPolicy, Task task)340             public OverlayUICallbacksImpl(boolean isAllowedByPolicy, Task task) {
341                 mIsAllowedByPolicy = isAllowedByPolicy;
342                 mTask = task;
343             }
344 
345             @SuppressLint("NewApi")
onScreenshot()346             public void onScreenshot() {
347                 endLiveTileMode(() -> saveScreenshot(mTask));
348             }
349 
onSplit()350             public void onSplit() {
351                 endLiveTileMode(TaskOverlay.this::enterSplitSelect);
352             }
353         }
354     }
355 
356     /**
357      * Callbacks the Ui can generate. This is the only way for a Ui to call methods on the
358      * controller.
359      */
360     public interface OverlayUICallbacks {
361         /** User has indicated they want to screenshot the current task. */
362         void onScreenshot();
363 
364         /** User wants to start split screen with current app. */
365         void onSplit();
366     }
367 }
368