• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 com.android.launcher3.Flags.enableRefactorTaskThumbnail;
20 import static com.android.quickstep.views.OverviewActionsView.DISABLED_NO_THUMBNAIL;
21 import static com.android.quickstep.views.OverviewActionsView.DISABLED_ROTATED;
22 
23 import android.annotation.SuppressLint;
24 import android.content.Context;
25 import android.graphics.Bitmap;
26 import android.graphics.Insets;
27 import android.graphics.Matrix;
28 import android.graphics.Rect;
29 import android.graphics.RectF;
30 import android.os.Build;
31 import android.view.View;
32 
33 import androidx.annotation.NonNull;
34 import androidx.annotation.Nullable;
35 import androidx.annotation.RequiresApi;
36 
37 import com.android.launcher3.BaseActivity;
38 import com.android.launcher3.R;
39 import com.android.launcher3.model.data.ItemInfo;
40 import com.android.launcher3.model.data.WorkspaceItemInfo;
41 import com.android.launcher3.popup.SystemShortcut;
42 import com.android.launcher3.util.ResourceBasedOverride;
43 import com.android.launcher3.views.ActivityContext;
44 import com.android.launcher3.views.Snackbar;
45 import com.android.quickstep.recents.domain.usecase.ThumbnailPosition;
46 import com.android.quickstep.util.RecentsOrientedState;
47 import com.android.quickstep.views.DesktopTaskView;
48 import com.android.quickstep.views.GroupedTaskView;
49 import com.android.quickstep.views.OverviewActionsView;
50 import com.android.quickstep.views.RecentsView;
51 import com.android.quickstep.views.RecentsViewContainer;
52 import com.android.quickstep.views.TaskContainer;
53 import com.android.quickstep.views.TaskView;
54 import com.android.systemui.shared.recents.model.Task;
55 import com.android.systemui.shared.recents.model.ThumbnailData;
56 
57 import java.util.ArrayList;
58 import java.util.List;
59 
60 /**
61  * Factory class to create and add an overlays on the TaskView
62  */
63 public class TaskOverlayFactory implements ResourceBasedOverride {
64 
getEnabledShortcuts(TaskView taskView, TaskContainer taskContainer)65     public static List<SystemShortcut> getEnabledShortcuts(TaskView taskView,
66             TaskContainer taskContainer) {
67         final ArrayList<SystemShortcut> shortcuts = new ArrayList<>();
68         final RecentsViewContainer container =
69                 RecentsViewContainer.containerFromContext(taskView.getContext());
70         for (TaskShortcutFactory menuOption : MENU_OPTIONS) {
71             if (taskView instanceof GroupedTaskView && !menuOption.showForGroupedTask()) {
72                 continue;
73             }
74             if (taskView instanceof DesktopTaskView && !menuOption.showForDesktopTask()) {
75                 continue;
76             }
77 
78             List<SystemShortcut> menuShortcuts = menuOption.getShortcuts(container, taskContainer);
79             if (menuShortcuts == null) {
80                 continue;
81             }
82             shortcuts.addAll(menuShortcuts);
83         }
84         return shortcuts;
85     }
86 
87     /** Creates a {@link TaskOverlay} associated with the provide {@link TaskContainer}. */
createOverlay(TaskContainer taskContainer)88     public TaskOverlay<?> createOverlay(TaskContainer taskContainer) {
89         return new TaskOverlay<>(taskContainer);
90     }
91 
92     /**
93      * Subclasses can attach any system listeners in this method, must be paired with
94      * {@link #removeListeners()}
95      */
initListeners()96     public void initListeners() {
97     }
98 
99     /**
100      * Subclasses should remove any system listeners in this method, must be paired with
101      * {@link #initListeners()}
102      */
removeListeners()103     public void removeListeners() {
104     }
105 
106     /**
107      * Clears any active state outside of the TaskOverlay lifecycle which might have built
108      * up over time
109      */
clearAllActiveState()110     public void clearAllActiveState() { }
111 
112     /** Note that these will be shown in order from top to bottom, if available for the task. */
113     private static final TaskShortcutFactory[] MENU_OPTIONS = new TaskShortcutFactory[]{
114             TaskShortcutFactory.APP_INFO,
115             TaskShortcutFactory.SPLIT_SELECT,
116             TaskShortcutFactory.PIN,
117             TaskShortcutFactory.INSTALL,
118             TaskShortcutFactory.FREE_FORM,
119             DesktopSystemShortcut.Companion.createFactory(),
120             ExternalDisplaySystemShortcut.Companion.createFactory(),
121             AspectRatioSystemShortcut.Companion.createFactory(),
122             TaskShortcutFactory.WELLBEING,
123             TaskShortcutFactory.SAVE_APP_PAIR,
124             TaskShortcutFactory.SCREENSHOT,
125             TaskShortcutFactory.MODAL,
126             TaskShortcutFactory.CLOSE,
127     };
128 
129     /**
130      * Overlay on each task handling Overview Action Buttons.
131      */
132     public static class TaskOverlay<T extends OverviewActionsView> {
133 
134         protected final Context mApplicationContext;
135         protected final TaskContainer mTaskContainer;
136 
137         private T mActionsView;
138         protected ImageActionsApi mImageApi;
139         private ThumbnailData mThumbnailData = null;
140 
TaskOverlay(TaskContainer taskContainer)141         protected TaskOverlay(TaskContainer taskContainer) {
142             mApplicationContext = taskContainer.getTaskView().getContext().getApplicationContext();
143             mTaskContainer = taskContainer;
144             mImageApi = new ImageActionsApi(mApplicationContext, this::getThumbnail);
145         }
146 
setThumbnailState(@ullable ThumbnailData thumbnailData)147         public void setThumbnailState(@Nullable ThumbnailData thumbnailData) {
148             mThumbnailData = thumbnailData;
149         }
150 
getThumbnail()151         protected @Nullable Bitmap getThumbnail() {
152             if (enableRefactorTaskThumbnail()) {
153                 return mThumbnailData == null ? null : mThumbnailData.getThumbnail();
154             } else {
155                 return mTaskContainer.getThumbnailViewDeprecated().getThumbnail();
156             }
157         }
158         /**
159          * Returns whether the snapshot is real. If the device is locked for the user of the task,
160          * the snapshot used will be an app-theme generated snapshot instead of a real snapshot.
161          */
isRealSnapshot()162         protected boolean isRealSnapshot() {
163             if (enableRefactorTaskThumbnail()) {
164                 if (mThumbnailData == null) return false;
165 
166                 return mThumbnailData.isRealSnapshot && !mTaskContainer.getTask().isLocked;
167             } else {
168                 return mTaskContainer.getThumbnailViewDeprecated().isRealSnapshot();
169             }
170         }
171 
172         /**
173          * Returns whether the snapshot is rotated compared to the current task orientation.
174          */
isThumbnailRotationDifferentFromTask()175         public boolean isThumbnailRotationDifferentFromTask() {
176             if (enableRefactorTaskThumbnail()) {
177                 ThumbnailPosition thumbnailPosition = mTaskContainer.getThumbnailPosition();
178                 return thumbnailPosition != null && thumbnailPosition.isRotated();
179             }
180 
181             return mTaskContainer.getThumbnailViewDeprecated()
182                     .isThumbnailRotationDifferentFromTask();
183         }
184 
getActionsView()185         protected T getActionsView() {
186             if (mActionsView == null) {
187                 mActionsView = (T) RecentsViewContainer.containerFromContext(
188                         mTaskContainer.getTaskView().getContext()).getActionsView();
189             }
190             return mActionsView;
191         }
192 
getTaskView()193         public TaskView getTaskView() {
194             return mTaskContainer.getTaskView();
195         }
196 
getSnapshotView()197         public View getSnapshotView() {
198             return mTaskContainer.getSnapshotView();
199         }
200 
201         /**
202          * Called when the current task is interactive for the user
203          */
initOverlay(Task task, @Nullable Bitmap thumbnail, Matrix matrix, boolean rotated)204         public void initOverlay(Task task, @Nullable Bitmap thumbnail, Matrix matrix,
205                 boolean rotated) {
206             if (!enableRefactorTaskThumbnail()) {
207                 getActionsView().updateDisabledFlags(DISABLED_NO_THUMBNAIL, thumbnail == null);
208             }
209 
210             if (thumbnail != null) {
211                 if (!enableRefactorTaskThumbnail()) {
212                     getActionsView().updateDisabledFlags(DISABLED_ROTATED, rotated);
213                 }
214                 getActionsView().setCallbacks(new OverlayUICallbacksImpl(isRealSnapshot(), task));
215             }
216         }
217 
218         /**
219          * End rendering live tile in Overview.
220          *
221          * @param callback callback to run, after switching to screenshot
222          */
endLiveTileMode(@onNull Runnable callback)223         public void endLiveTileMode(@NonNull Runnable callback) {
224             RecentsView recentsView =
225                     mTaskContainer.getTaskView().getRecentsView();
226             // Task has already been dismissed
227             if (recentsView == null) return;
228             recentsView.switchToScreenshot(
229                     () -> recentsView.finishRecentsAnimation(true /* toRecents */,
230                             false /* shouldPip */, callback));
231         }
232 
233         /**
234          * Called to save screenshot of the task thumbnail.
235          */
236         @SuppressLint("NewApi")
saveScreenshot(Task task)237         protected void saveScreenshot(Task task) {
238             if (isRealSnapshot()) {
239                 mImageApi.saveScreenshot(getThumbnail(),
240                         getTaskSnapshotBounds(), getTaskSnapshotInsets(), task.key);
241             } else {
242                 showBlockedByPolicyMessage();
243             }
244         }
245 
enterSplitSelect()246         protected void enterSplitSelect() {
247             RecentsView overviewPanel = mTaskContainer.getTaskView().getRecentsView();
248             // Task has already been dismissed
249             if (overviewPanel == null) return;
250             overviewPanel.initiateSplitSelect(mTaskContainer);
251         }
252 
saveAppPair()253         protected void saveAppPair() {
254             GroupedTaskView taskView = (GroupedTaskView) mTaskContainer.getTaskView();
255             taskView.getRecentsView().getSplitSelectController().getAppPairsController()
256                     .saveAppPair(taskView);
257         }
258 
259         /**
260          * Called when the overlay is no longer used.
261          */
reset()262         public void reset() {
263         }
264 
265         /**
266          * Called when the system wants to reset the modal visuals.
267          */
resetModalVisuals()268         public void resetModalVisuals() {
269         }
270 
271         /**
272          * Gets the modal state system shortcut.
273          */
getModalStateSystemShortcut(WorkspaceItemInfo itemInfo, View original)274         public SystemShortcut getModalStateSystemShortcut(WorkspaceItemInfo itemInfo,
275                 View original) {
276             return null;
277         }
278 
279         /**
280          * Sets full screen progress to the task overlay.
281          */
setFullscreenProgress(float progress)282         public void setFullscreenProgress(float progress) {
283         }
284 
285         /**
286          * Gets the system shortcut for the screenshot that will be added to the task menu.
287          */
getScreenshotShortcut(RecentsViewContainer container, ItemInfo iteminfo, View originalView)288         public SystemShortcut getScreenshotShortcut(RecentsViewContainer container,
289                 ItemInfo iteminfo, View originalView) {
290             return new ScreenshotSystemShortcut(container, iteminfo, originalView);
291         }
292 
293         /**
294          * Gets the task snapshot as it is displayed on the screen.
295          *
296          * @return the bounds of the snapshot in screen coordinates.
297          */
getTaskSnapshotBounds()298         public Rect getTaskSnapshotBounds() {
299             int[] location = new int[2];
300             mTaskContainer.getSnapshotView().getLocationOnScreen(location);
301 
302             return new Rect(location[0], location[1],
303                     mTaskContainer.getSnapshotView().getWidth() + location[0],
304                     mTaskContainer.getSnapshotView().getHeight() + location[1]);
305         }
306 
307         /**
308          * Gets the insets that the snapshot is drawn with.
309          *
310          * @return the insets in screen coordinates.
311          */
312         @RequiresApi(api = Build.VERSION_CODES.Q)
getTaskSnapshotInsets()313         public Insets getTaskSnapshotInsets() {
314             Bitmap thumbnail = getThumbnail();
315             if (thumbnail == null) {
316                 return Insets.NONE;
317             }
318 
319             RectF bitmapRect = new RectF(
320                     0,
321                     0,
322                     thumbnail.getWidth(),
323                     thumbnail.getHeight());
324             View snapshotView = mTaskContainer.getSnapshotView();
325             RectF viewRect = new RectF(0, 0, snapshotView.getMeasuredWidth(),
326                     snapshotView.getMeasuredHeight());
327 
328             // The position helper matrix tells us how to transform the bitmap to fit the view, the
329             // inverse tells us where the view would be in the bitmaps coordinates. The insets are
330             // the difference between the bitmap bounds and the projected view bounds.
331             Matrix boundsToBitmapSpace = new Matrix();
332             Matrix thumbnailMatrix;
333             if (enableRefactorTaskThumbnail()) {
334                 if (mTaskContainer.getThumbnailPosition() != null) {
335                     thumbnailMatrix = mTaskContainer.getThumbnailPosition().getMatrix();
336                 } else {
337                     thumbnailMatrix = Matrix.IDENTITY_MATRIX;
338                 }
339             } else {
340                 thumbnailMatrix = mTaskContainer.getThumbnailViewDeprecated().getThumbnailMatrix();
341             }
342             thumbnailMatrix.invert(boundsToBitmapSpace);
343             RectF boundsInBitmapSpace = new RectF();
344             boundsToBitmapSpace.mapRect(boundsInBitmapSpace, viewRect);
345 
346             RecentsViewContainer container = RecentsViewContainer.containerFromContext(
347                     getTaskView().getContext());
348             int bottomInset = container.getDeviceProfile().isTablet
349                     ? Math.round(bitmapRect.bottom - boundsInBitmapSpace.bottom) : 0;
350             return Insets.of(0, 0, 0, bottomInset);
351         }
352 
353         /**
354          * Called when the device rotated.
355          */
updateOrientationState(RecentsOrientedState state)356         public void updateOrientationState(RecentsOrientedState state) {
357         }
358 
showBlockedByPolicyMessage()359         protected void showBlockedByPolicyMessage() {
360             ActivityContext activityContext = ActivityContext.lookupContext(
361                     mTaskContainer.getTaskView().getContext());
362             String message = activityContext.getStringCache() != null
363                     ? activityContext.getStringCache().disabledByAdminMessage
364                     : mTaskContainer.getTaskView().getContext().getString(
365                             R.string.blocked_by_policy);
366 
367             Snackbar.show(BaseActivity.fromContext(
368                     mTaskContainer.getTaskView().getContext()), message, null);
369         }
370 
371         /** Called when the snapshot has updated its full screen drawing parameters. */
setFullscreenParams(FullscreenDrawParams fullscreenParams)372         public void setFullscreenParams(FullscreenDrawParams fullscreenParams) {}
373 
374         /** Sets visibility for the overlay associated elements. */
setVisibility(int visibility)375         public void setVisibility(int visibility) {}
376 
377         /** See {@link View#addChildrenForAccessibility(ArrayList)} */
addChildForAccessibility(ArrayList<View> outChildren)378         public void addChildForAccessibility(ArrayList<View> outChildren) {}
379 
380         private class ScreenshotSystemShortcut extends SystemShortcut {
381 
ScreenshotSystemShortcut(RecentsViewContainer container, ItemInfo itemInfo, View originalView)382             ScreenshotSystemShortcut(RecentsViewContainer container, ItemInfo itemInfo,
383                     View originalView) {
384                 super(R.drawable.ic_screenshot, R.string.action_screenshot, container, itemInfo,
385                         originalView);
386             }
387 
388             @Override
onClick(View view)389             public void onClick(View view) {
390                 saveScreenshot(mTaskContainer.getTask());
391                 dismissTaskMenuView();
392             }
393         }
394 
395         protected class OverlayUICallbacksImpl implements OverlayUICallbacks {
396             protected final boolean mIsAllowedByPolicy;
397             protected final Task mTask;
398 
OverlayUICallbacksImpl(boolean isAllowedByPolicy, Task task)399             public OverlayUICallbacksImpl(boolean isAllowedByPolicy, Task task) {
400                 mIsAllowedByPolicy = isAllowedByPolicy;
401                 mTask = task;
402             }
403 
404             @SuppressLint("NewApi")
onScreenshot()405             public void onScreenshot() {
406                 endLiveTileMode(() -> saveScreenshot(mTask));
407             }
408 
onSplit()409             public void onSplit() {
410                 endLiveTileMode(TaskOverlay.this::enterSplitSelect);
411             }
412 
onSaveAppPair()413             public void onSaveAppPair() {
414                 endLiveTileMode(TaskOverlay.this::saveAppPair);
415             }
416         }
417     }
418 
419     /**
420      * Callbacks the Ui can generate. This is the only way for a Ui to call methods on the
421      * controller.
422      */
423     public interface OverlayUICallbacks {
424         /** User has indicated they want to screenshot the current task. */
onScreenshot()425         void onScreenshot();
426 
427         /** User wants to start split screen with current app. */
onSplit()428         void onSplit();
429 
430         /** User wants to save an app pair with current group of apps. */
onSaveAppPair()431         void onSaveAppPair();
432     }
433 }
434