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 recentsView.getSplitSelectController().launchTasks(this /*groupedTaskView*/, 227 success -> { 228 endCallback.executeAllAndDestroy(); 229 InteractionJankMonitorWrapper.end( 230 InteractionJankMonitorWrapper.CUJ_SPLIT_SCREEN_ENTER); 231 }, 232 false /* freezeTaskList */); 233 234 // Callbacks get run from recentsView for case when recents animation already running 235 recentsView.addSideTaskLaunchCallback(endCallback); 236 return endCallback; 237 } 238 239 @Override launchTask(@onNull Consumer<Boolean> callback, boolean freezeTaskList)240 public void launchTask(@NonNull Consumer<Boolean> callback, boolean freezeTaskList) { 241 getRecentsView().getSplitSelectController().launchTasks(mTask.key.id, mSecondaryTask.key.id, 242 SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT, callback, freezeTaskList, 243 getSplitRatio()); 244 } 245 246 @Override refreshThumbnails(@ullable HashMap<Integer, ThumbnailData> thumbnailDatas)247 void refreshThumbnails(@Nullable HashMap<Integer, ThumbnailData> thumbnailDatas) { 248 super.refreshThumbnails(thumbnailDatas); 249 if (mSecondaryTask != null && thumbnailDatas != null) { 250 final ThumbnailData thumbnailData = thumbnailDatas.get(mSecondaryTask.key.id); 251 if (thumbnailData != null) { 252 mSnapshotView2.setThumbnail(mSecondaryTask, thumbnailData); 253 return; 254 } 255 } 256 257 mSnapshotView2.refresh(); 258 } 259 260 @Override containsTaskId(int taskId)261 public boolean containsTaskId(int taskId) { 262 return (mTask != null && mTask.key.id == taskId) 263 || (mSecondaryTask != null && mSecondaryTask.key.id == taskId); 264 } 265 266 @Override getThumbnails()267 public TaskThumbnailView[] getThumbnails() { 268 return new TaskThumbnailView[]{mSnapshotView, mSnapshotView2}; 269 } 270 271 @Override getLastSelectedChildTaskIndex()272 protected int getLastSelectedChildTaskIndex() { 273 SplitSelectStateController splitSelectController = 274 getRecentsView().getSplitSelectController(); 275 if (splitSelectController.isDismissingFromSplitPair()) { 276 // return the container index of the task that wasn't initially selected to split with 277 // because that is the only remaining app that can be selected. The coordinate checks 278 // below aren't reliable since both of those views may be gone/transformed 279 int initSplitTaskId = getThisTaskCurrentlyInSplitSelection(); 280 if (initSplitTaskId != INVALID_TASK_ID) { 281 return initSplitTaskId == mTask.key.id ? 1 : 0; 282 } 283 } 284 285 // Check which of the two apps was selected 286 if (isCoordInView(mIconView2, mLastTouchDownPosition) 287 || isCoordInView(mSnapshotView2, mLastTouchDownPosition)) { 288 return 1; 289 } 290 return super.getLastSelectedChildTaskIndex(); 291 } 292 isCoordInView(View v, PointF position)293 private boolean isCoordInView(View v, PointF position) { 294 float[] localPos = new float[]{position.x, position.y}; 295 Utilities.mapCoordInSelfToDescendant(v, this, localPos); 296 return Utilities.pointInView(v, localPos[0], localPos[1], 0f /* slop */); 297 } 298 299 @Override onRecycle()300 public void onRecycle() { 301 super.onRecycle(); 302 mSnapshotView2.setThumbnail(mSecondaryTask, null); 303 mSplitBoundsConfig = null; 304 } 305 306 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)307 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 308 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 309 int widthSize = MeasureSpec.getSize(widthMeasureSpec); 310 int heightSize = MeasureSpec.getSize(heightMeasureSpec); 311 setMeasuredDimension(widthSize, heightSize); 312 if (mSplitBoundsConfig == null || mSnapshotView == null || mSnapshotView2 == null) { 313 return; 314 } 315 int initSplitTaskId = getThisTaskCurrentlyInSplitSelection(); 316 if (initSplitTaskId == INVALID_TASK_ID) { 317 getPagedOrientationHandler().measureGroupedTaskViewThumbnailBounds(mSnapshotView, 318 mSnapshotView2, widthSize, heightSize, mSplitBoundsConfig, 319 mActivity.getDeviceProfile(), getLayoutDirection() == LAYOUT_DIRECTION_RTL); 320 // Should we be having a separate translation step apart from the measuring above? 321 // The following only applies to large screen for now, but for future reference 322 // we'd want to abstract this out in PagedViewHandlers to get the primary/secondary 323 // translation directions 324 mSnapshotView.applySplitSelectTranslateX(mSnapshotView.getTranslationX()); 325 mSnapshotView.applySplitSelectTranslateY(mSnapshotView.getTranslationY()); 326 mSnapshotView2.applySplitSelectTranslateX(mSnapshotView2.getTranslationX()); 327 mSnapshotView2.applySplitSelectTranslateY(mSnapshotView2.getTranslationY()); 328 } else { 329 // Currently being split with this taskView, let the non-split selected thumbnail 330 // take up full thumbnail area 331 TaskIdAttributeContainer container = 332 mTaskIdAttributeContainer[initSplitTaskId == mTask.key.id ? 1 : 0]; 333 container.getThumbnailView().measure(widthMeasureSpec, 334 View.MeasureSpec.makeMeasureSpec( 335 heightSize - 336 mActivity.getDeviceProfile().overviewTaskThumbnailTopMarginPx, 337 MeasureSpec.EXACTLY)); 338 } 339 updateIconPlacement(); 340 } 341 342 @Override setOverlayEnabled(boolean overlayEnabled)343 public void setOverlayEnabled(boolean overlayEnabled) { 344 // Intentional no-op to prevent setting smart actions overlay on thumbnails 345 } 346 347 @Override setOrientationState(RecentsOrientedState orientationState)348 public void setOrientationState(RecentsOrientedState orientationState) { 349 super.setOrientationState(orientationState); 350 DeviceProfile deviceProfile = mActivity.getDeviceProfile(); 351 boolean isGridTask = deviceProfile.isTablet && !isFocusedTask(); 352 int iconDrawableSize = isGridTask ? deviceProfile.overviewTaskIconDrawableSizeGridPx 353 : deviceProfile.overviewTaskIconDrawableSizePx; 354 mIconView2.setDrawableSize(iconDrawableSize, iconDrawableSize); 355 mIconView2.setRotation(getPagedOrientationHandler().getDegreesRotated()); 356 updateIconPlacement(); 357 updateSecondaryDwbPlacement(); 358 } 359 updateIconPlacement()360 private void updateIconPlacement() { 361 if (mSplitBoundsConfig == null) { 362 return; 363 } 364 365 DeviceProfile deviceProfile = mActivity.getDeviceProfile(); 366 int taskIconHeight = deviceProfile.overviewTaskIconSizePx; 367 boolean isRtl = getLayoutDirection() == LAYOUT_DIRECTION_RTL; 368 369 getPagedOrientationHandler().setSplitIconParams(mIconView, mIconView2, 370 taskIconHeight, mSnapshotView.getMeasuredWidth(), mSnapshotView.getMeasuredHeight(), 371 getMeasuredHeight(), getMeasuredWidth(), isRtl, deviceProfile, 372 mSplitBoundsConfig); 373 } 374 updateSecondaryDwbPlacement()375 private void updateSecondaryDwbPlacement() { 376 if (mSecondaryTask == null) { 377 return; 378 } 379 mDigitalWellBeingToast2.initialize(mSecondaryTask); 380 } 381 382 @Override updateSnapshotRadius()383 protected void updateSnapshotRadius() { 384 super.updateSnapshotRadius(); 385 mSnapshotView2.setFullscreenParams(mCurrentFullscreenParams); 386 } 387 388 @Override setIconsAndBannersTransitionProgress(float progress, boolean invert)389 protected void setIconsAndBannersTransitionProgress(float progress, boolean invert) { 390 super.setIconsAndBannersTransitionProgress(progress, invert); 391 // Value set by super call 392 float scale = mIconView.getAlpha(); 393 mIconView2.setAlpha(scale); 394 mDigitalWellBeingToast2.updateBannerOffset(1f - scale); 395 } 396 397 @Override setColorTint(float amount, int tintColor)398 public void setColorTint(float amount, int tintColor) { 399 super.setColorTint(amount, tintColor); 400 mIconView2.setIconColorTint(tintColor, amount); 401 mSnapshotView2.setDimAlpha(amount); 402 mDigitalWellBeingToast2.setBannerColorTint(tintColor, amount); 403 } 404 405 @Override applyThumbnailSplashAlpha()406 protected void applyThumbnailSplashAlpha() { 407 super.applyThumbnailSplashAlpha(); 408 mSnapshotView2.setSplashAlpha(mTaskThumbnailSplashAlpha); 409 } 410 411 @Override refreshTaskThumbnailSplash()412 protected void refreshTaskThumbnailSplash() { 413 super.refreshTaskThumbnailSplash(); 414 mSnapshotView2.refreshSplashView(); 415 } 416 417 @Override resetViewTransforms()418 protected void resetViewTransforms() { 419 super.resetViewTransforms(); 420 mSnapshotView2.resetViewTransforms(); 421 } 422 423 /** 424 * Sets visibility for thumbnails and associated elements (DWB banners). 425 * IconView is unaffected. 426 * 427 * When setting INVISIBLE, sets the visibility for the last selected child task. 428 * When setting VISIBLE (as a reset), sets the visibility for both tasks. 429 */ 430 @Override setThumbnailVisibility(int visibility, int taskId)431 void setThumbnailVisibility(int visibility, int taskId) { 432 if (visibility == VISIBLE) { 433 mSnapshotView.setVisibility(visibility); 434 mDigitalWellBeingToast.setBannerVisibility(visibility); 435 mSnapshotView2.setVisibility(visibility); 436 mDigitalWellBeingToast2.setBannerVisibility(visibility); 437 } else if (taskId == getTaskIds()[0]) { 438 mSnapshotView.setVisibility(visibility); 439 mDigitalWellBeingToast.setBannerVisibility(visibility); 440 } else { 441 mSnapshotView2.setVisibility(visibility); 442 mDigitalWellBeingToast2.setBannerVisibility(visibility); 443 } 444 } 445 } 446