diff --git a/quickstep/res/layout/task_desktop.xml b/quickstep/res/layout/task_desktop.xml new file mode 100644 index 0000000000..0c8543f414 --- /dev/null +++ b/quickstep/res/layout/task_desktop.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + diff --git a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java index 006628cc9e..07ddcc8b31 100644 --- a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java +++ b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java @@ -1882,6 +1882,7 @@ public abstract class AbsSwipeUpHandler, } private void finishCurrentTransitionToRecents() { + // TODO(b/245569277#comment2): enable once isFreeformActive is implemented mStateCallback.setStateOnUiThread(STATE_CURRENT_TASK_FINISHED); if (mRecentsAnimationController != null) { mRecentsAnimationController.detachNavigationBarFromApp(true); diff --git a/quickstep/src/com/android/quickstep/RecentTasksList.java b/quickstep/src/com/android/quickstep/RecentTasksList.java index 6b616b1b07..7bcc661af7 100644 --- a/quickstep/src/com/android/quickstep/RecentTasksList.java +++ b/quickstep/src/com/android/quickstep/RecentTasksList.java @@ -17,6 +17,8 @@ package com.android.quickstep; import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR; +import static com.android.quickstep.views.DesktopTaskView.DESKTOP_MODE_SUPPORTED; +import static com.android.wm.shell.util.GroupedRecentTaskInfo.TYPE_FREEFORM; import android.annotation.TargetApi; import android.app.ActivityManager; @@ -30,6 +32,7 @@ import androidx.annotation.VisibleForTesting; import com.android.launcher3.util.LooperExecutor; import com.android.launcher3.util.SplitConfigurationOptions; +import com.android.quickstep.util.DesktopTask; import com.android.quickstep.util.GroupTask; import com.android.systemui.shared.recents.model.Task; import com.android.wm.shell.recents.IRecentTasksListener; @@ -253,8 +256,9 @@ public class RecentTasksList { }; TaskLoadResult allTasks = new TaskLoadResult(requestId, loadKeysOnly, rawTasks.size()); + for (GroupedRecentTaskInfo rawTask : rawTasks) { - if (rawTask.getType() == GroupedRecentTaskInfo.TYPE_FREEFORM) { + if (DESKTOP_MODE_SUPPORTED && rawTask.getType() == TYPE_FREEFORM) { GroupTask desktopTask = createDesktopTask(rawTask); allTasks.add(desktopTask); continue; @@ -284,14 +288,18 @@ public class RecentTasksList { return allTasks; } - private GroupTask createDesktopTask(GroupedRecentTaskInfo taskInfo) { - // TODO(b/244348395): create a subclass of GroupTask for desktop tile - // We need a single task information as the primary task. Use the first task - Task.TaskKey key = new Task.TaskKey(taskInfo.getTaskInfo1()); - Task task = new Task(key); - task.desktopTile = true; - task.topActivity = key.sourceComponent; - return new GroupTask(task, null, null); + private DesktopTask createDesktopTask(GroupedRecentTaskInfo recentTaskInfo) { + ArrayList tasks = new ArrayList<>(recentTaskInfo.getTaskInfoList().size()); + for (ActivityManager.RecentTaskInfo taskInfo : recentTaskInfo.getTaskInfoList()) { + Task.TaskKey key = new Task.TaskKey(taskInfo); + Task task = Task.from(key, taskInfo, false); + task.setLastSnapshotData(taskInfo); + task.positionInParent = taskInfo.positionInParent; + task.appBounds = taskInfo.configuration.windowConfiguration.getAppBounds(); + // TODO(b/244348395): tasks should be sorted from oldest to most recently used + tasks.add(task); + } + return new DesktopTask(tasks); } private SplitConfigurationOptions.SplitBounds convertSplitBounds( @@ -306,7 +314,7 @@ public class RecentTasksList { private ArrayList copyOf(ArrayList tasks) { ArrayList newTasks = new ArrayList<>(); for (int i = 0; i < tasks.size(); i++) { - newTasks.add(new GroupTask(tasks.get(i))); + newTasks.add(tasks.get(i).copy()); } return newTasks; } diff --git a/quickstep/src/com/android/quickstep/TaskViewUtils.java b/quickstep/src/com/android/quickstep/TaskViewUtils.java index 7d3d8a2ede..df80e2f867 100644 --- a/quickstep/src/com/android/quickstep/TaskViewUtils.java +++ b/quickstep/src/com/android/quickstep/TaskViewUtils.java @@ -302,9 +302,15 @@ public final class TaskViewUtils { // to follow the TaskViewSimulator. So the final matrix applied on the thumbnailView is: // Mt K(0)` K(t) Mt` TaskThumbnailView[] thumbnails = v.getThumbnails(); - Matrix[] mt = new Matrix[simulatorCopies.length]; - Matrix[] mti = new Matrix[simulatorCopies.length]; - for (int i = 0; i < thumbnails.length; i++) { + + // In case simulator copies and thumbnail size do no match, ensure we get the lesser. + // This ensures we do not create arrays with empty elements or attempt to references + // indexes out of array bounds. + final int matrixSize = Math.min(simulatorCopies.length, thumbnails.length); + + Matrix[] mt = new Matrix[matrixSize]; + Matrix[] mti = new Matrix[matrixSize]; + for (int i = 0; i < matrixSize; i++) { TaskThumbnailView ttv = thumbnails[i]; RectF localBounds = new RectF(0, 0, ttv.getWidth(), ttv.getHeight()); float[] tvBoundsMapped = new float[]{0, 0, ttv.getWidth(), ttv.getHeight()}; @@ -321,14 +327,14 @@ public final class TaskViewUtils { mti[i] = localMti; } - Matrix[] k0i = new Matrix[simulatorCopies.length]; - for (int i = 0; i < simulatorCopies.length; i++) { + Matrix[] k0i = new Matrix[matrixSize]; + for (int i = 0; i < matrixSize; i++) { k0i[i] = new Matrix(); simulatorCopies[i].getTaskViewSimulator().getCurrentMatrix().invert(k0i[i]); } Matrix animationMatrix = new Matrix(); out.addOnFrameCallback(() -> { - for (int i = 0; i < simulatorCopies.length; i++) { + for (int i = 0; i < matrixSize; i++) { animationMatrix.set(mt[i]); animationMatrix.postConcat(k0i[i]); animationMatrix.postConcat(simulatorCopies[i] diff --git a/quickstep/src/com/android/quickstep/util/DesktopTask.java b/quickstep/src/com/android/quickstep/util/DesktopTask.java new file mode 100644 index 0000000000..433d23fa97 --- /dev/null +++ b/quickstep/src/com/android/quickstep/util/DesktopTask.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.util; + +import com.android.quickstep.views.TaskView; +import com.android.systemui.shared.recents.model.Task; + +import java.util.ArrayList; + +/** + * A {@link Task} container that can contain N number of tasks that are part of the desktop in + * recent tasks list. + */ +public class DesktopTask extends GroupTask { + + public ArrayList tasks; + + public DesktopTask(ArrayList tasks) { + super(tasks.get(0), null, null, TaskView.Type.DESKTOP); + this.tasks = tasks; + } + + @Override + public boolean containsTask(int taskId) { + for (Task task : tasks) { + if (task.key.id == taskId) { + return true; + } + } + return false; + } + + @Override + public boolean hasMultipleTasks() { + return true; + } + + @Override + public DesktopTask copy() { + return new DesktopTask(tasks); + } +} diff --git a/quickstep/src/com/android/quickstep/util/GroupTask.java b/quickstep/src/com/android/quickstep/util/GroupTask.java index f30d00c722..2be4f0a519 100644 --- a/quickstep/src/com/android/quickstep/util/GroupTask.java +++ b/quickstep/src/com/android/quickstep/util/GroupTask.java @@ -20,6 +20,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.launcher3.util.SplitConfigurationOptions.SplitBounds; +import com.android.quickstep.views.TaskView; import com.android.systemui.shared.recents.model.Task; /** @@ -27,24 +28,25 @@ import com.android.systemui.shared.recents.model.Task; * are represented as an app-pair in the recents task list. */ public class GroupTask { - public @NonNull Task task1; - public @Nullable Task task2; - public @Nullable - SplitBounds mSplitBounds; + @NonNull + public final Task task1; + @Nullable + public final Task task2; + @Nullable + public final SplitBounds mSplitBounds; + @TaskView.Type + public final int taskViewType; - public GroupTask(@NonNull Task t1, @Nullable Task t2, - @Nullable SplitBounds splitBounds) { + public GroupTask(@NonNull Task t1, @Nullable Task t2, @Nullable SplitBounds splitBounds) { + this(t1, t2, splitBounds, t2 != null ? TaskView.Type.GROUPED : TaskView.Type.SINGLE); + } + + protected GroupTask(@NonNull Task t1, @Nullable Task t2, @Nullable SplitBounds splitBounds, + @TaskView.Type int taskViewType) { task1 = t1; task2 = t2; mSplitBounds = splitBounds; - } - - public GroupTask(@NonNull GroupTask group) { - task1 = new Task(group.task1); - task2 = group.task2 != null - ? new Task(group.task2) - : null; - mSplitBounds = group.mSplitBounds; + this.taskViewType = taskViewType; } public boolean containsTask(int taskId) { @@ -54,4 +56,14 @@ public class GroupTask { public boolean hasMultipleTasks() { return task2 != null; } + + /** + * Create a copy of this instance + */ + public GroupTask copy() { + return new GroupTask( + new Task(task1), + task2 != null ? new Task(task2) : null, + mSplitBounds); + } } diff --git a/quickstep/src/com/android/quickstep/views/DesktopTaskView.java b/quickstep/src/com/android/quickstep/views/DesktopTaskView.java new file mode 100644 index 0000000000..9874f9644a --- /dev/null +++ b/quickstep/src/com/android/quickstep/views/DesktopTaskView.java @@ -0,0 +1,467 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.views; + +import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; + +import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_UNDEFINED; + +import android.content.Context; +import android.graphics.Point; +import android.graphics.Rect; +import android.graphics.drawable.ShapeDrawable; +import android.graphics.drawable.shapes.RoundRectShape; +import android.os.SystemProperties; +import android.util.AttributeSet; +import android.util.Log; +import android.util.SparseArray; +import android.view.MotionEvent; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.launcher3.DeviceProfile; +import com.android.launcher3.Utilities; +import com.android.launcher3.touch.PagedOrientationHandler; +import com.android.launcher3.util.RunnableList; +import com.android.quickstep.RecentsModel; +import com.android.quickstep.SystemUiProxy; +import com.android.quickstep.TaskThumbnailCache; +import com.android.quickstep.util.CancellableTask; +import com.android.quickstep.util.RecentsOrientedState; +import com.android.systemui.shared.recents.model.Task; +import com.android.systemui.shared.recents.model.ThumbnailData; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.function.Consumer; + +/** + * TaskView that contains all tasks that are part of the desktop. + */ +// TODO(b/249371338): TaskView needs to be refactored to have better support for N tasks. +public class DesktopTaskView extends TaskView { + + /** Flag to indicate whether desktop mode is available on the device */ + public static final boolean DESKTOP_MODE_SUPPORTED = SystemProperties.getBoolean( + "persist.wm.debug.desktop_mode", false); + + private static final String TAG = DesktopTaskView.class.getSimpleName(); + + private static final boolean DEBUG = true; + + private List mTasks; + + private final ArrayList mSnapshotViews = new ArrayList<>(); + + /** Maps {@code taskIds} to corresponding {@link TaskThumbnailView}s */ + private final SparseArray mSnapshotViewMap = new SparseArray<>(); + + private final ArrayList> mPendingThumbnailRequests = new ArrayList<>(); + + public DesktopTaskView(Context context) { + this(context, null); + } + + public DesktopTaskView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public DesktopTaskView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + float[] outerRadii = new float[8]; + Arrays.fill(outerRadii, getTaskCornerRadius()); + RoundRectShape shape = new RoundRectShape(outerRadii, null, null); + ShapeDrawable background = new ShapeDrawable(shape); + background.setTint(getResources().getColor(android.R.color.system_neutral2_300)); + // TODO(b/244348395): this should be wallpaper + setBackground(background); + + mSnapshotViews.add(mSnapshotView); + } + + @Override + public void bind(Task task, RecentsOrientedState orientedState) { + bind(Collections.singletonList(task), orientedState); + } + + /** + * Updates this desktop task to the gives task list defined in {@code tasks} + */ + public void bind(List tasks, RecentsOrientedState orientedState) { + if (DEBUG) { + StringBuilder sb = new StringBuilder(); + sb.append("bind tasks=").append(tasks.size()).append("\n"); + for (Task task : tasks) { + sb.append(" key=").append(task.key).append("\n"); + } + Log.d(TAG, sb.toString()); + } + if (tasks.isEmpty()) { + return; + } + cancelPendingLoadTasks(); + + mTasks = tasks; + mSnapshotViewMap.clear(); + + // Ensure there are equal number of snapshot views and tasks. + // More tasks than views, add views. More views than tasks, remove views. + // TODO(b/251586230): use a ViewPool for creating TaskThumbnailViews + if (mSnapshotViews.size() > mTasks.size()) { + int diff = mSnapshotViews.size() - mTasks.size(); + for (int i = 0; i < diff; i++) { + TaskThumbnailView snapshotView = mSnapshotViews.remove(0); + removeView(snapshotView); + } + } else if (mSnapshotViews.size() < mTasks.size()) { + int diff = mTasks.size() - mSnapshotViews.size(); + for (int i = 0; i < diff; i++) { + TaskThumbnailView snapshotView = new TaskThumbnailView(getContext()); + mSnapshotViews.add(snapshotView); + addView(snapshotView, new LayoutParams(WRAP_CONTENT, WRAP_CONTENT)); + } + } + + for (int i = 0; i < mTasks.size(); i++) { + Task task = mTasks.get(i); + TaskThumbnailView snapshotView = mSnapshotViews.get(i); + snapshotView.bind(task); + mSnapshotViewMap.put(task.key.id, snapshotView); + } + + updateTaskIdContainer(); + updateTaskIdAttributeContainer(); + + setOrientationState(orientedState); + } + + private void updateTaskIdContainer() { + // TODO(b/249371338): TaskView expects the array to have at least 2 elements. + // At least 2 elements in the array + mTaskIdContainer = new int[Math.max(mTasks.size(), 2)]; + for (int i = 0; i < mTasks.size(); i++) { + mTaskIdContainer[i] = mTasks.get(i).key.id; + } + } + + private void updateTaskIdAttributeContainer() { + // TODO(b/249371338): TaskView expects the array to have at least 2 elements. + // At least 2 elements in the array + mTaskIdAttributeContainer = new TaskIdAttributeContainer[Math.max(mTasks.size(), 2)]; + for (int i = 0; i < mTasks.size(); i++) { + Task task = mTasks.get(i); + TaskThumbnailView thumbnailView = mSnapshotViewMap.get(task.key.id); + mTaskIdAttributeContainer[i] = createAttributeContainer(task, thumbnailView); + } + } + + private TaskIdAttributeContainer createAttributeContainer(Task task, + TaskThumbnailView thumbnailView) { + return new TaskIdAttributeContainer(task, thumbnailView, null, STAGE_POSITION_UNDEFINED); + } + + @Nullable + @Override + public Task getTask() { + // TODO(b/249371338): returning first task. This won't work well with multiple tasks. + return mTasks.size() > 0 ? mTasks.get(0) : null; + } + + @Override + public TaskThumbnailView getThumbnail() { + // TODO(b/249371338): returning single thumbnail. This won't work well with multiple tasks. + Task task = getTask(); + if (task != null) { + return mSnapshotViewMap.get(task.key.id); + } + return null; + } + + @Override + public boolean containsTaskId(int taskId) { + // Thumbnail map contains taskId -> thumbnail map. Use the keys for contains + return mSnapshotViewMap.contains(taskId); + } + + @Override + public void onTaskListVisibilityChanged(boolean visible, int changes) { + cancelPendingLoadTasks(); + if (visible) { + RecentsModel model = RecentsModel.INSTANCE.get(getContext()); + TaskThumbnailCache thumbnailCache = model.getThumbnailCache(); + + if (needsUpdate(changes, FLAG_UPDATE_THUMBNAIL)) { + for (Task task : mTasks) { + CancellableTask thumbLoadRequest = + thumbnailCache.updateThumbnailInBackground(task, thumbnailData -> { + TaskThumbnailView thumbnailView = mSnapshotViewMap.get(task.key.id); + if (thumbnailView != null) { + thumbnailView.setThumbnail(task, thumbnailData); + } + }); + if (thumbLoadRequest != null) { + mPendingThumbnailRequests.add(thumbLoadRequest); + } + } + } + } else { + if (needsUpdate(changes, FLAG_UPDATE_THUMBNAIL)) { + for (Task task : mTasks) { + TaskThumbnailView thumbnailView = mSnapshotViewMap.get(task.key.id); + if (thumbnailView != null) { + thumbnailView.setThumbnail(null, null); + } + // Reset the task thumbnail ref + task.thumbnail = null; + } + } + } + } + + @Override + public void setOrientationState(RecentsOrientedState orientationState) { + // TODO(b/249371338): this copies logic from TaskView + PagedOrientationHandler orientationHandler = orientationState.getOrientationHandler(); + boolean isRtl = getLayoutDirection() == LAYOUT_DIRECTION_RTL; + DeviceProfile deviceProfile = mActivity.getDeviceProfile(); + + LayoutParams iconParams = (LayoutParams) mIconView.getLayoutParams(); + + int thumbnailTopMargin = deviceProfile.overviewTaskThumbnailTopMarginPx; + int taskIconHeight = deviceProfile.overviewTaskIconSizePx; + int taskMargin = deviceProfile.overviewTaskMarginPx; + + orientationHandler.setTaskIconParams(iconParams, taskMargin, taskIconHeight, + thumbnailTopMargin, isRtl); + + LayoutParams snapshotParams = (LayoutParams) mSnapshotView.getLayoutParams(); + snapshotParams.topMargin = thumbnailTopMargin; + + for (int i = 0; i < mSnapshotViewMap.size(); i++) { + TaskThumbnailView thumbnailView = mSnapshotViewMap.valueAt(i); + thumbnailView.setLayoutParams(snapshotParams); + } + } + + @Override + protected void cancelPendingLoadTasks() { + for (CancellableTask cancellableTask : mPendingThumbnailRequests) { + cancellableTask.cancel(); + } + mPendingThumbnailRequests.clear(); + } + + @Override + public boolean offerTouchToChildren(MotionEvent event) { + return false; + } + + @Override + protected boolean showTaskMenuWithContainer(IconView iconView) { + return false; + } + + @Nullable + @Override + public RunnableList launchTaskAnimated() { + RunnableList endCallback = new RunnableList(); + SystemUiProxy.INSTANCE.get(getContext()).showDesktopApps(); + RecentsView recentsView = getRecentsView(); + recentsView.addSideTaskLaunchCallback(endCallback); + return endCallback; + } + + @Override + public void launchTask(@NonNull Consumer callback, boolean freezeTaskList) { + SystemUiProxy.INSTANCE.get(getContext()).showDesktopApps(); + callback.accept(true); + } + + @Override + void refreshThumbnails(@Nullable HashMap thumbnailDatas) { + // Sets new thumbnails based on the incoming data and refreshes the rest. + // Create a copy of the thumbnail map, so we can track thumbnails that need refreshing. + SparseArray thumbnailsToRefresh = mSnapshotViewMap.clone(); + if (thumbnailDatas != null) { + for (Task task : mTasks) { + int key = task.key.id; + TaskThumbnailView thumbnailView = thumbnailsToRefresh.get(key); + ThumbnailData thumbnailData = thumbnailDatas.get(key); + if (thumbnailView != null && thumbnailData != null) { + thumbnailView.setThumbnail(task, thumbnailData); + // Remove this thumbnail from the list that should be refreshed. + thumbnailsToRefresh.remove(key); + } + } + } + + // Refresh the rest that were not updated. + for (int i = 0; i < thumbnailsToRefresh.size(); i++) { + thumbnailsToRefresh.valueAt(i).refresh(); + } + } + + @Override + public TaskThumbnailView[] getThumbnails() { + TaskThumbnailView[] thumbnails = new TaskThumbnailView[mSnapshotViewMap.size()]; + for (int i = 0; i < thumbnails.length; i++) { + thumbnails[i] = mSnapshotViewMap.valueAt(i); + } + return thumbnails; + } + + @Override + public void onRecycle() { + resetPersistentViewTransforms(); + // Clear any references to the thumbnail (it will be re-read either from the cache or the + // system on next bind) + for (Task task : mTasks) { + TaskThumbnailView thumbnailView = mSnapshotViewMap.get(task.key.id); + if (thumbnailView != null) { + thumbnailView.setThumbnail(task, null); + } + } + setOverlayEnabled(false); + onTaskListVisibilityChanged(false); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + int containerWidth = MeasureSpec.getSize(widthMeasureSpec); + int containerHeight = MeasureSpec.getSize(heightMeasureSpec); + + setMeasuredDimension(containerWidth, containerHeight); + + int thumbnails = mSnapshotViewMap.size(); + if (thumbnails == 0) { + return; + } + + int windowWidth = mActivity.getDeviceProfile().widthPx; + int windowHeight = mActivity.getDeviceProfile().heightPx; + + float scaleWidth = containerWidth / (float) windowWidth; + float scaleHeight = containerHeight / (float) windowHeight; + + if (DEBUG) { + Log.d(TAG, + "onMeasure: container=[" + containerWidth + "," + containerHeight + "] window=[" + + windowWidth + "," + windowHeight + "] scale=[" + scaleWidth + "," + + scaleHeight + "]"); + } + + // Desktop tile is a shrunk down version of launcher and freeform task thumbnails. + for (int i = 0; i < mTasks.size(); i++) { + Task task = mTasks.get(i); + Rect taskSize = task.appBounds; + if (taskSize == null) { + // Default to quarter of the desktop if we did not get app bounds. + taskSize = new Rect(0, 0, windowWidth / 4, windowHeight / 4); + } + + int thumbWidth = (int) (taskSize.width() * scaleWidth); + int thumbHeight = (int) (taskSize.height() * scaleHeight); + + TaskThumbnailView thumbnailView = mSnapshotViewMap.get(task.key.id); + if (thumbnailView != null) { + thumbnailView.measure(MeasureSpec.makeMeasureSpec(thumbWidth, MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(thumbHeight, MeasureSpec.EXACTLY)); + + // Position the task to the same position as it would be on the desktop + Point positionInParent = task.positionInParent; + if (positionInParent == null) { + positionInParent = new Point(0, 0); + } + int taskX = (int) (positionInParent.x * scaleWidth); + int taskY = (int) (positionInParent.y * scaleHeight); + thumbnailView.setX(taskX); + thumbnailView.setY(taskY); + + if (DEBUG) { + Log.d(TAG, "onMeasure: task=" + task.key + " thumb=[" + thumbWidth + "," + + thumbHeight + "]" + " pos=[" + taskX + "," + taskY + "]"); + } + } + } + } + + @Override + public void setOverlayEnabled(boolean overlayEnabled) { + // Intentional no-op to prevent setting smart actions overlay on thumbnails + } + + @Override + public void setFullscreenProgress(float progress) { + // TODO(b/249371338): this copies parent implementation and makes it work for N thumbs + progress = Utilities.boundToRange(progress, 0, 1); + mFullscreenProgress = progress; + for (int i = 0; i < mSnapshotViewMap.size(); i++) { + TaskThumbnailView thumbnailView = mSnapshotViewMap.valueAt(i); + thumbnailView.getTaskOverlay().setFullscreenProgress(progress); + updateSnapshotRadius(); + } + } + + @Override + protected void updateSnapshotRadius() { + for (int i = 0; i < mSnapshotViewMap.size(); i++) { + mSnapshotViewMap.valueAt(i).setFullscreenParams(mCurrentFullscreenParams); + } + } + + @Override + protected void setIconAndDimTransitionProgress(float progress, boolean invert) { + // no-op + } + + @Override + public void setColorTint(float amount, int tintColor) { + for (int i = 0; i < mSnapshotViewMap.size(); i++) { + mSnapshotViewMap.valueAt(i).setDimAlpha(amount); + } + } + + @Override + protected void applyThumbnailSplashAlpha() { + for (int i = 0; i < mSnapshotViewMap.size(); i++) { + mSnapshotViewMap.valueAt(i).setSplashAlpha(mTaskThumbnailSplashAlpha); + } + } + + @Override + void setThumbnailVisibility(int visibility) { + for (int i = 0; i < mSnapshotViewMap.size(); i++) { + mSnapshotViewMap.valueAt(i).setVisibility(visibility); + } + } + + @Override + protected boolean confirmSecondSplitSelectApp() { + // Desktop tile can't be in split screen + return false; + } +} diff --git a/quickstep/src/com/android/quickstep/views/GroupedTaskView.java b/quickstep/src/com/android/quickstep/views/GroupedTaskView.java index 3a5f606467..71b0c60971 100644 --- a/quickstep/src/com/android/quickstep/views/GroupedTaskView.java +++ b/quickstep/src/com/android/quickstep/views/GroupedTaskView.java @@ -224,6 +224,12 @@ public class GroupedTaskView extends TaskView { mSnapshotView2.refresh(); } + @Override + public boolean containsTaskId(int taskId) { + return (mTask != null && mTask.key.id == taskId) + || (mSecondaryTask != null && mSecondaryTask.key.id == taskId); + } + @Override public TaskThumbnailView[] getThumbnails() { return new TaskThumbnailView[]{mSnapshotView, mSnapshotView2}; diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java index 0e0acf0173..eeaf23821d 100644 --- a/quickstep/src/com/android/quickstep/views/RecentsView.java +++ b/quickstep/src/com/android/quickstep/views/RecentsView.java @@ -54,6 +54,7 @@ import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITIO import static com.android.launcher3.util.SystemUiController.UI_STATE_FULLSCREEN_TASK; import static com.android.quickstep.TaskUtils.checkCurrentOrManagedUserId; import static com.android.quickstep.views.ClearAllButton.DISMISS_ALPHA; +import static com.android.quickstep.views.DesktopTaskView.DESKTOP_MODE_SUPPORTED; import static com.android.quickstep.views.OverviewActionsView.FLAG_IS_NOT_TABLET; import static com.android.quickstep.views.OverviewActionsView.FLAG_SINGLE_TASK; import static com.android.quickstep.views.OverviewActionsView.HIDDEN_NON_ZERO_ROTATION; @@ -72,6 +73,7 @@ import android.animation.PropertyValuesHolder; import android.animation.ValueAnimator; import android.annotation.SuppressLint; import android.annotation.TargetApi; +import android.app.WindowConfiguration; import android.content.Context; import android.content.LocusId; import android.content.res.Configuration; @@ -171,6 +173,7 @@ import com.android.quickstep.ViewUtils; import com.android.quickstep.util.ActiveGestureErrorDetector; import com.android.quickstep.util.ActiveGestureLog; import com.android.quickstep.util.AnimUtils; +import com.android.quickstep.util.DesktopTask; import com.android.quickstep.util.GroupTask; import com.android.quickstep.util.LayoutUtils; import com.android.quickstep.util.RecentsOrientedState; @@ -195,6 +198,7 @@ import com.android.systemui.shared.system.TaskStackChangeListeners; import com.android.wm.shell.pip.IPipAnimationListener; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Objects; @@ -477,10 +481,11 @@ public abstract class RecentsView mTaskViewPool; private final ViewPool mGroupedTaskViewPool; + private final ViewPool mDesktopTaskViewPool; private final TaskOverlayFactory mTaskOverlayFactory; @@ -737,6 +742,8 @@ public abstract class RecentsView(context, this, R.layout.task_grouped, 20 /* max size */, 10 /* initial size */); + mDesktopTaskViewPool = new ViewPool<>(context, this, R.layout.task_desktop, + 5 /* max size */, 1 /* initial size */); mIsRtl = mOrientationHandler.getRecentsRtlSetting(getResources()); setLayoutDirection(mIsRtl ? View.LAYOUT_DIRECTION_RTL : View.LAYOUT_DIRECTION_LTR); @@ -981,6 +988,8 @@ public abstract class RecentsView= 0; i--) { GroupTask groupTask = taskGroups.get(i); - boolean hasMultipleTasks = groupTask.hasMultipleTasks(); - TaskView taskView = getTaskViewFromPool(hasMultipleTasks); + TaskView taskView = getTaskViewFromPool(groupTask.taskViewType); addView(taskView); - if (hasMultipleTasks) { + if (taskView instanceof GroupedTaskView) { boolean firstTaskIsLeftTopTask = groupTask.mSplitBounds.leftTopTaskId == groupTask.task1.key.id; Task leftTopTask = firstTaskIsLeftTopTask ? groupTask.task1 : groupTask.task2; Task rightBottomTask = firstTaskIsLeftTopTask ? groupTask.task2 : groupTask.task1; ((GroupedTaskView) taskView).bind(leftTopTask, rightBottomTask, mOrientationState, groupTask.mSplitBounds); + } else if (taskView instanceof DesktopTaskView) { + ((DesktopTaskView) taskView).bind(((DesktopTask) groupTask).tasks, + mOrientationState); } else { taskView.bind(groupTask.task1, mOrientationState); } } + if (!taskGroups.isEmpty()) { addView(mClearAllButton); } @@ -2123,10 +2134,19 @@ public abstract class RecentsView T getTaskViewFromPool(boolean isGrouped) { - T taskView = isGrouped ? - (T) mGroupedTaskViewPool.getView() : - (T) mTaskViewPool.getView(); + private TaskView getTaskViewFromPool(@TaskView.Type int type) { + TaskView taskView; + switch (type) { + case TaskView.Type.GROUPED: + taskView = mGroupedTaskViewPool.getView(); + break; + case TaskView.Type.DESKTOP: + taskView = mDesktopTaskViewPool.getView(); + break; + case TaskView.Type.SINGLE: + default: + taskView = mTaskViewPool.getView(); + } taskView.setTaskViewId(mTaskViewIdCount); if (mTaskViewIdCount == Integer.MAX_VALUE) { mTaskViewIdCount = 0; @@ -2318,12 +2338,19 @@ public abstract class RecentsView 1; + boolean needDesktopTask = hasDesktopTask(runningTasks); if (shouldAddStubTaskView(runningTasks)) { boolean wasEmpty = getChildCount() == 0; // Add an empty view for now until the task plan is loaded and applied final TaskView taskView; - if (needGroupTaskView) { - taskView = getTaskViewFromPool(true); + if (needDesktopTask) { + taskView = getTaskViewFromPool(TaskView.Type.DESKTOP); + mTmpRunningTasks = Arrays.copyOf(runningTasks, runningTasks.length); + addView(taskView, 0); + ((DesktopTaskView) taskView).bind(Arrays.asList(mTmpRunningTasks), + mOrientationState); + } else if (needGroupTaskView) { + taskView = getTaskViewFromPool(TaskView.Type.GROUPED); mTmpRunningTasks = new Task[]{runningTasks[0], runningTasks[1]}; addView(taskView, 0); // When we create a placeholder task view mSplitBoundsConfig will be null, but with @@ -2332,7 +2359,7 @@ public abstract class RecentsView LauncherAtom.Task.newBuilder() + .setComponentName(component.flattenToShortString()) + .setIndex(screenId)) + .orElse(LauncherAtom.Task.newBuilder())); break; default: break;