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