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