1 package com.android.quickstep.views;
2 
3 import static com.android.launcher3.util.SplitConfigurationOptions.DEFAULT_SPLIT_RATIO;
4 import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT;
5 import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT;
6 
7 import android.content.Context;
8 import android.util.AttributeSet;
9 import android.view.MotionEvent;
10 
11 import androidx.annotation.NonNull;
12 import androidx.annotation.Nullable;
13 
14 import com.android.launcher3.DeviceProfile;
15 import com.android.launcher3.R;
16 import com.android.launcher3.util.RunnableList;
17 import com.android.launcher3.util.SplitConfigurationOptions.StagedSplitBounds;
18 import com.android.launcher3.util.TransformingTouchDelegate;
19 import com.android.quickstep.RecentsModel;
20 import com.android.quickstep.TaskIconCache;
21 import com.android.quickstep.TaskThumbnailCache;
22 import com.android.quickstep.util.CancellableTask;
23 import com.android.quickstep.util.RecentsOrientedState;
24 import com.android.systemui.shared.recents.model.Task;
25 import com.android.systemui.shared.recents.model.ThumbnailData;
26 
27 import java.util.HashMap;
28 import java.util.function.Consumer;
29 
30 /**
31  * TaskView that contains and shows thumbnails for not one, BUT TWO(!!) tasks
32  *
33  * That's right. If you call within the next 5 minutes we'll go ahead and double your order and
34  * send you !! TWO !! Tasks along with their TaskThumbnailViews complimentary. On. The. House.
35  * And not only that, we'll even clean up your thumbnail request if you don't like it.
36  * All the benefits of one TaskView, except DOUBLED!
37  *
38  * (Icon loading sold separately, fees may apply. Shipping & Handling for Overlays not included).
39  */
40 public class GroupedTaskView extends TaskView {
41 
42     @Nullable
43     private Task mSecondaryTask;
44     private TaskThumbnailView mSnapshotView2;
45     private IconView mIconView2;
46     @Nullable
47     private CancellableTask<ThumbnailData> mThumbnailLoadRequest2;
48     @Nullable
49     private CancellableTask mIconLoadRequest2;
50     private final float[] mIcon2CenterCoords = new float[2];
51     private TransformingTouchDelegate mIcon2TouchDelegate;
52     @Nullable private StagedSplitBounds mSplitBoundsConfig;
53     private final DigitalWellBeingToast mDigitalWellBeingToast2;
54 
55 
GroupedTaskView(Context context)56     public GroupedTaskView(Context context) {
57         this(context, null);
58     }
59 
GroupedTaskView(Context context, AttributeSet attrs)60     public GroupedTaskView(Context context, AttributeSet attrs) {
61         this(context, attrs, 0);
62     }
63 
GroupedTaskView(Context context, AttributeSet attrs, int defStyleAttr)64     public GroupedTaskView(Context context, AttributeSet attrs, int defStyleAttr) {
65         super(context, attrs, defStyleAttr);
66         mDigitalWellBeingToast2 = new DigitalWellBeingToast(mActivity, this);
67     }
68 
69     @Override
onFinishInflate()70     protected void onFinishInflate() {
71         super.onFinishInflate();
72         mSnapshotView2 = findViewById(R.id.bottomright_snapshot);
73         mIconView2 = findViewById(R.id.bottomRight_icon);
74         mIcon2TouchDelegate = new TransformingTouchDelegate(mIconView2);
75     }
76 
bind(Task primary, Task secondary, RecentsOrientedState orientedState, @Nullable StagedSplitBounds splitBoundsConfig)77     public void bind(Task primary, Task secondary, RecentsOrientedState orientedState,
78             @Nullable StagedSplitBounds splitBoundsConfig) {
79         super.bind(primary, orientedState);
80         mSecondaryTask = secondary;
81         mTaskIdContainer[1] = secondary.key.id;
82         mTaskIdAttributeContainer[1] = new TaskIdAttributeContainer(secondary, mSnapshotView2,
83                 mIconView2, STAGE_POSITION_BOTTOM_OR_RIGHT);
84         mTaskIdAttributeContainer[0].setStagePosition(STAGE_POSITION_TOP_OR_LEFT);
85         mSnapshotView2.bind(secondary);
86         mSplitBoundsConfig = splitBoundsConfig;
87     }
88 
89     @Override
onTaskListVisibilityChanged(boolean visible, int changes)90     public void onTaskListVisibilityChanged(boolean visible, int changes) {
91         super.onTaskListVisibilityChanged(visible, changes);
92         if (visible) {
93             RecentsModel model = RecentsModel.INSTANCE.get(getContext());
94             TaskThumbnailCache thumbnailCache = model.getThumbnailCache();
95             TaskIconCache iconCache = model.getIconCache();
96 
97             if (needsUpdate(changes, FLAG_UPDATE_THUMBNAIL)) {
98                 mThumbnailLoadRequest2 = thumbnailCache.updateThumbnailInBackground(mSecondaryTask,
99                         thumbnailData -> mSnapshotView2.setThumbnail(
100                                 mSecondaryTask, thumbnailData
101                         ));
102             }
103 
104             if (needsUpdate(changes, FLAG_UPDATE_ICON)) {
105                 mIconLoadRequest2 = iconCache.updateIconInBackground(mSecondaryTask,
106                         (task) -> {
107                             setIcon(mIconView2, task.icon);
108                             mDigitalWellBeingToast2.initialize(mSecondaryTask);
109                             mDigitalWellBeingToast2.setSplitConfiguration(mSplitBoundsConfig);
110                             mDigitalWellBeingToast.setSplitConfiguration(mSplitBoundsConfig);
111                         });
112             }
113         } else {
114             if (needsUpdate(changes, FLAG_UPDATE_THUMBNAIL)) {
115                 mSnapshotView2.setThumbnail(null, null);
116                 // Reset the task thumbnail reference as well (it will be fetched from the cache or
117                 // reloaded next time we need it)
118                 mSecondaryTask.thumbnail = null;
119             }
120             if (needsUpdate(changes, FLAG_UPDATE_ICON)) {
121                 setIcon(mIconView2, null);
122             }
123         }
124     }
125 
updateSplitBoundsConfig(StagedSplitBounds stagedSplitBounds)126     public void updateSplitBoundsConfig(StagedSplitBounds stagedSplitBounds) {
127         mSplitBoundsConfig = stagedSplitBounds;
128         invalidate();
129     }
130 
getSplitRatio()131     public float getSplitRatio() {
132         if (mSplitBoundsConfig != null) {
133             return mSplitBoundsConfig.appsStackedVertically
134                     ? mSplitBoundsConfig.topTaskPercent : mSplitBoundsConfig.leftTaskPercent;
135         }
136         return DEFAULT_SPLIT_RATIO;
137     }
138 
139     @Override
offerTouchToChildren(MotionEvent event)140     public boolean offerTouchToChildren(MotionEvent event) {
141         computeAndSetIconTouchDelegate(mIconView2, mIcon2CenterCoords, mIcon2TouchDelegate);
142         if (mIcon2TouchDelegate.onTouchEvent(event)) {
143             return true;
144         }
145 
146         return super.offerTouchToChildren(event);
147     }
148 
149     @Override
cancelPendingLoadTasks()150     protected void cancelPendingLoadTasks() {
151         super.cancelPendingLoadTasks();
152         if (mThumbnailLoadRequest2 != null) {
153             mThumbnailLoadRequest2.cancel();
154             mThumbnailLoadRequest2 = null;
155         }
156         if (mIconLoadRequest2 != null) {
157             mIconLoadRequest2.cancel();
158             mIconLoadRequest2 = null;
159         }
160     }
161 
162     @Nullable
163     @Override
launchTaskAnimated()164     public RunnableList launchTaskAnimated() {
165         if (mTask == null || mSecondaryTask == null) {
166             return null;
167         }
168 
169         RunnableList endCallback = new RunnableList();
170         RecentsView recentsView = getRecentsView();
171         // Callbacks run from remote animation when recents animation not currently running
172         recentsView.getSplitPlaceholder().launchTasks(this /*groupedTaskView*/,
173                 success -> endCallback.executeAllAndDestroy(),
174                 false /* freezeTaskList */);
175 
176         // Callbacks get run from recentsView for case when recents animation already running
177         recentsView.addSideTaskLaunchCallback(endCallback);
178         return endCallback;
179     }
180 
181     @Override
launchTask(@onNull Consumer<Boolean> callback, boolean freezeTaskList)182     public void launchTask(@NonNull Consumer<Boolean> callback, boolean freezeTaskList) {
183         getRecentsView().getSplitPlaceholder().launchTasks(mTask, mSecondaryTask,
184                 STAGE_POSITION_TOP_OR_LEFT, callback, freezeTaskList,
185                 getSplitRatio());
186     }
187 
188     @Override
refreshThumbnails(@ullable HashMap<Integer, ThumbnailData> thumbnailDatas)189     void refreshThumbnails(@Nullable HashMap<Integer, ThumbnailData> thumbnailDatas) {
190         super.refreshThumbnails(thumbnailDatas);
191         if (mSecondaryTask != null && thumbnailDatas != null) {
192             final ThumbnailData thumbnailData = thumbnailDatas.get(mSecondaryTask.key.id);
193             if (thumbnailData != null) {
194                 mSnapshotView2.setThumbnail(mSecondaryTask, thumbnailData);
195                 return;
196             }
197         }
198 
199         mSnapshotView2.refresh();
200     }
201 
202     @Override
getThumbnails()203     public TaskThumbnailView[] getThumbnails() {
204         return new TaskThumbnailView[]{mSnapshotView, mSnapshotView2};
205     }
206 
207     @Override
onRecycle()208     public void onRecycle() {
209         super.onRecycle();
210         mSnapshotView2.setThumbnail(mSecondaryTask, null);
211         mSplitBoundsConfig = null;
212     }
213 
214     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)215     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
216         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
217         int widthSize = MeasureSpec.getSize(widthMeasureSpec);
218         int heightSize = MeasureSpec.getSize(heightMeasureSpec);
219         setMeasuredDimension(widthSize, heightSize);
220         if (mSplitBoundsConfig == null || mSnapshotView == null || mSnapshotView2 == null) {
221             return;
222         }
223         getPagedOrientationHandler().measureGroupedTaskViewThumbnailBounds(mSnapshotView,
224                 mSnapshotView2, widthSize, heightSize, mSplitBoundsConfig,
225                 mActivity.getDeviceProfile());
226         updateIconPlacement();
227     }
228 
229     @Override
setOverlayEnabled(boolean overlayEnabled)230     public void setOverlayEnabled(boolean overlayEnabled) {
231         super.setOverlayEnabled(overlayEnabled);
232         mSnapshotView2.setOverlayEnabled(overlayEnabled);
233     }
234 
235     @Override
setOrientationState(RecentsOrientedState orientationState)236     public void setOrientationState(RecentsOrientedState orientationState) {
237         super.setOrientationState(orientationState);
238         DeviceProfile deviceProfile = mActivity.getDeviceProfile();
239         boolean isGridTask = deviceProfile.overviewShowAsGrid && !isFocusedTask();
240         int iconDrawableSize = isGridTask ? deviceProfile.overviewTaskIconDrawableSizeGridPx
241                 : deviceProfile.overviewTaskIconDrawableSizePx;
242         mIconView2.setDrawableSize(iconDrawableSize, iconDrawableSize);
243         mIconView2.setRotation(getPagedOrientationHandler().getDegreesRotated());
244         updateIconPlacement();
245     }
246 
updateIconPlacement()247     private void updateIconPlacement() {
248         if (mSplitBoundsConfig == null) {
249             return;
250         }
251 
252         DeviceProfile deviceProfile = mActivity.getDeviceProfile();
253         int taskIconHeight = deviceProfile.overviewTaskIconSizePx;
254         boolean isRtl = getLayoutDirection() == LAYOUT_DIRECTION_RTL;
255 
256         getPagedOrientationHandler().setSplitIconParams(mIconView, mIconView2,
257                 taskIconHeight, mSnapshotView.getMeasuredWidth(), mSnapshotView.getMeasuredHeight(),
258                 isRtl, deviceProfile, mSplitBoundsConfig);
259     }
260 
261     @Override
updateSnapshotRadius()262     protected void updateSnapshotRadius() {
263         super.updateSnapshotRadius();
264         mSnapshotView2.setFullscreenParams(mCurrentFullscreenParams);
265     }
266 
267     @Override
setIconAndDimTransitionProgress(float progress, boolean invert)268     protected void setIconAndDimTransitionProgress(float progress, boolean invert) {
269         super.setIconAndDimTransitionProgress(progress, invert);
270         // Value set by super call
271         float scale = mIconView.getAlpha();
272         mIconView2.setAlpha(scale);
273         mDigitalWellBeingToast2.updateBannerOffset(1f - scale,
274                 mCurrentFullscreenParams.mCurrentDrawnInsets.top
275                         + mCurrentFullscreenParams.mCurrentDrawnInsets.bottom);
276     }
277 
278     @Override
setColorTint(float amount, int tintColor)279     public void setColorTint(float amount, int tintColor) {
280         super.setColorTint(amount, tintColor);
281         mIconView2.setIconColorTint(tintColor, amount);
282         mSnapshotView2.setDimAlpha(amount);
283         mDigitalWellBeingToast2.setBannerColorTint(tintColor, amount);
284     }
285 }
286