4d01acf44e
Do not show the desktop tile in overview when there are no apps on the desktop. Bug: 270399069 Test: have desktop mode proto 2 enabled, launch an app in fullscreen, open overview, observe no desktop tile Test: move an app to desktop, be on desktop, open overview, observe overview is opened with desktop tile focused Test: have an app on desktop, go home and launch an app in fullscreen, open overview, observe fullscreen app is focused, desktop is peeking in from right Test: have an app on desktop and a fullscreen app in overview, open desktop from overview, close the desktop app, open overview, observe desktop tile is not shown Change-Id: Ia8657d5b260043a630f32b35f2560ea93273d421
511 lines
19 KiB
Java
511 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
|
|
protected void updateBorderBounds(Rect bounds) {
|
|
bounds.set(mBackgroundView.getLeft(), mBackgroundView.getTop(), mBackgroundView.getRight(),
|
|
mBackgroundView.getBottom());
|
|
}
|
|
|
|
@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);
|
|
setVisibility(VISIBLE);
|
|
}
|
|
|
|
@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;
|
|
}
|
|
}
|