1 package com.android.quickstep.views; 2 3 import static android.app.ActivityTaskManager.INVALID_TASK_ID; 4 5 import static com.android.launcher3.util.SplitConfigurationOptions.DEFAULT_SPLIT_RATIO; 6 import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT; 7 8 import android.content.Context; 9 import android.graphics.PointF; 10 import android.graphics.Rect; 11 import android.util.AttributeSet; 12 import android.view.MotionEvent; 13 import android.view.View; 14 15 import androidx.annotation.NonNull; 16 import androidx.annotation.Nullable; 17 18 import com.android.launcher3.DeviceProfile; 19 import com.android.launcher3.R; 20 import com.android.launcher3.Utilities; 21 import com.android.launcher3.util.RunnableList; 22 import com.android.launcher3.util.SplitConfigurationOptions; 23 import com.android.launcher3.util.SplitConfigurationOptions.SplitBounds; 24 import com.android.launcher3.util.TransformingTouchDelegate; 25 import com.android.quickstep.RecentsModel; 26 import com.android.quickstep.TaskIconCache; 27 import com.android.quickstep.TaskThumbnailCache; 28 import com.android.quickstep.util.CancellableTask; 29 import com.android.quickstep.util.RecentsOrientedState; 30 import com.android.quickstep.util.SplitSelectStateController; 31 import com.android.quickstep.util.TaskViewSimulator; 32 import com.android.systemui.shared.recents.model.Task; 33 import com.android.systemui.shared.recents.model.ThumbnailData; 34 import com.android.systemui.shared.recents.utilities.PreviewPositionHelper; 35 import com.android.systemui.shared.system.InteractionJankMonitorWrapper; 36 37 import java.util.HashMap; 38 import java.util.function.Consumer; 39 40 /** 41 * TaskView that contains and shows thumbnails for not one, BUT TWO(!!) tasks 42 * 43 * That's right. If you call within the next 5 minutes we'll go ahead and double your order and 44 * send you !! TWO !! Tasks along with their TaskThumbnailViews complimentary. On. The. House. 45 * And not only that, we'll even clean up your thumbnail request if you don't like it. 46 * All the benefits of one TaskView, except DOUBLED! 47 * 48 * (Icon loading sold separately, fees may apply. Shipping & Handling for Overlays not included). 49 */ 50 public class GroupedTaskView extends TaskView { 51 52 @Nullable 53 private Task mSecondaryTask; 54 private TaskThumbnailView mSnapshotView2; 55 private IconView mIconView2; 56 @Nullable 57 private CancellableTask<ThumbnailData> mThumbnailLoadRequest2; 58 @Nullable 59 private CancellableTask mIconLoadRequest2; 60 private final float[] mIcon2CenterCoords = new float[2]; 61 private TransformingTouchDelegate mIcon2TouchDelegate; 62 @Nullable private SplitBounds mSplitBoundsConfig; 63 private final DigitalWellBeingToast mDigitalWellBeingToast2; 64 GroupedTaskView(Context context)65 public GroupedTaskView(Context context) { 66 this(context, null); 67 } 68 GroupedTaskView(Context context, AttributeSet attrs)69 public GroupedTaskView(Context context, AttributeSet attrs) { 70 this(context, attrs, 0); 71 } 72 GroupedTaskView(Context context, AttributeSet attrs, int defStyleAttr)73 public GroupedTaskView(Context context, AttributeSet attrs, int defStyleAttr) { 74 super(context, attrs, defStyleAttr); 75 mDigitalWellBeingToast2 = new DigitalWellBeingToast(mActivity, this); 76 } 77 78 @Override updateBorderBounds(Rect bounds)79 protected void updateBorderBounds(Rect bounds) { 80 if (mSplitBoundsConfig == null) { 81 super.updateBorderBounds(bounds); 82 return; 83 } 84 bounds.set( 85 Math.min(mSnapshotView.getLeft() + Math.round(mSnapshotView.getTranslationX()), 86 mSnapshotView2.getLeft() + Math.round(mSnapshotView2.getTranslationX())), 87 Math.min(mSnapshotView.getTop() + Math.round(mSnapshotView.getTranslationY()), 88 mSnapshotView2.getTop() + Math.round(mSnapshotView2.getTranslationY())), 89 Math.max(mSnapshotView.getRight() + Math.round(mSnapshotView.getTranslationX()), 90 mSnapshotView2.getRight() + Math.round(mSnapshotView2.getTranslationX())), 91 Math.max(mSnapshotView.getBottom() + Math.round(mSnapshotView.getTranslationY()), 92 mSnapshotView2.getBottom() + Math.round(mSnapshotView2.getTranslationY()))); 93 } 94 95 @Override onFinishInflate()96 protected void onFinishInflate() { 97 super.onFinishInflate(); 98 mSnapshotView2 = findViewById(R.id.bottomright_snapshot); 99 mIconView2 = findViewById(R.id.bottomRight_icon); 100 mIcon2TouchDelegate = new TransformingTouchDelegate(mIconView2); 101 } 102 bind(Task primary, Task secondary, RecentsOrientedState orientedState, @Nullable SplitBounds splitBoundsConfig)103 public void bind(Task primary, Task secondary, RecentsOrientedState orientedState, 104 @Nullable SplitBounds splitBoundsConfig) { 105 super.bind(primary, orientedState); 106 mSecondaryTask = secondary; 107 mTaskIdContainer[1] = secondary.key.id; 108 mTaskIdAttributeContainer[1] = new TaskIdAttributeContainer(secondary, mSnapshotView2, 109 mIconView2, STAGE_POSITION_BOTTOM_OR_RIGHT); 110 mTaskIdAttributeContainer[0].setStagePosition( 111 SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT); 112 mSnapshotView2.bind(secondary); 113 mSplitBoundsConfig = splitBoundsConfig; 114 if (mSplitBoundsConfig == null) { 115 return; 116 } 117 mSnapshotView.getPreviewPositionHelper().setSplitBounds(TaskViewSimulator 118 .convertSplitBounds(splitBoundsConfig), 119 PreviewPositionHelper.STAGE_POSITION_TOP_OR_LEFT); 120 mSnapshotView2.getPreviewPositionHelper().setSplitBounds(TaskViewSimulator 121 .convertSplitBounds(splitBoundsConfig), 122 PreviewPositionHelper.STAGE_POSITION_BOTTOM_OR_RIGHT); 123 } 124 125 /** 126 * Sets up an on-click listener and the visibility for show_windows icon on top of each task. 127 */ 128 @Override setUpShowAllInstancesListener()129 public void setUpShowAllInstancesListener() { 130 // sets up the listener for the left/top task 131 super.setUpShowAllInstancesListener(); 132 133 // right/bottom task's base package name 134 String taskPackageName = mTaskIdAttributeContainer[1].getTask().key.getPackageName(); 135 136 // icon of the right/bottom task 137 View showWindowsView = findViewById(R.id.show_windows_right); 138 updateFilterCallback(showWindowsView, getFilterUpdateCallback(taskPackageName)); 139 } 140 141 @Override onTaskListVisibilityChanged(boolean visible, int changes)142 public void onTaskListVisibilityChanged(boolean visible, int changes) { 143 super.onTaskListVisibilityChanged(visible, changes); 144 if (visible) { 145 RecentsModel model = RecentsModel.INSTANCE.get(getContext()); 146 TaskThumbnailCache thumbnailCache = model.getThumbnailCache(); 147 TaskIconCache iconCache = model.getIconCache(); 148 149 if (needsUpdate(changes, FLAG_UPDATE_THUMBNAIL)) { 150 mThumbnailLoadRequest2 = thumbnailCache.updateThumbnailInBackground(mSecondaryTask, 151 thumbnailData -> mSnapshotView2.setThumbnail( 152 mSecondaryTask, thumbnailData 153 )); 154 } 155 156 if (needsUpdate(changes, FLAG_UPDATE_ICON)) { 157 mIconLoadRequest2 = iconCache.updateIconInBackground(mSecondaryTask, 158 (task) -> { 159 setIcon(mIconView2, task.icon); 160 mDigitalWellBeingToast2.initialize(mSecondaryTask); 161 mDigitalWellBeingToast2.setSplitConfiguration(mSplitBoundsConfig); 162 mDigitalWellBeingToast.setSplitConfiguration(mSplitBoundsConfig); 163 }); 164 } 165 } else { 166 if (needsUpdate(changes, FLAG_UPDATE_THUMBNAIL)) { 167 mSnapshotView2.setThumbnail(null, null); 168 // Reset the task thumbnail reference as well (it will be fetched from the cache or 169 // reloaded next time we need it) 170 mSecondaryTask.thumbnail = null; 171 } 172 if (needsUpdate(changes, FLAG_UPDATE_ICON)) { 173 setIcon(mIconView2, null); 174 } 175 } 176 } 177 updateSplitBoundsConfig(SplitBounds splitBounds)178 public void updateSplitBoundsConfig(SplitBounds splitBounds) { 179 mSplitBoundsConfig = splitBounds; 180 invalidate(); 181 } 182 getSplitRatio()183 public float getSplitRatio() { 184 if (mSplitBoundsConfig != null) { 185 return mSplitBoundsConfig.appsStackedVertically 186 ? mSplitBoundsConfig.topTaskPercent : mSplitBoundsConfig.leftTaskPercent; 187 } 188 return DEFAULT_SPLIT_RATIO; 189 } 190 191 @Override offerTouchToChildren(MotionEvent event)192 public boolean offerTouchToChildren(MotionEvent event) { 193 computeAndSetIconTouchDelegate(mIconView2, mIcon2CenterCoords, mIcon2TouchDelegate); 194 if (mIcon2TouchDelegate.onTouchEvent(event)) { 195 return true; 196 } 197 198 return super.offerTouchToChildren(event); 199 } 200 201 @Override cancelPendingLoadTasks()202 protected void cancelPendingLoadTasks() { 203 super.cancelPendingLoadTasks(); 204 if (mThumbnailLoadRequest2 != null) { 205 mThumbnailLoadRequest2.cancel(); 206 mThumbnailLoadRequest2 = null; 207 } 208 if (mIconLoadRequest2 != null) { 209 mIconLoadRequest2.cancel(); 210 mIconLoadRequest2 = null; 211 } 212 } 213 214 @Nullable 215 @Override launchTaskAnimated()216 public RunnableList launchTaskAnimated() { 217 if (mTask == null || mSecondaryTask == null) { 218 return null; 219 } 220 221 RunnableList endCallback = new RunnableList(); 222 RecentsView recentsView = getRecentsView(); 223 // Callbacks run from remote animation when recents animation not currently running 224 InteractionJankMonitorWrapper.begin(this, 225 InteractionJankMonitorWrapper.CUJ_SPLIT_SCREEN_ENTER, "Enter form GroupedTaskView"); 226 launchTaskInternal(success -> { 227 endCallback.executeAllAndDestroy(); 228 InteractionJankMonitorWrapper.end( 229 InteractionJankMonitorWrapper.CUJ_SPLIT_SCREEN_ENTER); 230 }, false /* freezeTaskList */, true /*launchingExistingTaskview*/); 231 232 233 // Callbacks get run from recentsView for case when recents animation already running 234 recentsView.addSideTaskLaunchCallback(endCallback); 235 return endCallback; 236 } 237 238 @Override launchTask(@onNull Consumer<Boolean> callback, boolean isQuickswitch)239 public void launchTask(@NonNull Consumer<Boolean> callback, boolean isQuickswitch) { 240 launchTaskInternal(callback, isQuickswitch, false /*launchingExistingTaskview*/); 241 } 242 243 /** 244 * @param launchingExistingTaskView {@link SplitSelectStateController#launchExistingSplitPair} 245 * uses existence of GroupedTaskView as control flow of how to animate in the incoming task. If 246 * we're launching from overview (from overview thumbnails) then pass in {@code true}, 247 * otherwise pass in {@code false} for case like quickswitching from home to task 248 */ launchTaskInternal(@onNull Consumer<Boolean> callback, boolean isQuickswitch, boolean launchingExistingTaskView)249 private void launchTaskInternal(@NonNull Consumer<Boolean> callback, boolean isQuickswitch, 250 boolean launchingExistingTaskView) { 251 getRecentsView().getSplitSelectController().launchExistingSplitPair( 252 launchingExistingTaskView ? this : null, mTask.key.id, 253 mSecondaryTask.key.id, SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT, 254 callback, isQuickswitch, getSplitRatio()); 255 } 256 257 @Override refreshThumbnails(@ullable HashMap<Integer, ThumbnailData> thumbnailDatas)258 void refreshThumbnails(@Nullable HashMap<Integer, ThumbnailData> thumbnailDatas) { 259 super.refreshThumbnails(thumbnailDatas); 260 if (mSecondaryTask != null && thumbnailDatas != null) { 261 final ThumbnailData thumbnailData = thumbnailDatas.get(mSecondaryTask.key.id); 262 if (thumbnailData != null) { 263 mSnapshotView2.setThumbnail(mSecondaryTask, thumbnailData); 264 return; 265 } 266 } 267 268 mSnapshotView2.refresh(); 269 } 270 271 @Override containsTaskId(int taskId)272 public boolean containsTaskId(int taskId) { 273 return (mTask != null && mTask.key.id == taskId) 274 || (mSecondaryTask != null && mSecondaryTask.key.id == taskId); 275 } 276 277 @Override getThumbnails()278 public TaskThumbnailView[] getThumbnails() { 279 return new TaskThumbnailView[]{mSnapshotView, mSnapshotView2}; 280 } 281 282 @Override getLastSelectedChildTaskIndex()283 protected int getLastSelectedChildTaskIndex() { 284 SplitSelectStateController splitSelectController = 285 getRecentsView().getSplitSelectController(); 286 if (splitSelectController.isDismissingFromSplitPair()) { 287 // return the container index of the task that wasn't initially selected to split with 288 // because that is the only remaining app that can be selected. The coordinate checks 289 // below aren't reliable since both of those views may be gone/transformed 290 int initSplitTaskId = getThisTaskCurrentlyInSplitSelection(); 291 if (initSplitTaskId != INVALID_TASK_ID) { 292 return initSplitTaskId == mTask.key.id ? 1 : 0; 293 } 294 } 295 296 // Check which of the two apps was selected 297 if (isCoordInView(mIconView2, mLastTouchDownPosition) 298 || isCoordInView(mSnapshotView2, mLastTouchDownPosition)) { 299 return 1; 300 } 301 return super.getLastSelectedChildTaskIndex(); 302 } 303 isCoordInView(View v, PointF position)304 private boolean isCoordInView(View v, PointF position) { 305 float[] localPos = new float[]{position.x, position.y}; 306 Utilities.mapCoordInSelfToDescendant(v, this, localPos); 307 return Utilities.pointInView(v, localPos[0], localPos[1], 0f /* slop */); 308 } 309 310 @Override onRecycle()311 public void onRecycle() { 312 super.onRecycle(); 313 mSnapshotView2.setThumbnail(mSecondaryTask, null); 314 mSplitBoundsConfig = null; 315 } 316 317 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)318 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 319 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 320 int widthSize = MeasureSpec.getSize(widthMeasureSpec); 321 int heightSize = MeasureSpec.getSize(heightMeasureSpec); 322 setMeasuredDimension(widthSize, heightSize); 323 if (mSplitBoundsConfig == null || mSnapshotView == null || mSnapshotView2 == null) { 324 return; 325 } 326 int initSplitTaskId = getThisTaskCurrentlyInSplitSelection(); 327 if (initSplitTaskId == INVALID_TASK_ID) { 328 getPagedOrientationHandler().measureGroupedTaskViewThumbnailBounds(mSnapshotView, 329 mSnapshotView2, widthSize, heightSize, mSplitBoundsConfig, 330 mActivity.getDeviceProfile(), getLayoutDirection() == LAYOUT_DIRECTION_RTL); 331 // Should we be having a separate translation step apart from the measuring above? 332 // The following only applies to large screen for now, but for future reference 333 // we'd want to abstract this out in PagedViewHandlers to get the primary/secondary 334 // translation directions 335 mSnapshotView.applySplitSelectTranslateX(mSnapshotView.getTranslationX()); 336 mSnapshotView.applySplitSelectTranslateY(mSnapshotView.getTranslationY()); 337 mSnapshotView2.applySplitSelectTranslateX(mSnapshotView2.getTranslationX()); 338 mSnapshotView2.applySplitSelectTranslateY(mSnapshotView2.getTranslationY()); 339 } else { 340 // Currently being split with this taskView, let the non-split selected thumbnail 341 // take up full thumbnail area 342 TaskIdAttributeContainer container = 343 mTaskIdAttributeContainer[initSplitTaskId == mTask.key.id ? 1 : 0]; 344 container.getThumbnailView().measure(widthMeasureSpec, 345 View.MeasureSpec.makeMeasureSpec( 346 heightSize - 347 mActivity.getDeviceProfile().overviewTaskThumbnailTopMarginPx, 348 MeasureSpec.EXACTLY)); 349 } 350 updateIconPlacement(); 351 } 352 353 @Override setOverlayEnabled(boolean overlayEnabled)354 public void setOverlayEnabled(boolean overlayEnabled) { 355 // Intentional no-op to prevent setting smart actions overlay on thumbnails 356 } 357 358 @Override setOrientationState(RecentsOrientedState orientationState)359 public void setOrientationState(RecentsOrientedState orientationState) { 360 super.setOrientationState(orientationState); 361 DeviceProfile deviceProfile = mActivity.getDeviceProfile(); 362 boolean isGridTask = deviceProfile.isTablet && !isFocusedTask(); 363 int iconDrawableSize = isGridTask ? deviceProfile.overviewTaskIconDrawableSizeGridPx 364 : deviceProfile.overviewTaskIconDrawableSizePx; 365 mIconView2.setDrawableSize(iconDrawableSize, iconDrawableSize); 366 mIconView2.setRotation(getPagedOrientationHandler().getDegreesRotated()); 367 updateIconPlacement(); 368 updateSecondaryDwbPlacement(); 369 } 370 updateIconPlacement()371 private void updateIconPlacement() { 372 if (mSplitBoundsConfig == null) { 373 return; 374 } 375 376 DeviceProfile deviceProfile = mActivity.getDeviceProfile(); 377 int taskIconHeight = deviceProfile.overviewTaskIconSizePx; 378 boolean isRtl = getLayoutDirection() == LAYOUT_DIRECTION_RTL; 379 380 getPagedOrientationHandler().setSplitIconParams(mIconView, mIconView2, 381 taskIconHeight, mSnapshotView.getMeasuredWidth(), mSnapshotView.getMeasuredHeight(), 382 getMeasuredHeight(), getMeasuredWidth(), isRtl, deviceProfile, 383 mSplitBoundsConfig); 384 } 385 updateSecondaryDwbPlacement()386 private void updateSecondaryDwbPlacement() { 387 if (mSecondaryTask == null) { 388 return; 389 } 390 mDigitalWellBeingToast2.initialize(mSecondaryTask); 391 } 392 393 @Override updateSnapshotRadius()394 protected void updateSnapshotRadius() { 395 super.updateSnapshotRadius(); 396 mSnapshotView2.setFullscreenParams(mCurrentFullscreenParams); 397 } 398 399 @Override setIconsAndBannersTransitionProgress(float progress, boolean invert)400 protected void setIconsAndBannersTransitionProgress(float progress, boolean invert) { 401 super.setIconsAndBannersTransitionProgress(progress, invert); 402 // Value set by super call 403 float scale = mIconView.getAlpha(); 404 mIconView2.setAlpha(scale); 405 mDigitalWellBeingToast2.updateBannerOffset(1f - scale); 406 } 407 408 @Override setColorTint(float amount, int tintColor)409 public void setColorTint(float amount, int tintColor) { 410 super.setColorTint(amount, tintColor); 411 mIconView2.setIconColorTint(tintColor, amount); 412 mSnapshotView2.setDimAlpha(amount); 413 mDigitalWellBeingToast2.setBannerColorTint(tintColor, amount); 414 } 415 416 @Override applyThumbnailSplashAlpha()417 protected void applyThumbnailSplashAlpha() { 418 super.applyThumbnailSplashAlpha(); 419 mSnapshotView2.setSplashAlpha(mTaskThumbnailSplashAlpha); 420 } 421 422 @Override refreshTaskThumbnailSplash()423 protected void refreshTaskThumbnailSplash() { 424 super.refreshTaskThumbnailSplash(); 425 mSnapshotView2.refreshSplashView(); 426 } 427 428 @Override resetViewTransforms()429 protected void resetViewTransforms() { 430 super.resetViewTransforms(); 431 mSnapshotView2.resetViewTransforms(); 432 } 433 434 /** 435 * Sets visibility for thumbnails and associated elements (DWB banners). 436 * IconView is unaffected. 437 * 438 * When setting INVISIBLE, sets the visibility for the last selected child task. 439 * When setting VISIBLE (as a reset), sets the visibility for both tasks. 440 */ 441 @Override setThumbnailVisibility(int visibility, int taskId)442 void setThumbnailVisibility(int visibility, int taskId) { 443 if (visibility == VISIBLE) { 444 mSnapshotView.setVisibility(visibility); 445 mDigitalWellBeingToast.setBannerVisibility(visibility); 446 mSnapshotView2.setVisibility(visibility); 447 mDigitalWellBeingToast2.setBannerVisibility(visibility); 448 } else if (taskId == getTaskIds()[0]) { 449 mSnapshotView.setVisibility(visibility); 450 mDigitalWellBeingToast.setBannerVisibility(visibility); 451 } else { 452 mSnapshotView2.setVisibility(visibility); 453 mDigitalWellBeingToast2.setBannerVisibility(visibility); 454 } 455 } 456 } 457