• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2022 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.views;
18 
19 import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
20 
21 import static com.android.launcher3.LauncherState.NORMAL;
22 import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_UNDEFINED;
23 
24 import android.content.Context;
25 import android.graphics.Point;
26 import android.graphics.Rect;
27 import android.graphics.drawable.Drawable;
28 import android.graphics.drawable.LayerDrawable;
29 import android.graphics.drawable.ShapeDrawable;
30 import android.graphics.drawable.shapes.RoundRectShape;
31 import android.os.SystemProperties;
32 import android.util.AttributeSet;
33 import android.util.Log;
34 import android.util.SparseArray;
35 import android.view.MotionEvent;
36 import android.view.View;
37 import android.widget.FrameLayout;
38 
39 import androidx.annotation.NonNull;
40 import androidx.annotation.Nullable;
41 
42 import com.android.launcher3.DeviceProfile;
43 import com.android.launcher3.Launcher;
44 import com.android.launcher3.R;
45 import com.android.launcher3.Utilities;
46 import com.android.launcher3.util.RunnableList;
47 import com.android.quickstep.RecentsModel;
48 import com.android.quickstep.SystemUiProxy;
49 import com.android.quickstep.TaskThumbnailCache;
50 import com.android.quickstep.util.CancellableTask;
51 import com.android.quickstep.util.RecentsOrientedState;
52 import com.android.systemui.shared.recents.model.Task;
53 import com.android.systemui.shared.recents.model.ThumbnailData;
54 
55 import java.util.ArrayList;
56 import java.util.Arrays;
57 import java.util.Collections;
58 import java.util.HashMap;
59 import java.util.List;
60 import java.util.function.Consumer;
61 
62 /**
63  * TaskView that contains all tasks that are part of the desktop.
64  */
65 // TODO(b/249371338): TaskView needs to be refactored to have better support for N tasks.
66 public class DesktopTaskView extends TaskView {
67 
68     /** Flag to indicate whether desktop windowing proto 1 is enabled */
69     private static final boolean DESKTOP_IS_PROTO1_ENABLED = SystemProperties.getBoolean(
70             "persist.wm.debug.desktop_mode", false);
71 
72     /** Flag to indicate whether desktop windowing proto 2 is enabled */
73     public static final boolean DESKTOP_IS_PROTO2_ENABLED = SystemProperties.getBoolean(
74             "persist.wm.debug.desktop_mode_2", false);
75 
76     /** Flags to indicate whether desktop mode is available on the device */
77     public static final boolean DESKTOP_MODE_SUPPORTED =
78             DESKTOP_IS_PROTO1_ENABLED || DESKTOP_IS_PROTO2_ENABLED;
79 
80     private static final String TAG = DesktopTaskView.class.getSimpleName();
81 
82     private static final boolean DEBUG = true;
83 
84     @NonNull
85     private List<Task> mTasks = new ArrayList<>();
86 
87     private final ArrayList<TaskThumbnailView> mSnapshotViews = new ArrayList<>();
88 
89     /** Maps {@code taskIds} to corresponding {@link TaskThumbnailView}s */
90     private final SparseArray<TaskThumbnailView> mSnapshotViewMap = new SparseArray<>();
91 
92     private final ArrayList<CancellableTask<?>> mPendingThumbnailRequests = new ArrayList<>();
93 
94     private View mBackgroundView;
95 
DesktopTaskView(Context context)96     public DesktopTaskView(Context context) {
97         this(context, null);
98     }
99 
DesktopTaskView(Context context, AttributeSet attrs)100     public DesktopTaskView(Context context, AttributeSet attrs) {
101         this(context, attrs, 0);
102     }
103 
DesktopTaskView(Context context, AttributeSet attrs, int defStyleAttr)104     public DesktopTaskView(Context context, AttributeSet attrs, int defStyleAttr) {
105         super(context, attrs, defStyleAttr);
106     }
107 
108     @Override
onFinishInflate()109     protected void onFinishInflate() {
110         super.onFinishInflate();
111 
112         mBackgroundView = findViewById(R.id.background);
113 
114         int topMarginPx =
115                 mActivity.getDeviceProfile().overviewTaskThumbnailTopMarginPx;
116         FrameLayout.LayoutParams params = (LayoutParams) mBackgroundView.getLayoutParams();
117         params.topMargin = topMarginPx;
118         mBackgroundView.setLayoutParams(params);
119 
120         float[] outerRadii = new float[8];
121         Arrays.fill(outerRadii, getTaskCornerRadius());
122         RoundRectShape shape = new RoundRectShape(outerRadii, null, null);
123         ShapeDrawable background = new ShapeDrawable(shape);
124         background.setTint(getResources().getColor(android.R.color.system_neutral2_300,
125                 getContext().getTheme()));
126         // TODO(b/244348395): this should be wallpaper
127         mBackgroundView.setBackground(background);
128 
129         Drawable icon = getResources().getDrawable(R.drawable.ic_desktop, getContext().getTheme());
130         Drawable iconBackground = getResources().getDrawable(R.drawable.bg_circle,
131                 getContext().getTheme());
132         mIconView.setDrawable(new LayerDrawable(new Drawable[]{iconBackground, icon}));
133     }
134 
135     @Override
updateBorderBounds(Rect bounds)136     protected void updateBorderBounds(Rect bounds) {
137         bounds.set(mBackgroundView.getLeft(), mBackgroundView.getTop(), mBackgroundView.getRight(),
138                 mBackgroundView.getBottom());
139     }
140 
141     @Override
bind(Task task, RecentsOrientedState orientedState)142     public void bind(Task task, RecentsOrientedState orientedState) {
143         bind(Collections.singletonList(task), orientedState);
144     }
145 
146     /**
147      * Updates this desktop task to the gives task list defined in {@code tasks}
148      */
bind(List<Task> tasks, RecentsOrientedState orientedState)149     public void bind(List<Task> tasks, RecentsOrientedState orientedState) {
150         if (DEBUG) {
151             StringBuilder sb = new StringBuilder();
152             sb.append("bind tasks=").append(tasks.size()).append("\n");
153             for (Task task : tasks) {
154                 sb.append(" key=").append(task.key).append("\n");
155             }
156             Log.d(TAG, sb.toString());
157         }
158         cancelPendingLoadTasks();
159 
160         mTasks = new ArrayList<>(tasks);
161         mSnapshotViewMap.clear();
162 
163         // Ensure there are equal number of snapshot views and tasks.
164         // More tasks than views, add views. More views than tasks, remove views.
165         // TODO(b/251586230): use a ViewPool for creating TaskThumbnailViews
166         if (mSnapshotViews.size() > mTasks.size()) {
167             int diff = mSnapshotViews.size() - mTasks.size();
168             for (int i = 0; i < diff; i++) {
169                 TaskThumbnailView snapshotView = mSnapshotViews.remove(0);
170                 removeView(snapshotView);
171             }
172         } else if (mSnapshotViews.size() < mTasks.size()) {
173             int diff = mTasks.size() - mSnapshotViews.size();
174             for (int i = 0; i < diff; i++) {
175                 TaskThumbnailView snapshotView = new TaskThumbnailView(getContext());
176                 mSnapshotViews.add(snapshotView);
177                 addView(snapshotView, new LayoutParams(WRAP_CONTENT, WRAP_CONTENT));
178             }
179         }
180 
181         for (int i = 0; i < mTasks.size(); i++) {
182             Task task = mTasks.get(i);
183             TaskThumbnailView snapshotView = mSnapshotViews.get(i);
184             snapshotView.bind(task);
185             mSnapshotViewMap.put(task.key.id, snapshotView);
186         }
187 
188         updateTaskIdContainer();
189         updateTaskIdAttributeContainer();
190 
191         setOrientationState(orientedState);
192     }
193 
updateTaskIdContainer()194     private void updateTaskIdContainer() {
195         // TODO(b/249371338): TaskView expects the array to have at least 2 elements.
196         // At least 2 elements in the array
197         mTaskIdContainer = new int[Math.max(mTasks.size(), 2)];
198         for (int i = 0; i < mTasks.size(); i++) {
199             mTaskIdContainer[i] = mTasks.get(i).key.id;
200         }
201     }
202 
updateTaskIdAttributeContainer()203     private void updateTaskIdAttributeContainer() {
204         // TODO(b/249371338): TaskView expects the array to have at least 2 elements.
205         // At least 2 elements in the array
206         mTaskIdAttributeContainer = new TaskIdAttributeContainer[Math.max(mTasks.size(), 2)];
207         for (int i = 0; i < mTasks.size(); i++) {
208             Task task = mTasks.get(i);
209             TaskThumbnailView thumbnailView = mSnapshotViewMap.get(task.key.id);
210             mTaskIdAttributeContainer[i] = createAttributeContainer(task, thumbnailView);
211         }
212     }
213 
createAttributeContainer(Task task, TaskThumbnailView thumbnailView)214     private TaskIdAttributeContainer createAttributeContainer(Task task,
215             TaskThumbnailView thumbnailView) {
216         return new TaskIdAttributeContainer(task, thumbnailView, null, STAGE_POSITION_UNDEFINED);
217     }
218 
219     @Nullable
220     @Override
getTask()221     public Task getTask() {
222         // TODO(b/249371338): returning first task. This won't work well with multiple tasks.
223         return mTasks.size() > 0 ? mTasks.get(0) : null;
224     }
225 
226     @Override
getThumbnail()227     public TaskThumbnailView getThumbnail() {
228         // TODO(b/249371338): returning single thumbnail. This won't work well with multiple tasks.
229         Task task = getTask();
230         if (task != null) {
231             return mSnapshotViewMap.get(task.key.id);
232         }
233         // Return the place holder snapshot views. Callers expect this to be non-null
234         return mSnapshotView;
235     }
236 
237     @Override
containsTaskId(int taskId)238     public boolean containsTaskId(int taskId) {
239         // Thumbnail map contains taskId -> thumbnail map. Use the keys for contains
240         return mSnapshotViewMap.contains(taskId);
241     }
242 
243     @Override
onTaskListVisibilityChanged(boolean visible, int changes)244     public void onTaskListVisibilityChanged(boolean visible, int changes) {
245         cancelPendingLoadTasks();
246         if (visible) {
247             RecentsModel model = RecentsModel.INSTANCE.get(getContext());
248             TaskThumbnailCache thumbnailCache = model.getThumbnailCache();
249 
250             if (needsUpdate(changes, FLAG_UPDATE_THUMBNAIL)) {
251                 for (Task task : mTasks) {
252                     CancellableTask<?> thumbLoadRequest =
253                             thumbnailCache.updateThumbnailInBackground(task, thumbnailData -> {
254                                 TaskThumbnailView thumbnailView = mSnapshotViewMap.get(task.key.id);
255                                 if (thumbnailView != null) {
256                                     thumbnailView.setThumbnail(task, thumbnailData);
257                                 }
258                             });
259                     if (thumbLoadRequest != null) {
260                         mPendingThumbnailRequests.add(thumbLoadRequest);
261                     }
262                 }
263             }
264         } else {
265             if (needsUpdate(changes, FLAG_UPDATE_THUMBNAIL)) {
266                 for (Task task : mTasks) {
267                     TaskThumbnailView thumbnailView = mSnapshotViewMap.get(task.key.id);
268                     if (thumbnailView != null) {
269                         thumbnailView.setThumbnail(null, null);
270                     }
271                     // Reset the task thumbnail ref
272                     task.thumbnail = null;
273                 }
274             }
275         }
276     }
277 
278     @Override
setThumbnailOrientation(RecentsOrientedState orientationState)279     protected void setThumbnailOrientation(RecentsOrientedState orientationState) {
280         DeviceProfile deviceProfile = mActivity.getDeviceProfile();
281         int thumbnailTopMargin = deviceProfile.overviewTaskThumbnailTopMarginPx;
282 
283         LayoutParams snapshotParams = (LayoutParams) mSnapshotView.getLayoutParams();
284         snapshotParams.topMargin = thumbnailTopMargin;
285 
286         for (int i = 0; i < mSnapshotViewMap.size(); i++) {
287             TaskThumbnailView thumbnailView = mSnapshotViewMap.valueAt(i);
288             thumbnailView.setLayoutParams(snapshotParams);
289         }
290     }
291 
292     @Override
cancelPendingLoadTasks()293     protected void cancelPendingLoadTasks() {
294         for (CancellableTask<?> cancellableTask : mPendingThumbnailRequests) {
295             cancellableTask.cancel();
296         }
297         mPendingThumbnailRequests.clear();
298     }
299 
300     @Override
offerTouchToChildren(MotionEvent event)301     public boolean offerTouchToChildren(MotionEvent event) {
302         return false;
303     }
304 
305     @Override
showTaskMenuWithContainer(IconView iconView)306     protected boolean showTaskMenuWithContainer(IconView iconView) {
307         return false;
308     }
309 
310     @Override
launchTasks()311     public RunnableList launchTasks() {
312         SystemUiProxy.INSTANCE.get(getContext()).showDesktopApps();
313         Launcher.getLauncher(mActivity).getStateManager().goToState(NORMAL, false /* animated */);
314         return null;
315     }
316 
317     @Nullable
318     @Override
launchTaskAnimated()319     public RunnableList launchTaskAnimated() {
320         return launchTasks();
321     }
322 
323     @Override
launchTask(@onNull Consumer<Boolean> callback, boolean freezeTaskList)324     public void launchTask(@NonNull Consumer<Boolean> callback, boolean freezeTaskList) {
325         launchTasks();
326         callback.accept(true);
327     }
328 
329     @Override
isDesktopTask()330     public boolean isDesktopTask() {
331         return true;
332     }
333 
334     @Override
refreshThumbnails(@ullable HashMap<Integer, ThumbnailData> thumbnailDatas)335     void refreshThumbnails(@Nullable HashMap<Integer, ThumbnailData> thumbnailDatas) {
336         // Sets new thumbnails based on the incoming data and refreshes the rest.
337         // Create a copy of the thumbnail map, so we can track thumbnails that need refreshing.
338         SparseArray<TaskThumbnailView> thumbnailsToRefresh = mSnapshotViewMap.clone();
339         if (thumbnailDatas != null) {
340             for (Task task : mTasks) {
341                 int key = task.key.id;
342                 TaskThumbnailView thumbnailView = thumbnailsToRefresh.get(key);
343                 ThumbnailData thumbnailData = thumbnailDatas.get(key);
344                 if (thumbnailView != null && thumbnailData != null) {
345                     thumbnailView.setThumbnail(task, thumbnailData);
346                     // Remove this thumbnail from the list that should be refreshed.
347                     thumbnailsToRefresh.remove(key);
348                 }
349             }
350         }
351 
352         // Refresh the rest that were not updated.
353         for (int i = 0; i < thumbnailsToRefresh.size(); i++) {
354             thumbnailsToRefresh.valueAt(i).refresh();
355         }
356     }
357 
358     @Override
getThumbnails()359     public TaskThumbnailView[] getThumbnails() {
360         TaskThumbnailView[] thumbnails = new TaskThumbnailView[mSnapshotViewMap.size()];
361         for (int i = 0; i < thumbnails.length; i++) {
362             thumbnails[i] = mSnapshotViewMap.valueAt(i);
363         }
364         return thumbnails;
365     }
366 
367     @Override
onRecycle()368     public void onRecycle() {
369         resetPersistentViewTransforms();
370         // Clear any references to the thumbnail (it will be re-read either from the cache or the
371         // system on next bind)
372         for (Task task : mTasks) {
373             TaskThumbnailView thumbnailView = mSnapshotViewMap.get(task.key.id);
374             if (thumbnailView != null) {
375                 thumbnailView.setThumbnail(task, null);
376             }
377         }
378         setOverlayEnabled(false);
379         onTaskListVisibilityChanged(false);
380         setVisibility(VISIBLE);
381     }
382 
383     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)384     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
385         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
386         int containerWidth = MeasureSpec.getSize(widthMeasureSpec);
387         int containerHeight = MeasureSpec.getSize(heightMeasureSpec);
388 
389         setMeasuredDimension(containerWidth, containerHeight);
390 
391         int thumbnailTopMarginPx = mActivity.getDeviceProfile().overviewTaskThumbnailTopMarginPx;
392         containerHeight -= thumbnailTopMarginPx;
393 
394         int thumbnails = mSnapshotViewMap.size();
395         if (thumbnails == 0) {
396             return;
397         }
398 
399         int windowWidth = mActivity.getDeviceProfile().widthPx;
400         int windowHeight = mActivity.getDeviceProfile().heightPx;
401 
402         float scaleWidth = containerWidth / (float) windowWidth;
403         float scaleHeight = containerHeight / (float) windowHeight;
404 
405         if (DEBUG) {
406             Log.d(TAG,
407                     "onMeasure: container=[" + containerWidth + "," + containerHeight + "] window=["
408                             + windowWidth + "," + windowHeight + "] scale=[" + scaleWidth + ","
409                             + scaleHeight + "]");
410         }
411 
412         // Desktop tile is a shrunk down version of launcher and freeform task thumbnails.
413         for (int i = 0; i < mTasks.size(); i++) {
414             Task task = mTasks.get(i);
415             Rect taskSize = task.appBounds;
416             if (taskSize == null) {
417                 // Default to quarter of the desktop if we did not get app bounds.
418                 taskSize = new Rect(0, 0, windowWidth / 4, windowHeight / 4);
419             }
420 
421             int thumbWidth = (int) (taskSize.width() * scaleWidth);
422             int thumbHeight = (int) (taskSize.height() * scaleHeight);
423 
424             TaskThumbnailView thumbnailView = mSnapshotViewMap.get(task.key.id);
425             if (thumbnailView != null) {
426                 thumbnailView.measure(MeasureSpec.makeMeasureSpec(thumbWidth, MeasureSpec.EXACTLY),
427                         MeasureSpec.makeMeasureSpec(thumbHeight, MeasureSpec.EXACTLY));
428 
429                 // Position the task to the same position as it would be on the desktop
430                 Point positionInParent = task.positionInParent;
431                 if (positionInParent == null) {
432                     positionInParent = new Point(0, 0);
433                 }
434                 int taskX = (int) (positionInParent.x * scaleWidth);
435                 int taskY = (int) (positionInParent.y * scaleHeight);
436                 // move task down by margin size
437                 taskY += thumbnailTopMarginPx;
438                 thumbnailView.setX(taskX);
439                 thumbnailView.setY(taskY);
440 
441                 if (DEBUG) {
442                     Log.d(TAG, "onMeasure: task=" + task.key + " thumb=[" + thumbWidth + ","
443                             + thumbHeight + "]" + " pos=[" + taskX + "," + taskY + "]");
444                 }
445             }
446         }
447     }
448 
449     @Override
setOverlayEnabled(boolean overlayEnabled)450     public void setOverlayEnabled(boolean overlayEnabled) {
451         // Intentional no-op to prevent setting smart actions overlay on thumbnails
452     }
453 
454     @Override
setFullscreenProgress(float progress)455     public void setFullscreenProgress(float progress) {
456         // TODO(b/249371338): this copies parent implementation and makes it work for N thumbs
457         progress = Utilities.boundToRange(progress, 0, 1);
458         mFullscreenProgress = progress;
459         if (mFullscreenProgress > 0) {
460             // Don't show background while we are transitioning to/from fullscreen
461             mBackgroundView.setVisibility(INVISIBLE);
462         } else {
463             mBackgroundView.setVisibility(VISIBLE);
464         }
465         for (int i = 0; i < mSnapshotViewMap.size(); i++) {
466             TaskThumbnailView thumbnailView = mSnapshotViewMap.valueAt(i);
467             thumbnailView.getTaskOverlay().setFullscreenProgress(progress);
468             updateSnapshotRadius();
469         }
470     }
471 
472     @Override
updateSnapshotRadius()473     protected void updateSnapshotRadius() {
474         for (int i = 0; i < mSnapshotViewMap.size(); i++) {
475             mSnapshotViewMap.valueAt(i).setFullscreenParams(mCurrentFullscreenParams);
476         }
477     }
478 
479     @Override
setIconsAndBannersTransitionProgress(float progress, boolean invert)480     protected void setIconsAndBannersTransitionProgress(float progress, boolean invert) {
481         // no-op
482     }
483 
484     @Override
setColorTint(float amount, int tintColor)485     public void setColorTint(float amount, int tintColor) {
486         for (int i = 0; i < mSnapshotViewMap.size(); i++) {
487             mSnapshotViewMap.valueAt(i).setDimAlpha(amount);
488         }
489     }
490 
491     @Override
applyThumbnailSplashAlpha()492     protected void applyThumbnailSplashAlpha() {
493         for (int i = 0; i < mSnapshotViewMap.size(); i++) {
494             mSnapshotViewMap.valueAt(i).setSplashAlpha(mTaskThumbnailSplashAlpha);
495         }
496     }
497 
498     @Override
setThumbnailVisibility(int visibility, int taskId)499     void setThumbnailVisibility(int visibility, int taskId) {
500         for (int i = 0; i < mSnapshotViewMap.size(); i++) {
501             mSnapshotViewMap.valueAt(i).setVisibility(visibility);
502         }
503     }
504 
505     @Override
confirmSecondSplitSelectApp()506     protected boolean confirmSecondSplitSelectApp() {
507         // Desktop tile can't be in split screen
508         return false;
509     }
510 }
511