78b614f555
Copied over icon from caption desktop button in WMShell. Temporary icon for now. Refactored some logic in TaskView to better support for custom orientation handling in subclasses. Subclasses can override setting orientation for icon or thumbnail. Bug: 267326722 Test: manual, enable desktop windowing proto 2 and go to overview Change-Id: Id66d48fa52a418a07b954a384b2c3ea22f091b1f
504 lines
19 KiB
Java
504 lines
19 KiB
Java
/*
|
|
* 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.LauncherState.NORMAL;
|
|
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.Drawable;
|
|
import android.graphics.drawable.LayerDrawable;
|
|
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 android.view.View;
|
|
import android.widget.FrameLayout;
|
|
|
|
import androidx.annotation.NonNull;
|
|
import androidx.annotation.Nullable;
|
|
|
|
import com.android.launcher3.DeviceProfile;
|
|
import com.android.launcher3.Launcher;
|
|
import com.android.launcher3.R;
|
|
import com.android.launcher3.Utilities;
|
|
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 windowing proto 1 is enabled */
|
|
private static final boolean DESKTOP_IS_PROTO1_ENABLED = SystemProperties.getBoolean(
|
|
"persist.wm.debug.desktop_mode", false);
|
|
|
|
/** Flag to indicate whether desktop windowing proto 2 is enabled */
|
|
public static final boolean DESKTOP_IS_PROTO2_ENABLED = SystemProperties.getBoolean(
|
|
"persist.wm.debug.desktop_mode_2", false);
|
|
|
|
/** Flags to indicate whether desktop mode is available on the device */
|
|
public static final boolean DESKTOP_MODE_SUPPORTED =
|
|
DESKTOP_IS_PROTO1_ENABLED || DESKTOP_IS_PROTO2_ENABLED;
|
|
|
|
private static final String TAG = DesktopTaskView.class.getSimpleName();
|
|
|
|
private static final boolean DEBUG = true;
|
|
|
|
@NonNull
|
|
private List<Task> mTasks = new ArrayList<>();
|
|
|
|
private final ArrayList<TaskThumbnailView> mSnapshotViews = new ArrayList<>();
|
|
|
|
/** Maps {@code taskIds} to corresponding {@link TaskThumbnailView}s */
|
|
private final SparseArray<TaskThumbnailView> mSnapshotViewMap = new SparseArray<>();
|
|
|
|
private final ArrayList<CancellableTask<?>> mPendingThumbnailRequests = new ArrayList<>();
|
|
|
|
private View mBackgroundView;
|
|
|
|
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();
|
|
|
|
mBackgroundView = findViewById(R.id.background);
|
|
|
|
int topMarginPx =
|
|
mActivity.getDeviceProfile().overviewTaskThumbnailTopMarginPx;
|
|
FrameLayout.LayoutParams params = (LayoutParams) mBackgroundView.getLayoutParams();
|
|
params.topMargin = topMarginPx;
|
|
mBackgroundView.setLayoutParams(params);
|
|
|
|
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,
|
|
getContext().getTheme()));
|
|
// TODO(b/244348395): this should be wallpaper
|
|
mBackgroundView.setBackground(background);
|
|
|
|
Drawable icon = getResources().getDrawable(R.drawable.ic_desktop, getContext().getTheme());
|
|
Drawable iconBackground = getResources().getDrawable(R.drawable.bg_circle,
|
|
getContext().getTheme());
|
|
mIconView.setDrawable(new LayerDrawable(new Drawable[]{iconBackground, icon}));
|
|
}
|
|
|
|
@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<Task> 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());
|
|
}
|
|
cancelPendingLoadTasks();
|
|
|
|
mTasks = new ArrayList<>(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 the place holder snapshot views. Callers expect this to be non-null
|
|
return mSnapshotView;
|
|
}
|
|
|
|
@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
|
|
protected void setThumbnailOrientation(RecentsOrientedState orientationState) {
|
|
DeviceProfile deviceProfile = mActivity.getDeviceProfile();
|
|
int thumbnailTopMargin = deviceProfile.overviewTaskThumbnailTopMarginPx;
|
|
|
|
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;
|
|
}
|
|
|
|
@Override
|
|
public RunnableList launchTasks() {
|
|
SystemUiProxy.INSTANCE.get(getContext()).showDesktopApps();
|
|
Launcher.getLauncher(mActivity).getStateManager().goToState(NORMAL, false /* animated */);
|
|
return null;
|
|
}
|
|
|
|
@Nullable
|
|
@Override
|
|
public RunnableList launchTaskAnimated() {
|
|
return launchTasks();
|
|
}
|
|
|
|
@Override
|
|
public void launchTask(@NonNull Consumer<Boolean> callback, boolean freezeTaskList) {
|
|
launchTasks();
|
|
callback.accept(true);
|
|
}
|
|
|
|
@Override
|
|
public boolean isDesktopTask() {
|
|
return true;
|
|
}
|
|
|
|
@Override
|
|
void refreshThumbnails(@Nullable HashMap<Integer, ThumbnailData> 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<TaskThumbnailView> 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 thumbnailTopMarginPx = mActivity.getDeviceProfile().overviewTaskThumbnailTopMarginPx;
|
|
containerHeight -= thumbnailTopMarginPx;
|
|
|
|
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);
|
|
// move task down by margin size
|
|
taskY += thumbnailTopMarginPx;
|
|
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;
|
|
if (mFullscreenProgress > 0) {
|
|
// Don't show background while we are transitioning to/from fullscreen
|
|
mBackgroundView.setVisibility(INVISIBLE);
|
|
} else {
|
|
mBackgroundView.setVisibility(VISIBLE);
|
|
}
|
|
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 setIconsAndBannersTransitionProgress(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;
|
|
}
|
|
}
|