• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 package com.android.quickstep.views;
2 
3 import static android.app.ActivityTaskManager.INVALID_TASK_ID;
4 
5 import static com.android.launcher3.util.SplitConfigurationOptions.DEFAULT_SPLIT_RATIO;
6 import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT;
7 
8 import android.content.Context;
9 import android.graphics.PointF;
10 import android.graphics.Rect;
11 import android.util.AttributeSet;
12 import android.view.MotionEvent;
13 import android.view.View;
14 
15 import androidx.annotation.NonNull;
16 import androidx.annotation.Nullable;
17 
18 import com.android.launcher3.DeviceProfile;
19 import com.android.launcher3.R;
20 import com.android.launcher3.Utilities;
21 import com.android.launcher3.util.RunnableList;
22 import com.android.launcher3.util.SplitConfigurationOptions;
23 import com.android.launcher3.util.SplitConfigurationOptions.SplitBounds;
24 import com.android.launcher3.util.TransformingTouchDelegate;
25 import com.android.quickstep.RecentsModel;
26 import com.android.quickstep.TaskIconCache;
27 import com.android.quickstep.TaskThumbnailCache;
28 import com.android.quickstep.util.CancellableTask;
29 import com.android.quickstep.util.RecentsOrientedState;
30 import com.android.quickstep.util.SplitSelectStateController;
31 import com.android.quickstep.util.TaskViewSimulator;
32 import com.android.systemui.shared.recents.model.Task;
33 import com.android.systemui.shared.recents.model.ThumbnailData;
34 import com.android.systemui.shared.recents.utilities.PreviewPositionHelper;
35 import com.android.systemui.shared.system.InteractionJankMonitorWrapper;
36 
37 import java.util.HashMap;
38 import java.util.function.Consumer;
39 
40 /**
41  * TaskView that contains and shows thumbnails for not one, BUT TWO(!!) tasks
42  *
43  * That's right. If you call within the next 5 minutes we'll go ahead and double your order and
44  * send you !! TWO !! Tasks along with their TaskThumbnailViews complimentary. On. The. House.
45  * And not only that, we'll even clean up your thumbnail request if you don't like it.
46  * All the benefits of one TaskView, except DOUBLED!
47  *
48  * (Icon loading sold separately, fees may apply. Shipping & Handling for Overlays not included).
49  */
50 public class GroupedTaskView extends TaskView {
51 
52     @Nullable
53     private Task mSecondaryTask;
54     private TaskThumbnailView mSnapshotView2;
55     private IconView mIconView2;
56     @Nullable
57     private CancellableTask<ThumbnailData> mThumbnailLoadRequest2;
58     @Nullable
59     private CancellableTask mIconLoadRequest2;
60     private final float[] mIcon2CenterCoords = new float[2];
61     private TransformingTouchDelegate mIcon2TouchDelegate;
62     @Nullable private SplitBounds mSplitBoundsConfig;
63     private final DigitalWellBeingToast mDigitalWellBeingToast2;
64 
GroupedTaskView(Context context)65     public GroupedTaskView(Context context) {
66         this(context, null);
67     }
68 
GroupedTaskView(Context context, AttributeSet attrs)69     public GroupedTaskView(Context context, AttributeSet attrs) {
70         this(context, attrs, 0);
71     }
72 
GroupedTaskView(Context context, AttributeSet attrs, int defStyleAttr)73     public GroupedTaskView(Context context, AttributeSet attrs, int defStyleAttr) {
74         super(context, attrs, defStyleAttr);
75         mDigitalWellBeingToast2 = new DigitalWellBeingToast(mActivity, this);
76     }
77 
78     @Override
updateBorderBounds(Rect bounds)79     protected void updateBorderBounds(Rect bounds) {
80         if (mSplitBoundsConfig == null) {
81             super.updateBorderBounds(bounds);
82             return;
83         }
84         bounds.set(
85                 Math.min(mSnapshotView.getLeft() + Math.round(mSnapshotView.getTranslationX()),
86                         mSnapshotView2.getLeft() + Math.round(mSnapshotView2.getTranslationX())),
87                 Math.min(mSnapshotView.getTop() + Math.round(mSnapshotView.getTranslationY()),
88                         mSnapshotView2.getTop() + Math.round(mSnapshotView2.getTranslationY())),
89                 Math.max(mSnapshotView.getRight() + Math.round(mSnapshotView.getTranslationX()),
90                         mSnapshotView2.getRight() + Math.round(mSnapshotView2.getTranslationX())),
91                 Math.max(mSnapshotView.getBottom() + Math.round(mSnapshotView.getTranslationY()),
92                         mSnapshotView2.getBottom() + Math.round(mSnapshotView2.getTranslationY())));
93     }
94 
95     @Override
onFinishInflate()96     protected void onFinishInflate() {
97         super.onFinishInflate();
98         mSnapshotView2 = findViewById(R.id.bottomright_snapshot);
99         mIconView2 = findViewById(R.id.bottomRight_icon);
100         mIcon2TouchDelegate = new TransformingTouchDelegate(mIconView2);
101     }
102 
bind(Task primary, Task secondary, RecentsOrientedState orientedState, @Nullable SplitBounds splitBoundsConfig)103     public void bind(Task primary, Task secondary, RecentsOrientedState orientedState,
104             @Nullable SplitBounds splitBoundsConfig) {
105         super.bind(primary, orientedState);
106         mSecondaryTask = secondary;
107         mTaskIdContainer[1] = secondary.key.id;
108         mTaskIdAttributeContainer[1] = new TaskIdAttributeContainer(secondary, mSnapshotView2,
109                 mIconView2, STAGE_POSITION_BOTTOM_OR_RIGHT);
110         mTaskIdAttributeContainer[0].setStagePosition(
111                 SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT);
112         mSnapshotView2.bind(secondary);
113         mSplitBoundsConfig = splitBoundsConfig;
114         if (mSplitBoundsConfig == null) {
115             return;
116         }
117         mSnapshotView.getPreviewPositionHelper().setSplitBounds(TaskViewSimulator
118                         .convertSplitBounds(splitBoundsConfig),
119                 PreviewPositionHelper.STAGE_POSITION_TOP_OR_LEFT);
120         mSnapshotView2.getPreviewPositionHelper().setSplitBounds(TaskViewSimulator
121                         .convertSplitBounds(splitBoundsConfig),
122                 PreviewPositionHelper.STAGE_POSITION_BOTTOM_OR_RIGHT);
123     }
124 
125     /**
126      * Sets up an on-click listener and the visibility for show_windows icon on top of each task.
127      */
128     @Override
setUpShowAllInstancesListener()129     public void setUpShowAllInstancesListener() {
130         // sets up the listener for the left/top task
131         super.setUpShowAllInstancesListener();
132 
133         // right/bottom task's base package name
134         String taskPackageName = mTaskIdAttributeContainer[1].getTask().key.getPackageName();
135 
136         // icon of the right/bottom task
137         View showWindowsView = findViewById(R.id.show_windows_right);
138         updateFilterCallback(showWindowsView, getFilterUpdateCallback(taskPackageName));
139     }
140 
141     @Override
onTaskListVisibilityChanged(boolean visible, int changes)142     public void onTaskListVisibilityChanged(boolean visible, int changes) {
143         super.onTaskListVisibilityChanged(visible, changes);
144         if (visible) {
145             RecentsModel model = RecentsModel.INSTANCE.get(getContext());
146             TaskThumbnailCache thumbnailCache = model.getThumbnailCache();
147             TaskIconCache iconCache = model.getIconCache();
148 
149             if (needsUpdate(changes, FLAG_UPDATE_THUMBNAIL)) {
150                 mThumbnailLoadRequest2 = thumbnailCache.updateThumbnailInBackground(mSecondaryTask,
151                         thumbnailData -> mSnapshotView2.setThumbnail(
152                                 mSecondaryTask, thumbnailData
153                         ));
154             }
155 
156             if (needsUpdate(changes, FLAG_UPDATE_ICON)) {
157                 mIconLoadRequest2 = iconCache.updateIconInBackground(mSecondaryTask,
158                         (task) -> {
159                             setIcon(mIconView2, task.icon);
160                             mDigitalWellBeingToast2.initialize(mSecondaryTask);
161                             mDigitalWellBeingToast2.setSplitConfiguration(mSplitBoundsConfig);
162                             mDigitalWellBeingToast.setSplitConfiguration(mSplitBoundsConfig);
163                         });
164             }
165         } else {
166             if (needsUpdate(changes, FLAG_UPDATE_THUMBNAIL)) {
167                 mSnapshotView2.setThumbnail(null, null);
168                 // Reset the task thumbnail reference as well (it will be fetched from the cache or
169                 // reloaded next time we need it)
170                 mSecondaryTask.thumbnail = null;
171             }
172             if (needsUpdate(changes, FLAG_UPDATE_ICON)) {
173                 setIcon(mIconView2, null);
174             }
175         }
176     }
177 
updateSplitBoundsConfig(SplitBounds splitBounds)178     public void updateSplitBoundsConfig(SplitBounds splitBounds) {
179         mSplitBoundsConfig = splitBounds;
180         invalidate();
181     }
182 
getSplitRatio()183     public float getSplitRatio() {
184         if (mSplitBoundsConfig != null) {
185             return mSplitBoundsConfig.appsStackedVertically
186                     ? mSplitBoundsConfig.topTaskPercent : mSplitBoundsConfig.leftTaskPercent;
187         }
188         return DEFAULT_SPLIT_RATIO;
189     }
190 
191     @Override
offerTouchToChildren(MotionEvent event)192     public boolean offerTouchToChildren(MotionEvent event) {
193         computeAndSetIconTouchDelegate(mIconView2, mIcon2CenterCoords, mIcon2TouchDelegate);
194         if (mIcon2TouchDelegate.onTouchEvent(event)) {
195             return true;
196         }
197 
198         return super.offerTouchToChildren(event);
199     }
200 
201     @Override
cancelPendingLoadTasks()202     protected void cancelPendingLoadTasks() {
203         super.cancelPendingLoadTasks();
204         if (mThumbnailLoadRequest2 != null) {
205             mThumbnailLoadRequest2.cancel();
206             mThumbnailLoadRequest2 = null;
207         }
208         if (mIconLoadRequest2 != null) {
209             mIconLoadRequest2.cancel();
210             mIconLoadRequest2 = null;
211         }
212     }
213 
214     @Nullable
215     @Override
launchTaskAnimated()216     public RunnableList launchTaskAnimated() {
217         if (mTask == null || mSecondaryTask == null) {
218             return null;
219         }
220 
221         RunnableList endCallback = new RunnableList();
222         RecentsView recentsView = getRecentsView();
223         // Callbacks run from remote animation when recents animation not currently running
224         InteractionJankMonitorWrapper.begin(this,
225                 InteractionJankMonitorWrapper.CUJ_SPLIT_SCREEN_ENTER, "Enter form GroupedTaskView");
226         launchTaskInternal(success -> {
227             endCallback.executeAllAndDestroy();
228             InteractionJankMonitorWrapper.end(
229                     InteractionJankMonitorWrapper.CUJ_SPLIT_SCREEN_ENTER);
230         }, false /* freezeTaskList */, true /*launchingExistingTaskview*/);
231 
232 
233         // Callbacks get run from recentsView for case when recents animation already running
234         recentsView.addSideTaskLaunchCallback(endCallback);
235         return endCallback;
236     }
237 
238     @Override
launchTask(@onNull Consumer<Boolean> callback, boolean isQuickswitch)239     public void launchTask(@NonNull Consumer<Boolean> callback, boolean isQuickswitch) {
240         launchTaskInternal(callback, isQuickswitch, false /*launchingExistingTaskview*/);
241     }
242 
243     /**
244      * @param launchingExistingTaskView {@link SplitSelectStateController#launchExistingSplitPair}
245      * uses existence of GroupedTaskView as control flow of how to animate in the incoming task. If
246      * we're launching from overview (from overview thumbnails) then pass in {@code true},
247      * otherwise pass in {@code false} for case like quickswitching from home to task
248      */
launchTaskInternal(@onNull Consumer<Boolean> callback, boolean isQuickswitch, boolean launchingExistingTaskView)249     private void launchTaskInternal(@NonNull Consumer<Boolean> callback, boolean isQuickswitch,
250             boolean launchingExistingTaskView) {
251         getRecentsView().getSplitSelectController().launchExistingSplitPair(
252                 launchingExistingTaskView ? this : null, mTask.key.id,
253                 mSecondaryTask.key.id, SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT,
254                 callback, isQuickswitch, getSplitRatio());
255     }
256 
257     @Override
refreshThumbnails(@ullable HashMap<Integer, ThumbnailData> thumbnailDatas)258     void refreshThumbnails(@Nullable HashMap<Integer, ThumbnailData> thumbnailDatas) {
259         super.refreshThumbnails(thumbnailDatas);
260         if (mSecondaryTask != null && thumbnailDatas != null) {
261             final ThumbnailData thumbnailData = thumbnailDatas.get(mSecondaryTask.key.id);
262             if (thumbnailData != null) {
263                 mSnapshotView2.setThumbnail(mSecondaryTask, thumbnailData);
264                 return;
265             }
266         }
267 
268         mSnapshotView2.refresh();
269     }
270 
271     @Override
containsTaskId(int taskId)272     public boolean containsTaskId(int taskId) {
273         return (mTask != null && mTask.key.id == taskId)
274                 || (mSecondaryTask != null && mSecondaryTask.key.id == taskId);
275     }
276 
277     @Override
getThumbnails()278     public TaskThumbnailView[] getThumbnails() {
279         return new TaskThumbnailView[]{mSnapshotView, mSnapshotView2};
280     }
281 
282     @Override
getLastSelectedChildTaskIndex()283     protected int getLastSelectedChildTaskIndex() {
284         SplitSelectStateController splitSelectController =
285                 getRecentsView().getSplitSelectController();
286         if (splitSelectController.isDismissingFromSplitPair()) {
287             // return the container index of the task that wasn't initially selected to split with
288             // because that is the only remaining app that can be selected. The coordinate checks
289             // below aren't reliable since both of those views may be gone/transformed
290             int initSplitTaskId = getThisTaskCurrentlyInSplitSelection();
291             if (initSplitTaskId != INVALID_TASK_ID) {
292                 return initSplitTaskId == mTask.key.id ? 1 : 0;
293             }
294         }
295 
296         // Check which of the two apps was selected
297         if (isCoordInView(mIconView2, mLastTouchDownPosition)
298                 || isCoordInView(mSnapshotView2, mLastTouchDownPosition)) {
299             return 1;
300         }
301         return super.getLastSelectedChildTaskIndex();
302     }
303 
isCoordInView(View v, PointF position)304     private boolean isCoordInView(View v, PointF position) {
305         float[] localPos = new float[]{position.x, position.y};
306         Utilities.mapCoordInSelfToDescendant(v, this, localPos);
307         return Utilities.pointInView(v, localPos[0], localPos[1], 0f /* slop */);
308     }
309 
310     @Override
onRecycle()311     public void onRecycle() {
312         super.onRecycle();
313         mSnapshotView2.setThumbnail(mSecondaryTask, null);
314         mSplitBoundsConfig = null;
315     }
316 
317     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)318     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
319         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
320         int widthSize = MeasureSpec.getSize(widthMeasureSpec);
321         int heightSize = MeasureSpec.getSize(heightMeasureSpec);
322         setMeasuredDimension(widthSize, heightSize);
323         if (mSplitBoundsConfig == null || mSnapshotView == null || mSnapshotView2 == null) {
324             return;
325         }
326         int initSplitTaskId = getThisTaskCurrentlyInSplitSelection();
327         if (initSplitTaskId == INVALID_TASK_ID) {
328             getPagedOrientationHandler().measureGroupedTaskViewThumbnailBounds(mSnapshotView,
329                     mSnapshotView2, widthSize, heightSize, mSplitBoundsConfig,
330                     mActivity.getDeviceProfile(), getLayoutDirection() == LAYOUT_DIRECTION_RTL);
331             // Should we be having a separate translation step apart from the measuring above?
332             // The following only applies to large screen for now, but for future reference
333             // we'd want to abstract this out in PagedViewHandlers to get the primary/secondary
334             // translation directions
335             mSnapshotView.applySplitSelectTranslateX(mSnapshotView.getTranslationX());
336             mSnapshotView.applySplitSelectTranslateY(mSnapshotView.getTranslationY());
337             mSnapshotView2.applySplitSelectTranslateX(mSnapshotView2.getTranslationX());
338             mSnapshotView2.applySplitSelectTranslateY(mSnapshotView2.getTranslationY());
339         } else {
340             // Currently being split with this taskView, let the non-split selected thumbnail
341             // take up full thumbnail area
342             TaskIdAttributeContainer container =
343                     mTaskIdAttributeContainer[initSplitTaskId == mTask.key.id ? 1 : 0];
344             container.getThumbnailView().measure(widthMeasureSpec,
345                     View.MeasureSpec.makeMeasureSpec(
346                             heightSize -
347                                     mActivity.getDeviceProfile().overviewTaskThumbnailTopMarginPx,
348                             MeasureSpec.EXACTLY));
349         }
350         updateIconPlacement();
351     }
352 
353     @Override
setOverlayEnabled(boolean overlayEnabled)354     public void setOverlayEnabled(boolean overlayEnabled) {
355         // Intentional no-op to prevent setting smart actions overlay on thumbnails
356     }
357 
358     @Override
setOrientationState(RecentsOrientedState orientationState)359     public void setOrientationState(RecentsOrientedState orientationState) {
360         super.setOrientationState(orientationState);
361         DeviceProfile deviceProfile = mActivity.getDeviceProfile();
362         boolean isGridTask = deviceProfile.isTablet && !isFocusedTask();
363         int iconDrawableSize = isGridTask ? deviceProfile.overviewTaskIconDrawableSizeGridPx
364                 : deviceProfile.overviewTaskIconDrawableSizePx;
365         mIconView2.setDrawableSize(iconDrawableSize, iconDrawableSize);
366         mIconView2.setRotation(getPagedOrientationHandler().getDegreesRotated());
367         updateIconPlacement();
368         updateSecondaryDwbPlacement();
369     }
370 
updateIconPlacement()371     private void updateIconPlacement() {
372         if (mSplitBoundsConfig == null) {
373             return;
374         }
375 
376         DeviceProfile deviceProfile = mActivity.getDeviceProfile();
377         int taskIconHeight = deviceProfile.overviewTaskIconSizePx;
378         boolean isRtl = getLayoutDirection() == LAYOUT_DIRECTION_RTL;
379 
380         getPagedOrientationHandler().setSplitIconParams(mIconView, mIconView2,
381                 taskIconHeight, mSnapshotView.getMeasuredWidth(), mSnapshotView.getMeasuredHeight(),
382                 getMeasuredHeight(), getMeasuredWidth(), isRtl, deviceProfile,
383                 mSplitBoundsConfig);
384     }
385 
updateSecondaryDwbPlacement()386     private void updateSecondaryDwbPlacement() {
387         if (mSecondaryTask == null) {
388             return;
389         }
390         mDigitalWellBeingToast2.initialize(mSecondaryTask);
391     }
392 
393     @Override
updateSnapshotRadius()394     protected void updateSnapshotRadius() {
395         super.updateSnapshotRadius();
396         mSnapshotView2.setFullscreenParams(mCurrentFullscreenParams);
397     }
398 
399     @Override
setIconsAndBannersTransitionProgress(float progress, boolean invert)400     protected void setIconsAndBannersTransitionProgress(float progress, boolean invert) {
401         super.setIconsAndBannersTransitionProgress(progress, invert);
402         // Value set by super call
403         float scale = mIconView.getAlpha();
404         mIconView2.setAlpha(scale);
405         mDigitalWellBeingToast2.updateBannerOffset(1f - scale);
406     }
407 
408     @Override
setColorTint(float amount, int tintColor)409     public void setColorTint(float amount, int tintColor) {
410         super.setColorTint(amount, tintColor);
411         mIconView2.setIconColorTint(tintColor, amount);
412         mSnapshotView2.setDimAlpha(amount);
413         mDigitalWellBeingToast2.setBannerColorTint(tintColor, amount);
414     }
415 
416     @Override
applyThumbnailSplashAlpha()417     protected void applyThumbnailSplashAlpha() {
418         super.applyThumbnailSplashAlpha();
419         mSnapshotView2.setSplashAlpha(mTaskThumbnailSplashAlpha);
420     }
421 
422     @Override
refreshTaskThumbnailSplash()423     protected void refreshTaskThumbnailSplash() {
424         super.refreshTaskThumbnailSplash();
425         mSnapshotView2.refreshSplashView();
426     }
427 
428     @Override
resetViewTransforms()429     protected void resetViewTransforms() {
430         super.resetViewTransforms();
431         mSnapshotView2.resetViewTransforms();
432     }
433 
434     /**
435      * Sets visibility for thumbnails and associated elements (DWB banners).
436      * IconView is unaffected.
437      *
438      * When setting INVISIBLE, sets the visibility for the last selected child task.
439      * When setting VISIBLE (as a reset), sets the visibility for both tasks.
440      */
441     @Override
setThumbnailVisibility(int visibility, int taskId)442     void setThumbnailVisibility(int visibility, int taskId) {
443         if (visibility == VISIBLE) {
444             mSnapshotView.setVisibility(visibility);
445             mDigitalWellBeingToast.setBannerVisibility(visibility);
446             mSnapshotView2.setVisibility(visibility);
447             mDigitalWellBeingToast2.setBannerVisibility(visibility);
448         } else if (taskId == getTaskIds()[0]) {
449             mSnapshotView.setVisibility(visibility);
450             mDigitalWellBeingToast.setBannerVisibility(visibility);
451         } else {
452             mSnapshotView2.setVisibility(visibility);
453             mDigitalWellBeingToast2.setBannerVisibility(visibility);
454         }
455     }
456 }
457