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