| /* |
| * 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; |
| private View mEmptyView; |
| |
| 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); |
| mEmptyView = findViewById(R.id.empty_view); |
| |
| 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); |
| } |
| |
| mEmptyView.setVisibility(mTasks.isEmpty() ? View.VISIBLE : View.GONE); |
| |
| 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; |
| } |
| } |