diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java index b1bb198dff..1db5253294 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java +++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java @@ -197,6 +197,8 @@ import com.android.systemui.unfold.config.UnfoldTransitionConfig; import com.android.systemui.unfold.progress.RemoteUnfoldTransitionReceiver; import com.android.systemui.unfold.updates.RotationChangeProvider; +import kotlin.Unit; + import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.ArrayList; @@ -586,6 +588,7 @@ public class QuickstepLauncher extends Launcher implements RecentsViewContainer } else { getStateManager().moveToRestState(); } + return Unit.INSTANCE; }); } else { getStateManager().goToState(NORMAL); diff --git a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java index 28ae3d2ff7..def04d3d04 100644 --- a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java +++ b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java @@ -152,6 +152,8 @@ import com.android.window.flags.Flags; import com.android.wm.shell.common.TransactionPool; import com.android.wm.shell.startingsurface.SplashScreenExitAnimationUtils; +import kotlin.Unit; + import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; @@ -2337,6 +2339,7 @@ public abstract class AbsSwipeUpHandler(context) .getOverviewPanel>() .mRecentsViewData, - (parent as TaskView).mTaskViewData + (parent as TaskView).taskViewData ) } diff --git a/quickstep/src/com/android/quickstep/util/BorderAnimator.kt b/quickstep/src/com/android/quickstep/util/BorderAnimator.kt index 44eb070120..85238ed826 100644 --- a/quickstep/src/com/android/quickstep/util/BorderAnimator.kt +++ b/quickstep/src/com/android/quickstep/util/BorderAnimator.kt @@ -86,7 +86,7 @@ private constructor( fun createSimpleBorderAnimator( @Px borderRadiusPx: Int, @Px borderWidthPx: Int, - boundsBuilder: (rect: Rect?) -> Unit, + boundsBuilder: (Rect) -> Unit, targetView: View, @ColorInt borderColor: Int = DEFAULT_BORDER_COLOR, appearanceDurationMs: Long = DEFAULT_APPEARANCE_ANIMATION_DURATION_MS, @@ -250,7 +250,7 @@ private constructor( /** BorderAnimationParams that simply draws the border outside the bounds of the target view. */ private class SimpleParams( @Px borderWidthPx: Int, - boundsBuilder: (rect: Rect?) -> Unit, + boundsBuilder: (Rect) -> Unit, targetView: View, ) : BorderAnimationParams(borderWidthPx, boundsBuilder, targetView) { override val alignmentAdjustmentInset = 0 diff --git a/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java b/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java index 9da4985775..49f4e5f701 100644 --- a/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java +++ b/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java @@ -551,7 +551,7 @@ public class TaskViewSimulator implements TransformParams.BuilderProxy { * TaskView */ public float getCurrentCornerRadius() { - float visibleRadius = mCurrentFullscreenParams.mCurrentDrawnCornerRadius; + float visibleRadius = mCurrentFullscreenParams.getCurrentDrawnCornerRadius(); mTempPoint[0] = visibleRadius; mTempPoint[1] = 0; mInversePositionMatrix.mapVectors(mTempPoint); diff --git a/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt b/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt index 3565174a9b..3935c6707d 100644 --- a/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt +++ b/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt @@ -27,34 +27,30 @@ import android.util.Log import android.view.View import android.view.ViewGroup import androidx.core.view.updateLayoutParams -import com.android.launcher3.LauncherState import com.android.launcher3.R -import com.android.launcher3.Utilities import com.android.launcher3.util.CancellableTask import com.android.launcher3.util.RunnableList import com.android.launcher3.util.SplitConfigurationOptions import com.android.launcher3.util.ViewPool +import com.android.launcher3.util.rects.set import com.android.quickstep.BaseContainerInterface import com.android.quickstep.RecentsModel +import com.android.quickstep.TaskOverlayFactory import com.android.quickstep.util.RecentsOrientedState import com.android.systemui.shared.recents.model.Task import com.android.systemui.shared.recents.model.ThumbnailData -import com.android.systemui.shared.system.QuickStepContract -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. -class DesktopTaskView @JvmOverloads constructor(context: Context?, attrs: AttributeSet? = null) : +class DesktopTaskView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : TaskView(context, attrs) { private val pendingThumbnailRequests = mutableListOf>() private val snapshotDrawParams = object : FullscreenDrawParams(context) { + // DesktopTaskView thumbnail's corner radius is independent of fullscreenProgress. override fun computeTaskCornerRadius(context: Context) = - QuickStepContract.getWindowCornerRadius(context) - - override fun computeWindowCornerRadius(context: Context) = - QuickStepContract.getWindowCornerRadius(context) + computeWindowCornerRadius(context) } private val taskThumbnailViewPool = ViewPool( @@ -69,15 +65,11 @@ class DesktopTaskView @JvmOverloads constructor(context: Context?, attrs: Attrib private lateinit var backgroundView: View private var childCountAtInflation = 0 - init { - mTaskContainers = ArrayList() - } - override fun onFinishInflate() { super.onFinishInflate() backgroundView = findViewById(R.id.background)!! - val topMarginPx = mContainer.deviceProfile.overviewTaskThumbnailTopMarginPx + val topMarginPx = container.deviceProfile.overviewTaskThumbnailTopMarginPx backgroundView.updateLayoutParams { topMargin = topMarginPx } val outerRadii = FloatArray(8) { taskCornerRadius } @@ -88,174 +80,25 @@ class DesktopTaskView @JvmOverloads constructor(context: Context?, attrs: Attrib val iconBackground = resources.getDrawable(R.drawable.bg_circle, context.theme) val icon = resources.getDrawable(R.drawable.ic_desktop, context.theme) - setIcon(mIconView, LayerDrawable(arrayOf(iconBackground, icon))) + setIcon(iconView, LayerDrawable(arrayOf(iconBackground, icon))) childCountAtInflation = childCount } - override fun getThumbnailBounds(bounds: Rect, relativeToDragLayer: Boolean) { - if (relativeToDragLayer) { - mContainer.dragLayer.getDescendantRectRelativeToSelf(backgroundView, bounds) - } else { - bounds.set( - backgroundView.left, - backgroundView.top, - backgroundView.right, - backgroundView.bottom - ) - } - } - - override fun bind(task: Task, orientedState: RecentsOrientedState) { - bind(listOf(task), orientedState) - } - - /** Updates this desktop task to the gives task list defined in `tasks` */ - fun bind(tasks: List, orientedState: RecentsOrientedState) { - if (DEBUG) { - val sb = StringBuilder() - sb.append("bind tasks=").append(tasks.size).append("\n") - tasks.forEach { sb.append(" key=${it.key}\n") } - Log.d(TAG, sb.toString()) - } - cancelPendingLoadTasks() - - (mTaskContainers as ArrayList).ensureCapacity(tasks.size) - tasks.forEachIndexed { index, task -> - val thumbnailView: TaskThumbnailViewDeprecated - if (index >= mTaskContainers.size) { - thumbnailView = taskThumbnailViewPool.view - // Add thumbnailView from to position after the initial child views. - addView( - thumbnailView, - childCountAtInflation, - LayoutParams( - ViewGroup.LayoutParams.WRAP_CONTENT, - ViewGroup.LayoutParams.WRAP_CONTENT - ) - ) - } else { - thumbnailView = mTaskContainers[index].thumbnailView - } - thumbnailView.bind(task) - val taskContainer = - TaskContainer( - task, - thumbnailView, - mIconView, - SplitConfigurationOptions.STAGE_POSITION_UNDEFINED, - null - ) - if (index >= mTaskContainers.size) { - mTaskContainers.add(taskContainer) - } else { - mTaskContainers[index] = taskContainer - } - } - while (mTaskContainers.size > tasks.size) { - mTaskContainers.removeLast().apply { - removeView(thumbnailView) - taskThumbnailViewPool.recycle(thumbnailView) - } - } - - setOrientationState(orientedState) - } - - override fun onTaskListVisibilityChanged(visible: Boolean, changes: Int) { - cancelPendingLoadTasks() - if (needsUpdate(changes, FLAG_UPDATE_THUMBNAIL)) { - mTaskContainers.forEach { - if (visible) { - RecentsModel.INSTANCE.get(context) - .thumbnailCache - .updateThumbnailInBackground(it.task) { thumbnailData: ThumbnailData -> - it.thumbnailView.setThumbnail(it.task, thumbnailData) - } - ?.apply { pendingThumbnailRequests.add(this) } - } else { - it.thumbnailView.setThumbnail(null, null) - // Reset the task thumbnail ref - it.task.thumbnail = null - } - } - } - } - - // thumbnailView is laid out differently and is handled in onMeasure - override fun setThumbnailOrientation(orientationState: RecentsOrientedState) {} - - override fun cancelPendingLoadTasks() { - pendingThumbnailRequests.forEach { it.cancel() } - pendingThumbnailRequests.clear() - } - - override fun launchTaskAnimated(): RunnableList? { - val recentsView = recentsView ?: return null - val endCallback = RunnableList() - val desktopController = recentsView.desktopRecentsController - if (desktopController != null) { - desktopController.launchDesktopFromRecents(this) { endCallback.executeAllAndDestroy() } - Log.d( - TAG, - "launchTaskAnimated - launchDesktopFromRecents: ${taskIds.contentToString()}" - ) - } else { - Log.d( - TAG, - "launchTaskAnimated - recentsController is null: ${taskIds.contentToString()}" - ) - } - - // Callbacks get run from recentsView for case when recents animation already running - recentsView.addSideTaskLaunchCallback(endCallback) - return endCallback - } - - override fun launchTask(callback: Consumer, isQuickswitch: Boolean) { - launchTasks() - callback.accept(true) - } - - public override fun refreshThumbnails(thumbnailDatas: HashMap?) { - // Sets new thumbnails based on the incoming data and refreshes the rest. - thumbnailDatas?.let { - mTaskContainers.forEach { - val thumbnailData = thumbnailDatas[it.task.key.id] - if (thumbnailData != null) { - it.thumbnailView.setThumbnail(it.task, thumbnailData) - } else { - // Refresh the rest that were not updated. - it.thumbnailView.refresh() - } - } - } - } - - override fun onRecycle() { - resetPersistentViewTransforms() - // Clear any references to the thumbnail (it will be re-read either from the cache or the - // system on next bind) - mTaskContainers.forEach { it.thumbnailView.setThumbnail(it.task, null) } - setOverlayEnabled(false) - onTaskListVisibilityChanged(false) - visibility = VISIBLE - } - override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { super.onMeasure(widthMeasureSpec, heightMeasureSpec) val containerWidth = MeasureSpec.getSize(widthMeasureSpec) var containerHeight = MeasureSpec.getSize(heightMeasureSpec) setMeasuredDimension(containerWidth, containerHeight) - if (mTaskContainers.isEmpty()) { + if (taskContainers.isEmpty()) { return } - val thumbnailTopMarginPx = mContainer.deviceProfile.overviewTaskThumbnailTopMarginPx + val thumbnailTopMarginPx = container.deviceProfile.overviewTaskThumbnailTopMarginPx containerHeight -= thumbnailTopMarginPx - BaseContainerInterface.getTaskDimension(mContext, mContainer.deviceProfile, tempPointF) + BaseContainerInterface.getTaskDimension(mContext, container.deviceProfile, tempPointF) val windowWidth = tempPointF.x.toInt() val windowHeight = tempPointF.y.toInt() val scaleWidth = containerWidth / windowWidth.toFloat() @@ -269,7 +112,7 @@ class DesktopTaskView @JvmOverloads constructor(context: Context?, attrs: Attrib } // Desktop tile is a shrunk down version of launcher and freeform task thumbnails. - mTaskContainers.forEach { + taskContainers.forEach { // Default to quarter of the desktop if we did not get app bounds. val taskSize = it.task.appBounds @@ -304,54 +147,191 @@ class DesktopTaskView @JvmOverloads constructor(context: Context?, attrs: Attrib } } - // TODO(b/330685808) support overlay for Screenshot action - override fun setOverlayEnabled(overlayEnabled: Boolean) {} + override fun onRecycle() { + resetPersistentViewTransforms() + // Clear any references to the thumbnail (it will be re-read either from the cache or the + // system on next bind) + taskContainers.forEach { it.thumbnailView.setThumbnail(it.task, null) } + setOverlayEnabled(false) + onTaskListVisibilityChanged(false) + visibility = VISIBLE + } - override fun setFullscreenProgress(progress: Float) { - // TODO(b/249371338): this copies parent implementation and makes it work for N thumbs - val boundProgress = Utilities.boundToRange(progress, 0f, 1f) - mFullscreenProgress = boundProgress - mIconView.setVisibility(if (boundProgress < 1) VISIBLE else INVISIBLE) - // Don't show background while we are transitioning to/from fullscreen - backgroundView.visibility = if (mFullscreenProgress > 0) INVISIBLE else VISIBLE - mTaskContainers.forEach { - it.thumbnailView.taskOverlay.setFullscreenProgress(boundProgress) + override fun bind( + task: Task, + orientedState: RecentsOrientedState, + taskOverlayFactory: TaskOverlayFactory + ) { + bind(listOf(task), orientedState, taskOverlayFactory) + } + + /** Updates this desktop task to the gives task list defined in `tasks` */ + fun bind( + tasks: List, + orientedState: RecentsOrientedState, + taskOverlayFactory: TaskOverlayFactory + ) { + if (DEBUG) { + val sb = StringBuilder() + sb.append("bind tasks=").append(tasks.size).append("\n") + tasks.forEach { sb.append(" key=${it.key}\n") } + Log.d(TAG, sb.toString()) } - // Animate icons and DWB banners in/out, except in QuickSwitch state, when tiles are - // oversized and banner would look disproportionately large. - if ( - mContainer.getOverviewPanel>().getStateManager().state != - LauncherState.BACKGROUND_APP - ) { - setIconsAndBannersTransitionProgress(boundProgress, true) + cancelPendingLoadTasks() + + if (!isTaskContainersInitialized()) { + taskContainers = arrayListOf() } - updateSnapshotRadius() + val taskContainers = taskContainers as ArrayList + taskContainers.ensureCapacity(tasks.size) + tasks.forEachIndexed { index, task -> + val thumbnailView: TaskThumbnailViewDeprecated + if (index >= taskContainers.size) { + thumbnailView = taskThumbnailViewPool.view + // Add thumbnailView from to position after the initial child views. + addView( + thumbnailView, + childCountAtInflation, + LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) + ) + } else { + thumbnailView = taskContainers[index].thumbnailView + } + thumbnailView.bind(task, taskOverlayFactory) + val taskContainer = + TaskContainer( + task, + thumbnailView, + iconView, + SplitConfigurationOptions.STAGE_POSITION_UNDEFINED, + null + ) + if (index >= taskContainers.size) { + taskContainers.add(taskContainer) + } else { + taskContainers[index] = taskContainer + } + } + repeat(taskContainers.size - tasks.size) { + taskContainers.removeLast().apply { + removeView(thumbnailView) + taskThumbnailViewPool.recycle(thumbnailView) + } + } + + setOrientationState(orientedState) } - override fun updateSnapshotRadius() { - super.updateSnapshotRadius() - updateFullscreenParams(snapshotDrawParams) - mTaskContainers.forEach { it.thumbnailView.setFullscreenParams(snapshotDrawParams) } + // thumbnailView is laid out differently and is handled in onMeasure + override fun setThumbnailOrientation(orientationState: RecentsOrientedState) {} + + override fun onTaskListVisibilityChanged(visible: Boolean, changes: Int) { + cancelPendingLoadTasks() + if (needsUpdate(changes, FLAG_UPDATE_THUMBNAIL)) { + taskContainers.forEach { + if (visible) { + RecentsModel.INSTANCE.get(context) + .thumbnailCache + .updateThumbnailInBackground(it.task) { thumbnailData: ThumbnailData -> + it.thumbnailView.setThumbnail(it.task, thumbnailData) + } + ?.apply { pendingThumbnailRequests.add(this) } + } else { + it.thumbnailView.setThumbnail(null, null) + // Reset the task thumbnail ref + it.task.thumbnail = null + } + } + } } - override fun setColorTint(amount: Float, tintColor: Int) { - mTaskContainers.forEach { it.thumbnailView.dimAlpha = amount } + override fun cancelPendingLoadTasks() { + pendingThumbnailRequests.forEach { it.cancel() } + pendingThumbnailRequests.clear() } - override fun applyThumbnailSplashAlpha() { - mTaskContainers.forEach { it.thumbnailView.setSplashAlpha(mTaskThumbnailSplashAlpha) } + override fun getThumbnailBounds(bounds: Rect, relativeToDragLayer: Boolean) { + if (relativeToDragLayer) { + container.dragLayer.getDescendantRectRelativeToSelf(backgroundView, bounds) + } else { + bounds.set(backgroundView) + } } - public override fun setThumbnailVisibility(visibility: Int, taskId: Int) { - mTaskContainers.forEach { it.thumbnailView.visibility = visibility } + override fun launchTaskAnimated(): RunnableList? { + val recentsView = recentsView ?: return null + val endCallback = RunnableList() + val desktopController = recentsView.desktopRecentsController + checkNotNull(desktopController) { "recentsController is null" } + desktopController.launchDesktopFromRecents(this) { endCallback.executeAllAndDestroy() } + Log.d(TAG, "launchTaskAnimated - launchDesktopFromRecents: ${taskIds.contentToString()}") + + // Callbacks get run from recentsView for case when recents animation already running + recentsView.addSideTaskLaunchCallback(endCallback) + return endCallback + } + + override fun launchTask(callback: (launched: Boolean) -> Unit, isQuickSwitch: Boolean) { + launchTasks() + callback(true) + } + + override fun refreshThumbnails(thumbnailDatas: HashMap?) { + // Sets new thumbnails based on the incoming data and refreshes the rest. + thumbnailDatas?.let { + taskContainers.forEach { + val thumbnailData = thumbnailDatas[it.task.key.id] + if (thumbnailData != null) { + it.thumbnailView.setThumbnail(it.task, thumbnailData) + } else { + // Refresh the rest that were not updated. + it.thumbnailView.refresh() + } + } + } } // Desktop tile can't be in split screen override fun confirmSecondSplitSelectApp(): Boolean = false + override fun setColorTint(amount: Float, tintColor: Int) { + taskContainers.forEach { it.thumbnailView.dimAlpha = amount } + } + + override fun setThumbnailVisibility(visibility: Int, taskId: Int) { + taskContainers.forEach { it.thumbnailView.visibility = visibility } + } + + // TODO(b/330685808) support overlay for Screenshot action + override fun setOverlayEnabled(overlayEnabled: Boolean) {} + + override fun onFullscreenProgressChanged(fullscreenProgress: Float) { + // TODO(b/249371338): this copies parent implementation and makes it work for N thumbs + iconView.setVisibility(if (fullscreenProgress < 1) VISIBLE else INVISIBLE) + // Don't show background while we are transitioning to/from fullscreen + backgroundView.visibility = if (fullscreenProgress > 0) INVISIBLE else VISIBLE + taskContainers.forEach { + it.thumbnailView.taskOverlay.setFullscreenProgress(fullscreenProgress) + } + setIconsAndBannersFullscreenProgress(fullscreenProgress) + updateSnapshotRadius() + } + + override fun updateSnapshotRadius() { + updateFullscreenParams(snapshotDrawParams) + taskContainers.forEach { it.thumbnailView.setFullscreenParams(snapshotDrawParams) } + } + + override fun applyThumbnailSplashAlpha() { + taskContainers.forEach { it.thumbnailView.setSplashAlpha(taskThumbnailSplashAlpha) } + } + companion object { private const val TAG = "DesktopTaskView" - private const val DEBUG = true + private const val DEBUG = false private val ORIGIN = Point(0, 0) } } diff --git a/quickstep/src/com/android/quickstep/views/GroupedTaskView.kt b/quickstep/src/com/android/quickstep/views/GroupedTaskView.kt index 93a7200050..f6ae0383a3 100644 --- a/quickstep/src/com/android/quickstep/views/GroupedTaskView.kt +++ b/quickstep/src/com/android/quickstep/views/GroupedTaskView.kt @@ -32,8 +32,12 @@ import com.android.launcher3.config.FeatureFlags import com.android.launcher3.util.CancellableTask import com.android.launcher3.util.RunnableList import com.android.launcher3.util.SplitConfigurationOptions +import com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT +import com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT +import com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_UNDEFINED import com.android.launcher3.util.TransformingTouchDelegate import com.android.quickstep.RecentsModel +import com.android.quickstep.TaskOverlayFactory import com.android.quickstep.util.RecentsOrientedState import com.android.quickstep.util.SplitScreenUtils.Companion.convertLauncherSplitBoundsToShell import com.android.quickstep.util.SplitSelectStateController @@ -42,7 +46,6 @@ import com.android.systemui.shared.recents.model.ThumbnailData import com.android.systemui.shared.recents.utilities.PreviewPositionHelper import com.android.systemui.shared.system.InteractionJankMonitorWrapper import com.android.wm.shell.common.split.SplitScreenConstants.PersistentSnapPosition -import java.util.function.Consumer /** * TaskView that contains and shows thumbnails for not one, BUT TWO(!!) tasks @@ -54,63 +57,31 @@ import java.util.function.Consumer * * (Icon loading sold separately, fees may apply. Shipping & Handling for Overlays not included). */ -class GroupedTaskView @JvmOverloads constructor(context: Context?, attrs: AttributeSet? = null) : +class GroupedTaskView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : TaskView(context, attrs) { // TODO(b/336612373): Support new TTV for GroupedTaskView - private val icon2CenterCoords = FloatArray(2) + private val icon2CenterCoordinates = FloatArray(2) private val digitalWellBeingToast2: DigitalWellBeingToast = - DigitalWellBeingToast(mContainer, this) + DigitalWellBeingToast(container, this) + private lateinit var snapshotView2: TaskThumbnailViewDeprecated private lateinit var iconView2: TaskViewIcon private lateinit var icon2TouchDelegate: TransformingTouchDelegate - private var thumbnailLoadRequest2: CancellableTask? = null - private var iconLoadRequest2: CancellableTask<*>? = null + var splitBoundsConfig: SplitConfigurationOptions.SplitBounds? = null private set - - @get:Deprecated("Use {@link #mTaskContainers} instead.") - private val secondTask: Task - /** Returns the second task bound to this TaskView. */ - get() { - assert(mTaskContainers.size > 1) { "GroupedTaskView is not bound" } - return mTaskContainers[1].task - } + private var thumbnailLoadRequest2: CancellableTask? = null + private var iconLoadRequest2: CancellableTask<*>? = null @get:PersistentSnapPosition val snapPosition: Int /** Returns the [PersistentSnapPosition] of this pair of tasks. */ - get() { - checkNotNull(splitBoundsConfig) { "mSplitBoundsConfig is null" } - return splitBoundsConfig!!.snapPosition - } + get() = splitBoundsConfig?.snapPosition ?: STAGE_POSITION_UNDEFINED - override fun getThumbnailBounds(bounds: Rect, relativeToDragLayer: Boolean) { - splitBoundsConfig ?: return super.getThumbnailBounds(bounds, relativeToDragLayer) - if (relativeToDragLayer) { - val firstThumbnailBounds = Rect() - val secondThumbnailBounds = Rect() - with(mContainer.dragLayer) { - getDescendantRectRelativeToSelf(mTaskThumbnailViewDeprecated, firstThumbnailBounds) - getDescendantRectRelativeToSelf(snapshotView2, secondThumbnailBounds) - } - bounds.set(firstThumbnailBounds) - bounds.union(secondThumbnailBounds) - } else { - bounds.set(getSnapshotViewBounds(mTaskThumbnailViewDeprecated)) - bounds.union(getSnapshotViewBounds(snapshotView2)) - } - } - - private fun getSnapshotViewBounds(snapshotView: View): Rect { - val snapshotViewX = Math.round(snapshotView.x) - val snapshotViewY = Math.round(snapshotView.y) - return Rect( - snapshotViewX, - snapshotViewY, - snapshotViewX + snapshotView.width, - snapshotViewY + snapshotView.height - ) - } + @get:Deprecated("Use {@link #mTaskContainers} instead.") + private val secondTask: Task + /** Returns the second task bound to this TaskView. */ + get() = taskContainers[1].task override fun onFinishInflate() { super.onFinishInflate() @@ -125,31 +96,84 @@ class GroupedTaskView @JvmOverloads constructor(context: Context?, attrs: Attrib icon2TouchDelegate = TransformingTouchDelegate(iconView2.asView()) } + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec) + val widthSize = MeasureSpec.getSize(widthMeasureSpec) + val heightSize = MeasureSpec.getSize(heightMeasureSpec) + setMeasuredDimension(widthSize, heightSize) + val splitBoundsConfig = splitBoundsConfig ?: return + val initSplitTaskId = getThisTaskCurrentlyInSplitSelection() + if (initSplitTaskId == INVALID_TASK_ID) { + pagedOrientationHandler.measureGroupedTaskViewThumbnailBounds( + taskThumbnailViewDeprecated, + snapshotView2, + widthSize, + heightSize, + splitBoundsConfig, + container.deviceProfile, + layoutDirection == LAYOUT_DIRECTION_RTL + ) + // Should we be having a separate translation step apart from the measuring above? + // The following only applies to large screen for now, but for future reference + // we'd want to abstract this out in PagedViewHandlers to get the primary/secondary + // translation directions + taskThumbnailViewDeprecated.applySplitSelectTranslateX( + taskThumbnailViewDeprecated.translationX + ) + taskThumbnailViewDeprecated.applySplitSelectTranslateY( + taskThumbnailViewDeprecated.translationY + ) + snapshotView2.applySplitSelectTranslateX(snapshotView2.translationX) + snapshotView2.applySplitSelectTranslateY(snapshotView2.translationY) + } else { + // Currently being split with this taskView, let the non-split selected thumbnail + // take up full thumbnail area + taskContainers + .firstOrNull { it.task.key.id != initSplitTaskId } + ?.thumbnailView + ?.measure( + widthMeasureSpec, + MeasureSpec.makeMeasureSpec( + heightSize - container.deviceProfile.overviewTaskThumbnailTopMarginPx, + MeasureSpec.EXACTLY + ) + ) + } + if (!enableOverviewIconMenu()) { + updateIconPlacement() + } + } + + override fun onRecycle() { + super.onRecycle() + snapshotView2.setThumbnail(secondTask, null) + splitBoundsConfig = null + } + fun bind( primaryTask: Task, secondaryTask: Task, orientedState: RecentsOrientedState, + taskOverlayFactory: TaskOverlayFactory, splitBoundsConfig: SplitConfigurationOptions.SplitBounds?, ) { cancelPendingLoadTasks() - setupTaskContainers(primaryTask) - mTaskContainers = + setupTaskContainers(primaryTask, taskOverlayFactory) + taskContainers = listOf( - mTaskContainers[0].apply { - stagePosition = SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT - }, + taskContainers[0].apply { stagePosition = STAGE_POSITION_TOP_OR_LEFT }, TaskContainer( secondaryTask, findViewById(R.id.bottomright_snapshot)!!, iconView2, - SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT, + STAGE_POSITION_BOTTOM_OR_RIGHT, digitalWellBeingToast2 ) ) - snapshotView2.bind(secondaryTask) + snapshotView2.bind(secondaryTask, taskOverlayFactory) this.splitBoundsConfig = splitBoundsConfig this.splitBoundsConfig?.let { - mTaskThumbnailViewDeprecated.previewPositionHelper.setSplitBounds( + taskThumbnailViewDeprecated.previewPositionHelper.setSplitBounds( convertLauncherSplitBoundsToShell(it), PreviewPositionHelper.STAGE_POSITION_TOP_OR_LEFT ) @@ -161,27 +185,88 @@ class GroupedTaskView @JvmOverloads constructor(context: Context?, attrs: Attrib setOrientationState(orientedState) } - /** - * Sets up an on-click listener and the visibility for show_windows icon on top of each task. - */ - override fun setUpShowAllInstancesListener() { - // sets up the listener for the left/top task - super.setUpShowAllInstancesListener() - if (mTaskContainers.size < 2) { - return + override fun setOrientationState(orientationState: RecentsOrientedState) { + if (enableOverviewIconMenu()) { + splitBoundsConfig?.let { + val groupedTaskViewSizes = + orientationState.orientationHandler.getGroupedTaskViewSizes( + container.deviceProfile, + it, + layoutParams.width, + layoutParams.height + ) + val iconViewMarginStart = + resources.getDimensionPixelSize( + R.dimen.task_thumbnail_icon_menu_expanded_top_start_margin + ) + val iconViewBackgroundMarginStart = + resources.getDimensionPixelSize( + R.dimen.task_thumbnail_icon_menu_background_margin_top_start + ) + val iconMargins = (iconViewMarginStart + iconViewBackgroundMarginStart) * 2 + // setMaxWidth() needs to be called before mIconView.setIconOrientation which is + // called in the super below. + (iconView as IconAppChipView).setMaxWidth( + groupedTaskViewSizes.first.x - iconMargins + ) + (iconView2 as IconAppChipView).setMaxWidth( + groupedTaskViewSizes.second.x - iconMargins + ) + } } + super.setOrientationState(orientationState) + iconView2.setIconOrientation(orientationState, isGridTask) + updateIconPlacement() + } - // right/bottom task's base package name - val taskPackageName = mTaskContainers[1].task.key.packageName + override fun setThumbnailOrientation(orientationState: RecentsOrientedState) { + super.setThumbnailOrientation(orientationState) + digitalWellBeingToast2.initialize(secondTask) + } - // icon of the right/bottom task - val showWindowsView = findViewById(R.id.show_windows_right)!! - updateFilterCallback(showWindowsView, getFilterUpdateCallback(taskPackageName)) + private fun updateIconPlacement() { + val splitBoundsConfig = splitBoundsConfig ?: return + val taskIconHeight = container.deviceProfile.overviewTaskIconSizePx + val isRtl = layoutDirection == LAYOUT_DIRECTION_RTL + if (enableOverviewIconMenu()) { + val groupedTaskViewSizes = + pagedOrientationHandler.getGroupedTaskViewSizes( + container.deviceProfile, + splitBoundsConfig, + layoutParams.width, + layoutParams.height + ) + pagedOrientationHandler.setSplitIconParams( + iconView.asView(), + iconView2.asView(), + taskIconHeight, + groupedTaskViewSizes.first.x, + groupedTaskViewSizes.first.y, + layoutParams.height, + layoutParams.width, + isRtl, + container.deviceProfile, + splitBoundsConfig + ) + } else { + pagedOrientationHandler.setSplitIconParams( + iconView.asView(), + iconView2.asView(), + taskIconHeight, + taskThumbnailViewDeprecated.measuredWidth, + taskThumbnailViewDeprecated.measuredHeight, + measuredHeight, + measuredWidth, + isRtl, + container.deviceProfile, + splitBoundsConfig + ) + } } override fun onTaskListVisibilityChanged(visible: Boolean, changes: Int) { super.onTaskListVisibilityChanged(visible, changes) - val model = RecentsModel.INSTANCE[context] + val model = RecentsModel.INSTANCE.get(context) if (needsUpdate(changes, FLAG_UPDATE_THUMBNAIL)) { if (visible) { thumbnailLoadRequest2 = @@ -205,7 +290,7 @@ class GroupedTaskView @JvmOverloads constructor(context: Context?, attrs: Attrib } digitalWellBeingToast2.initialize(secondTask) digitalWellBeingToast2.setSplitConfiguration(splitBoundsConfig) - mDigitalWellBeingToast.setSplitConfiguration(splitBoundsConfig) + digitalWellBeingToast.setSplitConfiguration(splitBoundsConfig) } } else { setIcon(iconView2, null) @@ -216,18 +301,6 @@ class GroupedTaskView @JvmOverloads constructor(context: Context?, attrs: Attrib } } - fun updateSplitBoundsConfig(splitBounds: SplitConfigurationOptions.SplitBounds?) { - splitBoundsConfig = splitBounds - invalidate() - } - - override fun offerTouchToChildren(event: MotionEvent): Boolean { - computeAndSetIconTouchDelegate(iconView2, icon2CenterCoords, icon2TouchDelegate) - return if (icon2TouchDelegate.onTouchEvent(event)) { - true - } else super.offerTouchToChildren(event) - } - override fun cancelPendingLoadTasks() { super.cancelPendingLoadTasks() thumbnailLoadRequest2?.cancel() @@ -236,8 +309,61 @@ class GroupedTaskView @JvmOverloads constructor(context: Context?, attrs: Attrib iconLoadRequest2 = null } + override fun getThumbnailBounds(bounds: Rect, relativeToDragLayer: Boolean) { + splitBoundsConfig ?: return super.getThumbnailBounds(bounds, relativeToDragLayer) + if (relativeToDragLayer) { + val firstThumbnailBounds = Rect() + val secondThumbnailBounds = Rect() + with(container.dragLayer) { + getDescendantRectRelativeToSelf(snapshotView, firstThumbnailBounds) + getDescendantRectRelativeToSelf(snapshotView2, secondThumbnailBounds) + } + bounds.set(firstThumbnailBounds) + bounds.union(secondThumbnailBounds) + } else { + bounds.set(getSnapshotViewBounds(snapshotView)) + bounds.union(getSnapshotViewBounds(snapshotView2)) + } + } + + private fun getSnapshotViewBounds(snapshotView: View): Rect { + val snapshotViewX = Math.round(snapshotView.x) + val snapshotViewY = Math.round(snapshotView.y) + return Rect( + snapshotViewX, + snapshotViewY, + snapshotViewX + snapshotView.width, + snapshotViewY + snapshotView.height + ) + } + + /** + * Sets up an on-click listener and the visibility for show_windows icon on top of each task. + */ + override fun setUpShowAllInstancesListener() { + // sets up the listener for the left/top task + super.setUpShowAllInstancesListener() + // right/bottom task's base package name + val taskPackageName = taskContainers[1].task.key.packageName + // icon of the right/bottom task + val showWindowsView = findViewById(R.id.show_windows_right)!! + updateFilterCallback(showWindowsView, getFilterUpdateCallback(taskPackageName)) + } + + fun updateSplitBoundsConfig(splitBounds: SplitConfigurationOptions.SplitBounds?) { + splitBoundsConfig = splitBounds + invalidate() + } + + override fun offerTouchToChildren(event: MotionEvent): Boolean { + computeAndSetIconTouchDelegate(iconView2, icon2CenterCoordinates, icon2TouchDelegate) + return if (icon2TouchDelegate.onTouchEvent(event)) { + true + } else super.offerTouchToChildren(event) + } + override fun launchTaskAnimated(): RunnableList? { - if (mTaskContainers.isEmpty()) { + if (taskContainers.isEmpty()) { Log.d(TAG, "launchTaskAnimated - task is not bound") return null } @@ -259,8 +385,8 @@ class GroupedTaskView @JvmOverloads constructor(context: Context?, attrs: Attrib return endCallback } - override fun launchTask(callback: Consumer, isQuickswitch: Boolean) { - launchTaskInternal(isQuickswitch, false, callback /*launchingExistingTaskview*/) + override fun launchTask(callback: (launched: Boolean) -> Unit, isQuickSwitch: Boolean) { + launchTaskInternal(isQuickSwitch, false, callback /*launchingExistingTaskview*/) } /** @@ -272,15 +398,14 @@ class GroupedTaskView @JvmOverloads constructor(context: Context?, attrs: Attrib private fun launchTaskInternal( isQuickSwitch: Boolean, launchingExistingTaskView: Boolean, - callback: Consumer + callback: (launched: Boolean) -> Unit ) { - check(mTaskContainers.size >= 2) { "task not bound" } recentsView?.let { it.splitSelectController.launchExistingSplitPair( if (launchingExistingTaskView) this else null, - mTaskContainers[0].task.key.id, - mTaskContainers[1].task.key.id, - SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT, + taskContainers[0].task.key.id, + taskContainers[1].task.key.id, + STAGE_POSITION_TOP_OR_LEFT, callback, isQuickSwitch, snapPosition @@ -289,7 +414,7 @@ class GroupedTaskView @JvmOverloads constructor(context: Context?, attrs: Attrib } } - public override fun refreshThumbnails(thumbnailDatas: HashMap?) { + override fun refreshThumbnails(thumbnailDatas: HashMap?) { super.refreshThumbnails(thumbnailDatas) thumbnailDatas?.get(secondTask.key.id)?.let { snapshotView2.setThumbnail(secondTask, it) } ?: { snapshotView2.refresh() } @@ -312,7 +437,7 @@ class GroupedTaskView @JvmOverloads constructor(context: Context?, attrs: Attrib // checks below aren't reliable since both of those views may be gone/transformed val initSplitTaskId = getThisTaskCurrentlyInSplitSelection() if (initSplitTaskId != INVALID_TASK_ID) { - return if (initSplitTaskId == firstTask!!.key.id) 1 else 0 + return if (initSplitTaskId == firstTask.key.id) 1 else 0 } } @@ -332,57 +457,33 @@ class GroupedTaskView @JvmOverloads constructor(context: Context?, attrs: Attrib return Utilities.pointInView(this, localPos[0], localPos[1], 0f /* slop */) } - override fun onRecycle() { - super.onRecycle() - snapshotView2.setThumbnail(secondTask, null) - splitBoundsConfig = null + override fun setIconsAndBannersTransitionProgress(progress: Float, invert: Boolean) { + super.setIconsAndBannersTransitionProgress(progress, invert) + getIconContentScale(invert).let { + iconView2.setContentAlpha(it) + digitalWellBeingToast2.updateBannerOffset(1f - it) + } } - override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { - super.onMeasure(widthMeasureSpec, heightMeasureSpec) - val widthSize = MeasureSpec.getSize(widthMeasureSpec) - val heightSize = MeasureSpec.getSize(heightMeasureSpec) - setMeasuredDimension(widthSize, heightSize) - val splitBoundsConfig = splitBoundsConfig ?: return - val initSplitTaskId = getThisTaskCurrentlyInSplitSelection() - if (initSplitTaskId == INVALID_TASK_ID) { - pagedOrientationHandler.measureGroupedTaskViewThumbnailBounds( - mTaskThumbnailViewDeprecated, - snapshotView2, - widthSize, - heightSize, - splitBoundsConfig, - mContainer.deviceProfile, - layoutDirection == LAYOUT_DIRECTION_RTL - ) - // Should we be having a separate translation step apart from the measuring above? - // The following only applies to large screen for now, but for future reference - // we'd want to abstract this out in PagedViewHandlers to get the primary/secondary - // translation directions - mTaskThumbnailViewDeprecated.applySplitSelectTranslateX( - mTaskThumbnailViewDeprecated.translationX - ) - mTaskThumbnailViewDeprecated.applySplitSelectTranslateY( - mTaskThumbnailViewDeprecated.translationY - ) - snapshotView2.applySplitSelectTranslateX(snapshotView2.translationX) - snapshotView2.applySplitSelectTranslateY(snapshotView2.translationY) - } else { - // Currently being split with this taskView, let the non-split selected thumbnail - // take up full thumbnail area - mTaskContainers - .firstOrNull { it.task.key.id != initSplitTaskId } - ?.thumbnailView - ?.measure( - widthMeasureSpec, - MeasureSpec.makeMeasureSpec( - heightSize - mContainer.deviceProfile.overviewTaskThumbnailTopMarginPx, - MeasureSpec.EXACTLY - ) - ) - } - if (!enableOverviewIconMenu()) { - updateIconPlacement() + override fun setColorTint(amount: Float, tintColor: Int) { + super.setColorTint(amount, tintColor) + iconView2.setIconColorTint(tintColor, amount) + snapshotView2.dimAlpha = amount + digitalWellBeingToast2.setBannerColorTint(tintColor, amount) + } + + /** + * Sets visibility for thumbnails and associated elements (DWB banners). IconView is unaffected. + * + * When setting INVISIBLE, sets the visibility for the last selected child task. When setting + * VISIBLE (as a reset), sets the visibility for both tasks. + */ + override fun setThumbnailVisibility(visibility: Int, taskId: Int) { + taskContainers.forEach { + if (visibility == VISIBLE || it.task.key.id == taskId) { + it.thumbnailView.visibility = visibility + it.digitalWellBeingToast?.setBannerVisibility(visibility) + } } } @@ -394,113 +495,19 @@ class GroupedTaskView @JvmOverloads constructor(context: Context?, attrs: Attrib } } - override fun setOrientationState(orientationState: RecentsOrientedState) { - if (enableOverviewIconMenu()) { - splitBoundsConfig?.let { - val groupedTaskViewSizes = - orientationState.orientationHandler.getGroupedTaskViewSizes( - mContainer.deviceProfile, - it, - layoutParams.width, - layoutParams.height - ) - val iconViewMarginStart = - resources.getDimensionPixelSize( - R.dimen.task_thumbnail_icon_menu_expanded_top_start_margin - ) - val iconViewBackgroundMarginStart = - resources.getDimensionPixelSize( - R.dimen.task_thumbnail_icon_menu_background_margin_top_start - ) - val iconMargins = (iconViewMarginStart + iconViewBackgroundMarginStart) * 2 - // setMaxWidth() needs to be called before mIconView.setIconOrientation which is - // called in the super below. - (mIconView as IconAppChipView).setMaxWidth( - groupedTaskViewSizes.first.x - iconMargins - ) - (iconView2 as IconAppChipView).setMaxWidth( - groupedTaskViewSizes.second.x - iconMargins - ) - } - } - super.setOrientationState(orientationState) - iconView2.setIconOrientation(orientationState, isGridTask()) - updateIconPlacement() - } - - override fun setThumbnailOrientation(orientationState: RecentsOrientedState?) { - super.setThumbnailOrientation(orientationState) - digitalWellBeingToast2.initialize(secondTask) - } - - private fun updateIconPlacement() { - val splitBoundsConfig = splitBoundsConfig ?: return - val taskIconHeight = mContainer.deviceProfile.overviewTaskIconSizePx - val isRtl = layoutDirection == LAYOUT_DIRECTION_RTL - if (enableOverviewIconMenu()) { - val groupedTaskViewSizes = - pagedOrientationHandler.getGroupedTaskViewSizes( - mContainer.deviceProfile, - splitBoundsConfig, - layoutParams.width, - layoutParams.height - ) - pagedOrientationHandler.setSplitIconParams( - mIconView.asView(), - iconView2.asView(), - taskIconHeight, - groupedTaskViewSizes.first.x, - groupedTaskViewSizes.first.y, - layoutParams.height, - layoutParams.width, - isRtl, - mContainer.deviceProfile, - splitBoundsConfig - ) - } else { - pagedOrientationHandler.setSplitIconParams( - mIconView.asView(), - iconView2.asView(), - taskIconHeight, - mTaskThumbnailViewDeprecated.measuredWidth, - mTaskThumbnailViewDeprecated.measuredHeight, - measuredHeight, - measuredWidth, - isRtl, - mContainer.deviceProfile, - splitBoundsConfig - ) - } + override fun refreshTaskThumbnailSplash() { + super.refreshTaskThumbnailSplash() + snapshotView2.refreshSplashView() } override fun updateSnapshotRadius() { super.updateSnapshotRadius() - snapshotView2.setFullscreenParams(mCurrentFullscreenParams) - } - - override fun setIconsAndBannersTransitionProgress(progress: Float, invert: Boolean) { - super.setIconsAndBannersTransitionProgress(progress, invert) - // Value set by super call - val scale = mIconView.alpha - iconView2.setContentAlpha(scale) - digitalWellBeingToast2.updateBannerOffset(1f - scale) - } - - override fun setColorTint(amount: Float, tintColor: Int) { - super.setColorTint(amount, tintColor) - iconView2.setIconColorTint(tintColor, amount) - snapshotView2.dimAlpha = amount - digitalWellBeingToast2.setBannerColorTint(tintColor, amount) + snapshotView2.setFullscreenParams(currentFullscreenParams) } override fun applyThumbnailSplashAlpha() { super.applyThumbnailSplashAlpha() - snapshotView2.setSplashAlpha(mTaskThumbnailSplashAlpha) - } - - override fun refreshTaskThumbnailSplash() { - super.refreshTaskThumbnailSplash() - snapshotView2.refreshSplashView() + snapshotView2.setSplashAlpha(taskThumbnailSplashAlpha) } override fun resetViewTransforms() { @@ -508,21 +515,6 @@ class GroupedTaskView @JvmOverloads constructor(context: Context?, attrs: Attrib snapshotView2.resetViewTransforms() } - /** - * Sets visibility for thumbnails and associated elements (DWB banners). IconView is unaffected. - * - * When setting INVISIBLE, sets the visibility for the last selected child task. When setting - * VISIBLE (as a reset), sets the visibility for both tasks. - */ - public override fun setThumbnailVisibility(visibility: Int, taskId: Int) { - mTaskContainers.forEach { - if (visibility == VISIBLE || it.task.key.id == taskId) { - it.thumbnailView.visibility = visibility - it.digitalWellBeingToast.setBannerVisibility(visibility) - } - } - } - companion object { private const val TAG = "GroupedTaskView" } diff --git a/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java b/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java index 8d1907ff7d..fd95da1dee 100644 --- a/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java +++ b/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java @@ -56,6 +56,8 @@ import com.android.quickstep.SystemUiProxy; import com.android.quickstep.util.SplitSelectStateController; import com.android.systemui.shared.recents.model.Task; +import kotlin.Unit; + /** * {@link RecentsView} used in Launcher activity */ @@ -107,7 +109,7 @@ public class LauncherRecentsView extends RecentsView= 0; i--) { GroupTask groupTask = taskGroups.get(i); - boolean isRemovalNeeded = stagedTaskIdToBeRemovedFromGrid != INVALID_TASK_ID - && groupTask.containsTask(stagedTaskIdToBeRemovedFromGrid); + boolean isRemovalNeeded = stagedTaskIdToBeRemoved != INVALID_TASK_ID + && groupTask.containsTask(stagedTaskIdToBeRemoved); - TaskView taskView; - if (isRemovalNeeded && groupTask.hasMultipleTasks()) { - // If we need to remove half of a pair of tasks, force a TaskView with Type.SINGLE - // to be a temporary container for the remaining task. - taskView = getTaskViewFromPool(TaskView.Type.SINGLE); - } else { - taskView = getTaskViewFromPool(groupTask.taskViewType); + if (isRemovalNeeded && !groupTask.hasMultipleTasks()) { + // If the task we need to remove is not part of a pair, avoiding creating the + // TaskView. + continue; } - addView(taskView); - - if (isRemovalNeeded && groupTask.hasMultipleTasks()) { - if (groupTask.task1.key.id == stagedTaskIdToBeRemovedFromGrid) { - taskView.bind(groupTask.task2, mOrientationState); - } else { - taskView.bind(groupTask.task1, mOrientationState); - } - } else if (isRemovalNeeded) { - // If the task we need to remove is not part of a pair, bind it to the TaskView - // first (to prevent problems), then remove the whole thing. - taskView.bind(groupTask.task1, mOrientationState); - removeView(taskView); - } else if (taskView instanceof GroupedTaskView) { + // If we need to remove half of a pair of tasks, force a TaskView with Type.SINGLE + // to be a temporary container for the remaining task. + TaskView taskView = getTaskViewFromPool( + isRemovalNeeded ? TaskView.Type.SINGLE : groupTask.taskViewType); + if (taskView instanceof GroupedTaskView) { boolean firstTaskIsLeftTopTask = groupTask.mSplitBounds.leftTopTaskId == groupTask.task1.key.id; Task leftTopTask = firstTaskIsLeftTopTask ? groupTask.task1 : groupTask.task2; Task rightBottomTask = firstTaskIsLeftTopTask ? groupTask.task2 : groupTask.task1; - ((GroupedTaskView) taskView).bind(leftTopTask, rightBottomTask, mOrientationState, - groupTask.mSplitBounds); + mTaskOverlayFactory, groupTask.mSplitBounds); } else if (taskView instanceof DesktopTaskView) { ((DesktopTaskView) taskView).bind(((DesktopTask) groupTask).tasks, - mOrientationState); + mOrientationState, mTaskOverlayFactory); mDesktopTaskView = (DesktopTaskView) taskView; } else { - taskView.bind(groupTask.task1, mOrientationState); + Task task = groupTask.task1.key.id == stagedTaskIdToBeRemoved ? groupTask.task2 + : groupTask.task1; + taskView.bind(task, mOrientationState, mTaskOverlayFactory); } + addView(taskView); // enables instance filtering if the feature flag for it is on if (FeatureFlags.ENABLE_MULTI_INSTANCE.get()) { @@ -2175,7 +2166,8 @@ public abstract class RecentsView removeTaskInternal(dismissedTaskViewId)); - } else { - removeTaskInternal(dismissedTaskViewId); - } - announceForAccessibility( - getResources().getString(R.string.task_view_closed)); - mContainer.getStatsLogManager().logger() - .withItemInfo(dismissedTaskView.getItemInfo()) - .log(LAUNCHER_TASK_DISMISS_SWIPE_UP); + if (dismissedTaskView.isRunningTask()) { + finishRecentsAnimation(true /* toRecents */, false /* shouldPip */, + () -> removeTaskInternal(dismissedTaskViewId)); + } else { + removeTaskInternal(dismissedTaskViewId); } + announceForAccessibility( + getResources().getString(R.string.task_view_closed)); + mContainer.getStatsLogManager().logger() + .withItemInfo(dismissedTaskView.getItemInfo()) + .log(LAUNCHER_TASK_DISMISS_SWIPE_UP); } int pageToSnapTo = mCurrentPage; @@ -5280,11 +5268,8 @@ public abstract class RecentsView : ArrowPopup where T : RecentsViewContainer, T companion object { const val TAG = "TaskMenuViewWithArrow" - fun showForTask( - taskContainer: TaskContainer, - alignedOptionIndex: Int = 0 - ): Boolean where T : RecentsViewContainer, T : Context { + fun showForTask(taskContainer: TaskContainer, alignedOptionIndex: Int = 0): Boolean { val container: RecentsViewContainer = RecentsViewContainer.containerFromContext(taskContainer.taskView.context) val taskMenuViewWithArrow = @@ -54,7 +51,7 @@ class TaskMenuViewWithArrow : ArrowPopup where T : RecentsViewContainer, T R.layout.task_menu_with_arrow, container.dragLayer, false - ) as TaskMenuViewWithArrow + ) as TaskMenuViewWithArrow<*> return taskMenuViewWithArrow.populateAndShowForTask(taskContainer, alignedOptionIndex) } diff --git a/quickstep/src/com/android/quickstep/views/TaskThumbnailViewDeprecated.java b/quickstep/src/com/android/quickstep/views/TaskThumbnailViewDeprecated.java index 21c6ca8e06..f7b64969d7 100644 --- a/quickstep/src/com/android/quickstep/views/TaskThumbnailViewDeprecated.java +++ b/quickstep/src/com/android/quickstep/views/TaskThumbnailViewDeprecated.java @@ -54,6 +54,7 @@ import com.android.launcher3.util.MainThreadInitializedObject; import com.android.launcher3.util.SystemUiController; import com.android.launcher3.util.SystemUiController.SystemUiControllerFlags; import com.android.launcher3.util.ViewPool; +import com.android.quickstep.TaskOverlayFactory; import com.android.quickstep.TaskOverlayFactory.TaskOverlay; import com.android.quickstep.orientation.RecentsPagedOrientationHandler; import com.android.quickstep.views.TaskView.FullscreenDrawParams; @@ -128,6 +129,7 @@ public class TaskThumbnailViewDeprecated extends View implements ViewPool.Reusab }; private final RecentsViewContainer mContainer; + private TaskOverlayFactory mTaskOverlayFactory; @Nullable private TaskOverlay mOverlay; private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); @@ -188,7 +190,8 @@ public class TaskThumbnailViewDeprecated extends View implements ViewPool.Reusab /** * Updates the thumbnail to draw the provided task */ - public void bind(Task task) { + public void bind(Task task, TaskOverlayFactory taskOverlayFactory) { + mTaskOverlayFactory = taskOverlayFactory; getTaskOverlay().reset(); mTask = task; int color = task == null ? Color.BLACK : task.colorBackground | 0xFF000000; @@ -281,7 +284,7 @@ public class TaskThumbnailViewDeprecated extends View implements ViewPool.Reusab public TaskOverlay getTaskOverlay() { if (mOverlay == null) { - mOverlay = getTaskView().getRecentsView().getTaskOverlayFactory().createOverlay(this); + mOverlay = mTaskOverlayFactory.createOverlay(this); } return mOverlay; } @@ -348,7 +351,7 @@ public class TaskThumbnailViewDeprecated extends View implements ViewPool.Reusab canvas.save(); // Draw the insets if we're being drawn fullscreen (we do this for quick switch). drawOnCanvas(canvas, 0, 0, getMeasuredWidth(), getMeasuredHeight(), - mFullscreenParams.mCurrentDrawnCornerRadius); + mFullscreenParams.getCurrentDrawnCornerRadius()); canvas.restore(); } @@ -364,7 +367,8 @@ public class TaskThumbnailViewDeprecated extends View implements ViewPool.Reusab public void drawOnCanvas(Canvas canvas, float x, float y, float width, float height, float cornerRadius) { - if (mTask != null && getTaskView().isRunningTask() && !getTaskView().showScreenshot()) { + if (mTask != null && getTaskView().isRunningTask() + && !getTaskView().getShouldShowScreenshot()) { canvas.drawRoundRect(x, y, width, height, cornerRadius, cornerRadius, mClearPaint); canvas.drawRoundRect(x, y, width, height, cornerRadius, cornerRadius, mDimmingPaintAfterClearing); @@ -561,8 +565,7 @@ public class TaskThumbnailViewDeprecated extends View implements ViewPool.Reusab if (mBitmapShader != null && mThumbnailData != null) { mPreviewRect.set(0, 0, mThumbnailData.thumbnail.getWidth(), mThumbnailData.thumbnail.getHeight()); - int currentRotation = getTaskView().getRecentsView().getPagedViewOrientedState() - .getRecentsActivityRotation(); + int currentRotation = getTaskView().getOrientedState().getRecentsActivityRotation(); boolean isRtl = getLayoutDirection() == LAYOUT_DIRECTION_RTL; mPreviewPositionHelper.updateThumbnailMatrix(mPreviewRect, mThumbnailData, getMeasuredWidth(), getMeasuredHeight(), dp.isTablet, currentRotation, isRtl); diff --git a/quickstep/src/com/android/quickstep/views/TaskView.java b/quickstep/src/com/android/quickstep/views/TaskView.java deleted file mode 100644 index cc24bc33bd..0000000000 --- a/quickstep/src/com/android/quickstep/views/TaskView.java +++ /dev/null @@ -1,2060 +0,0 @@ -/* - * Copyright (C) 2017 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.Display.DEFAULT_DISPLAY; -import static android.widget.Toast.LENGTH_SHORT; - -import static com.android.app.animation.Interpolators.FAST_OUT_SLOW_IN; -import static com.android.app.animation.Interpolators.LINEAR; -import static com.android.launcher3.Flags.enableCursorHoverStates; -import static com.android.launcher3.Flags.enableGridOnlyOverview; -import static com.android.launcher3.Flags.enableOverviewIconMenu; -import static com.android.launcher3.Flags.enableRefactorTaskThumbnail; -import static com.android.launcher3.LauncherState.BACKGROUND_APP; -import static com.android.launcher3.Utilities.getDescendantCoordRelativeToAncestor; -import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASK_ICON_TAP_OR_LONGPRESS; -import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASK_LAUNCH_TAP; -import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_NOT_PINNABLE; -import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; -import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR; -import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT; -import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_UNDEFINED; -import static com.android.launcher3.util.SplitConfigurationOptions.getLogEventForPosition; -import static com.android.quickstep.TaskOverlayFactory.getEnabledShortcuts; -import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.EXPECTING_TASK_APPEARED; -import static com.android.quickstep.util.BorderAnimator.DEFAULT_BORDER_COLOR; - -import static java.lang.annotation.RetentionPolicy.SOURCE; - -import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; -import android.animation.AnimatorSet; -import android.animation.ObjectAnimator; -import android.annotation.IdRes; -import android.app.ActivityOptions; -import android.content.Context; -import android.content.Intent; -import android.content.res.TypedArray; -import android.graphics.Canvas; -import android.graphics.PointF; -import android.graphics.Rect; -import android.graphics.drawable.Drawable; -import android.os.Bundle; -import android.util.AttributeSet; -import android.util.FloatProperty; -import android.util.Log; -import android.view.Display; -import android.view.MotionEvent; -import android.view.RemoteAnimationTarget; -import android.view.TouchDelegate; -import android.view.View; -import android.view.ViewGroup; -import android.view.ViewStub; -import android.view.accessibility.AccessibilityNodeInfo; -import android.widget.FrameLayout; -import android.widget.Toast; - -import androidx.annotation.IntDef; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.VisibleForTesting; - -import com.android.app.animation.Interpolators; -import com.android.launcher3.DeviceProfile; -import com.android.launcher3.Flags; -import com.android.launcher3.LauncherSettings; -import com.android.launcher3.R; -import com.android.launcher3.Utilities; -import com.android.launcher3.config.FeatureFlags; -import com.android.launcher3.model.data.WorkspaceItemInfo; -import com.android.launcher3.pm.UserCache; -import com.android.launcher3.popup.SystemShortcut; -import com.android.launcher3.testing.TestLogging; -import com.android.launcher3.testing.shared.TestProtocol; -import com.android.launcher3.util.ActivityOptionsWrapper; -import com.android.launcher3.util.CancellableTask; -import com.android.launcher3.util.ComponentKey; -import com.android.launcher3.util.RunnableList; -import com.android.launcher3.util.SafeCloseable; -import com.android.launcher3.util.SplitConfigurationOptions; -import com.android.launcher3.util.SplitConfigurationOptions.SplitPositionOption; -import com.android.launcher3.util.TraceHelper; -import com.android.launcher3.util.TransformingTouchDelegate; -import com.android.launcher3.util.ViewPool.Reusable; -import com.android.quickstep.RecentsModel; -import com.android.quickstep.RemoteAnimationTargets; -import com.android.quickstep.RemoteTargetGluer.RemoteTargetHandle; -import com.android.quickstep.TaskAnimationManager; -import com.android.quickstep.TaskIconCache; -import com.android.quickstep.TaskThumbnailCache; -import com.android.quickstep.TaskUtils; -import com.android.quickstep.TaskViewUtils; -import com.android.quickstep.orientation.RecentsPagedOrientationHandler; -import com.android.quickstep.task.thumbnail.TaskThumbnail; -import com.android.quickstep.task.thumbnail.TaskThumbnailView; -import com.android.quickstep.task.viewmodel.TaskViewData; -import com.android.quickstep.util.ActiveGestureLog; -import com.android.quickstep.util.BorderAnimator; -import com.android.quickstep.util.RecentsOrientedState; -import com.android.quickstep.util.SplitSelectStateController; -import com.android.quickstep.util.TaskCornerRadius; -import com.android.quickstep.util.TaskRemovedDuringLaunchListener; -import com.android.systemui.shared.recents.model.Task; -import com.android.systemui.shared.recents.model.ThumbnailData; -import com.android.systemui.shared.system.ActivityManagerWrapper; -import com.android.systemui.shared.system.QuickStepContract; - -import kotlin.Unit; - -import java.lang.annotation.Retention; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Optional; -import java.util.function.Consumer; -import java.util.stream.Stream; - - -/** - * A task in the Recents view. - */ -public class TaskView extends FrameLayout implements Reusable { - - private static final String TAG = "TaskView"; - public static final int FLAG_UPDATE_ICON = 1; - public static final int FLAG_UPDATE_THUMBNAIL = FLAG_UPDATE_ICON << 1; - public static final int FLAG_UPDATE_CORNER_RADIUS = FLAG_UPDATE_THUMBNAIL << 1; - - public static final int FLAG_UPDATE_ALL = FLAG_UPDATE_ICON | FLAG_UPDATE_THUMBNAIL - | FLAG_UPDATE_CORNER_RADIUS; - - /** - * Used in conjunction with {@link #onTaskListVisibilityChanged(boolean, int)}, providing more - * granularity on which components of this task require an update - */ - @Retention(SOURCE) - @IntDef({FLAG_UPDATE_ALL, FLAG_UPDATE_ICON, FLAG_UPDATE_THUMBNAIL, FLAG_UPDATE_CORNER_RADIUS}) - public @interface TaskDataChanges { - } - - /** - * Type of task view - */ - @Retention(SOURCE) - @IntDef({Type.SINGLE, Type.GROUPED, Type.DESKTOP}) - public @interface Type { - int SINGLE = 1; - int GROUPED = 2; - int DESKTOP = 3; - } - - /** The maximum amount that a task view can be scrimmed, dimmed or tinted. */ - public static final float MAX_PAGE_SCRIM_ALPHA = 0.4f; - - private static final float EDGE_SCALE_DOWN_FACTOR_CAROUSEL = 0.03f; - private static final float EDGE_SCALE_DOWN_FACTOR_GRID = 0.00f; - - public static final long SCALE_ICON_DURATION = 120; - private static final long DIM_ANIM_DURATION = 700; - - /** - * This technically can be a vanilla {@link TouchDelegate} class, however that class requires - * setting the touch bounds at construction, so we'd repeatedly be created many instances - * unnecessarily as scrolling occurs, whereas {@link TransformingTouchDelegate} allows touch - * delegated bounds only to be updated. - */ - private TransformingTouchDelegate mIconTouchDelegate; - - private static final List SYSTEM_GESTURE_EXCLUSION_RECT = - Collections.singletonList(new Rect()); - - public static final FloatProperty FOCUS_TRANSITION = - new FloatProperty<>("focusTransition") { - @Override - public void setValue(TaskView taskView, float v) { - taskView.setIconsAndBannersTransitionProgress(v, false /* invert */); - } - - @Override - public Float get(TaskView taskView) { - return taskView.mFocusTransitionProgress; - } - }; - - private static final FloatProperty SPLIT_SELECT_TRANSLATION_X = - new FloatProperty<>("splitSelectTranslationX") { - @Override - public void setValue(TaskView taskView, float v) { - taskView.setSplitSelectTranslationX(v); - } - - @Override - public Float get(TaskView taskView) { - return taskView.mSplitSelectTranslationX; - } - }; - - private static final FloatProperty SPLIT_SELECT_TRANSLATION_Y = - new FloatProperty<>("splitSelectTranslationY") { - @Override - public void setValue(TaskView taskView, float v) { - taskView.setSplitSelectTranslationY(v); - } - - @Override - public Float get(TaskView taskView) { - return taskView.mSplitSelectTranslationY; - } - }; - - private static final FloatProperty DISMISS_TRANSLATION_X = - new FloatProperty<>("dismissTranslationX") { - @Override - public void setValue(TaskView taskView, float v) { - taskView.setDismissTranslationX(v); - } - - @Override - public Float get(TaskView taskView) { - return taskView.mDismissTranslationX; - } - }; - - private static final FloatProperty DISMISS_TRANSLATION_Y = - new FloatProperty<>("dismissTranslationY") { - @Override - public void setValue(TaskView taskView, float v) { - taskView.setDismissTranslationY(v); - } - - @Override - public Float get(TaskView taskView) { - return taskView.mDismissTranslationY; - } - }; - - private static final FloatProperty TASK_OFFSET_TRANSLATION_X = - new FloatProperty<>("taskOffsetTranslationX") { - @Override - public void setValue(TaskView taskView, float v) { - taskView.setTaskOffsetTranslationX(v); - } - - @Override - public Float get(TaskView taskView) { - return taskView.mTaskOffsetTranslationX; - } - }; - - private static final FloatProperty TASK_OFFSET_TRANSLATION_Y = - new FloatProperty<>("taskOffsetTranslationY") { - @Override - public void setValue(TaskView taskView, float v) { - taskView.setTaskOffsetTranslationY(v); - } - - @Override - public Float get(TaskView taskView) { - return taskView.mTaskOffsetTranslationY; - } - }; - - private static final FloatProperty TASK_RESISTANCE_TRANSLATION_X = - new FloatProperty<>("taskResistanceTranslationX") { - @Override - public void setValue(TaskView taskView, float v) { - taskView.setTaskResistanceTranslationX(v); - } - - @Override - public Float get(TaskView taskView) { - return taskView.mTaskResistanceTranslationX; - } - }; - - private static final FloatProperty TASK_RESISTANCE_TRANSLATION_Y = - new FloatProperty<>("taskResistanceTranslationY") { - @Override - public void setValue(TaskView taskView, float v) { - taskView.setTaskResistanceTranslationY(v); - } - - @Override - public Float get(TaskView taskView) { - return taskView.mTaskResistanceTranslationY; - } - }; - - public static final FloatProperty GRID_END_TRANSLATION_X = - new FloatProperty<>("gridEndTranslationX") { - @Override - public void setValue(TaskView taskView, float v) { - taskView.setGridEndTranslationX(v); - } - - @Override - public Float get(TaskView taskView) { - return taskView.mGridEndTranslationX; - } - }; - - public static final FloatProperty SNAPSHOT_SCALE = - new FloatProperty<>("snapshotScale") { - @Override - public void setValue(TaskView taskView, float v) { - taskView.setSnapshotScale(v); - } - - @Override - public Float get(TaskView taskView) { - return taskView.mTaskThumbnailViewDeprecated.getScaleX(); - } - }; - - public TaskViewData mTaskViewData = new TaskViewData(); - protected TaskThumbnailViewDeprecated mTaskThumbnailViewDeprecated; - protected TaskThumbnailView mTaskThumbnailView; - protected TaskViewIcon mIconView; - protected final DigitalWellBeingToast mDigitalWellBeingToast; - protected float mFullscreenProgress; - private float mGridProgress; - protected float mTaskThumbnailSplashAlpha; - private float mNonGridScale = 1; - private float mDismissScale = 1; - protected final FullscreenDrawParams mCurrentFullscreenParams; - protected final RecentsViewContainer mContainer; - - // Various causes of changing primary translation, which we aggregate to setTranslationX/Y(). - private float mDismissTranslationX; - private float mDismissTranslationY; - private float mTaskOffsetTranslationX; - private float mTaskOffsetTranslationY; - private float mTaskResistanceTranslationX; - private float mTaskResistanceTranslationY; - // The following translation variables should only be used in the same orientation as Launcher. - private float mBoxTranslationY; - // The following grid translations scales with mGridProgress. - private float mGridTranslationX; - private float mGridTranslationY; - // The following grid translation is used to animate closing the gap between grid and clear all. - private float mGridEndTranslationX; - // Applied as a complement to gridTranslation, for adjusting the carousel overview and quick - // switch. - private float mNonGridTranslationX; - private float mNonGridPivotTranslationX; - // Used when in SplitScreenSelectState - private float mSplitSelectTranslationY; - private float mSplitSelectTranslationX; - - @Nullable - private ObjectAnimator mIconAndDimAnimator; - private float mIconScaleAnimStartProgress = 0; - private float mFocusTransitionProgress = 1; - private float mModalness = 0; - private float mStableAlpha = 1; - - private int mTaskViewId = -1; - protected List mTaskContainers = Collections.emptyList(); - - private boolean mShowScreenshot; - private boolean mBorderEnabled; - - // The current background requests to load the task thumbnail and icon - @Nullable - private CancellableTask mThumbnailLoadRequest; - @Nullable - private CancellableTask mIconLoadRequest; - - private boolean mEndQuickswitchCuj; - - private final float[] mIconCenterCoords = new float[2]; - - protected final PointF mLastTouchDownPosition = new PointF(); - - private boolean mIsClickableAsLiveTile = true; - - @Nullable - private final BorderAnimator mFocusBorderAnimator; - - @Nullable - private final BorderAnimator mHoverBorderAnimator; - - public TaskView(Context context) { - this(context, null); - } - - public TaskView(Context context, @Nullable AttributeSet attrs) { - this(context, attrs, 0); - } - - public TaskView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { - this(context, attrs, defStyleAttr, 0); - } - - public TaskView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, - int defStyleRes) { - this(context, attrs, defStyleAttr, defStyleRes, null, null); - } - - @VisibleForTesting - public TaskView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, - int defStyleRes, BorderAnimator focusBorderAnimator, - BorderAnimator hoverBorderAnimator) { - super(context, attrs, defStyleAttr, defStyleRes); - mContainer = RecentsViewContainer.containerFromContext(context); - setOnClickListener(this::onClick); - - mCurrentFullscreenParams = new FullscreenDrawParams(context); - mDigitalWellBeingToast = new DigitalWellBeingToast(mContainer, this); - - boolean keyboardFocusHighlightEnabled = FeatureFlags.ENABLE_KEYBOARD_QUICK_SWITCH.get() - || Flags.enableFocusOutline(); - boolean cursorHoverStatesEnabled = enableCursorHoverStates(); - - setWillNotDraw(!keyboardFocusHighlightEnabled && !cursorHoverStatesEnabled); - - TypedArray styledAttrs = context.obtainStyledAttributes( - attrs, R.styleable.TaskView, defStyleAttr, defStyleRes); - - if (focusBorderAnimator != null) { - mFocusBorderAnimator = focusBorderAnimator; - } else { - mFocusBorderAnimator = keyboardFocusHighlightEnabled - ? BorderAnimator.createSimpleBorderAnimator( - /* borderRadiusPx= */ (int) mCurrentFullscreenParams.mCornerRadius, - /* borderWidthPx= */ context.getResources().getDimensionPixelSize( - R.dimen.keyboard_quick_switch_border_width), - /* boundsBuilder= */ this::getThumbnailBounds, - /* targetView= */ this, - /* borderColor= */ styledAttrs.getColor( - R.styleable.TaskView_focusBorderColor, DEFAULT_BORDER_COLOR)) - : null; - } - - if (hoverBorderAnimator != null) { - mHoverBorderAnimator = hoverBorderAnimator; - } else { - mHoverBorderAnimator = cursorHoverStatesEnabled - ? BorderAnimator.createSimpleBorderAnimator( - /* borderRadiusPx= */ (int) mCurrentFullscreenParams.mCornerRadius, - /* borderWidthPx= */ context.getResources().getDimensionPixelSize( - R.dimen.task_hover_border_width), - /* boundsBuilder= */ this::getThumbnailBounds, - /* targetView= */ this, - /* borderColor= */ styledAttrs.getColor( - R.styleable.TaskView_hoverBorderColor, DEFAULT_BORDER_COLOR)) - : null; - } - styledAttrs.recycle(); - } - - /** Returns the thumbnail's bounds relative to this view. */ - public Unit getThumbnailBounds(@NonNull Rect bounds) { - return getThumbnailBounds(bounds, false); - } - - /** Returns the thumbnail's bounds, optionally relative to the screen. */ - public Unit getThumbnailBounds(@NonNull Rect bounds, boolean relativeToDragLayer) { - View snapshotView = getSnapshotView(); - - if (relativeToDragLayer) { - mContainer.getDragLayer().getDescendantRectRelativeToSelf(snapshotView, bounds); - } else { - bounds.set(snapshotView.getLeft() + Math.round(snapshotView.getTranslationX()), - snapshotView.getTop() + Math.round(snapshotView.getTranslationY()), - snapshotView.getRight() + Math.round(snapshotView.getTranslationX()), - snapshotView.getBottom() + Math.round(snapshotView.getTranslationY())); - } - return Unit.INSTANCE; - } - - public void setTaskViewId(int id) { - this.mTaskViewId = id; - } - - public int getTaskViewId() { - return mTaskViewId; - } - - /** - * Updates [TaskThumbnailView] to reflect the latest [Task] state (i.e., task isRunning). - */ - public void notifyIsRunningTaskUpdated() { - // TODO(b/335649589): TaskView's VM will already have access to TaskThumbnailView's VM - // so there will be no need to access TaskThumbnailView's VM through the TaskThumbnailView - if (!mTaskContainers.isEmpty()) { - bindTaskThumbnailView(); - } - } - - /** - * Builds proto for logging. - * - * @deprecated Use {@link #getItemInfo(Task)} instead. - */ - @Deprecated - public WorkspaceItemInfo getItemInfo() { - return getItemInfo(getFirstTask()); - } - - /** - * Builds proto for logging - */ - @VisibleForTesting - public WorkspaceItemInfo getItemInfo(@Nullable Task task) { - WorkspaceItemInfo stubInfo = new WorkspaceItemInfo(); - stubInfo.itemType = LauncherSettings.Favorites.ITEM_TYPE_TASK; - stubInfo.container = LauncherSettings.Favorites.CONTAINER_TASKSWITCHER; - if (task == null) { - return stubInfo; - } - - ComponentKey componentKey = TaskUtils.getLaunchComponentKeyForTask(task.key); - stubInfo.user = componentKey.user; - stubInfo.intent = new Intent().setComponent(componentKey.componentName); - stubInfo.title = task.title; - if (getRecentsView() != null) { - stubInfo.screenId = getRecentsView().indexOfChild(this); - } - if (Flags.privateSpaceRestrictAccessibilityDrag()) { - if (UserCache.getInstance(getContext()).getUserInfo(componentKey.user).isPrivate()) { - stubInfo.runtimeStatusFlags |= FLAG_NOT_PINNABLE; - } - } - return stubInfo; - } - - @Override - protected void onFinishInflate() { - super.onFinishInflate(); - mTaskThumbnailViewDeprecated = findViewById(R.id.snapshot); - if (enableRefactorTaskThumbnail()) { - mTaskThumbnailView = new TaskThumbnailView(mContext); - mTaskThumbnailView.setLayoutParams(mTaskThumbnailViewDeprecated.getLayoutParams()); - int indexOfSnapshotView = indexOfChild(mTaskThumbnailViewDeprecated); - addView(mTaskThumbnailView, indexOfSnapshotView); - mTaskThumbnailViewDeprecated.setVisibility(View.GONE); - } - ViewStub iconViewStub = findViewById(R.id.icon); - if (enableOverviewIconMenu()) { - iconViewStub.setLayoutResource(R.layout.icon_app_chip_view); - } else { - iconViewStub.setLayoutResource(R.layout.icon_view); - } - mIconView = (TaskViewIcon) iconViewStub.inflate(); - mIconTouchDelegate = new TransformingTouchDelegate(mIconView.asView()); - } - - @Override - @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) - public void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) { - super.onFocusChanged(gainFocus, direction, previouslyFocusedRect); - if (mFocusBorderAnimator != null && mBorderEnabled) { - mFocusBorderAnimator.setBorderVisibility(gainFocus, /* animated= */ true); - } - } - - @Override - public boolean onHoverEvent(MotionEvent event) { - if (mHoverBorderAnimator != null && mBorderEnabled) { - switch (event.getAction()) { - case MotionEvent.ACTION_HOVER_ENTER: - mHoverBorderAnimator.setBorderVisibility(/* visible= */ true, /* animated= */ - true); - break; - case MotionEvent.ACTION_HOVER_EXIT: - mHoverBorderAnimator.setBorderVisibility(/* visible= */ false, /* animated= */ - true); - break; - default: - break; - } - } - return super.onHoverEvent(event); - } - - /** - * Enable or disable showing border on hover and focus change - */ - public void setBorderEnabled(boolean enabled) { - if (mBorderEnabled == enabled) { - return; - } - - mBorderEnabled = enabled; - // Set the animation correctly in case it misses the hover/focus event during state - // transition - if (mHoverBorderAnimator != null) { - mHoverBorderAnimator.setBorderVisibility(/* visible= */ - enabled && isHovered(), /* animated= */ true); - } - - if (mFocusBorderAnimator != null) { - mFocusBorderAnimator.setBorderVisibility(/* visible= */ - enabled && isFocused(), /* animated= */true); - } - } - - @Override - public boolean onInterceptHoverEvent(MotionEvent event) { - if (enableCursorHoverStates()) { - // avoid triggering hover event on child elements which would cause HOVER_EXIT for this - // task view - return true; - } else { - return super.onInterceptHoverEvent(event); - } - } - - @Override - public void draw(Canvas canvas) { - // Draw border first so any child views outside of the thumbnail bounds are drawn above it. - if (mFocusBorderAnimator != null) { - mFocusBorderAnimator.drawBorder(canvas); - } - if (mHoverBorderAnimator != null) { - mHoverBorderAnimator.drawBorder(canvas); - } - super.draw(canvas); - } - - /** - * Whether the taskview should take the touch event from parent. Events passed to children - * that might require special handling. - */ - public boolean offerTouchToChildren(MotionEvent event) { - if (event.getAction() == MotionEvent.ACTION_DOWN) { - computeAndSetIconTouchDelegate(mIconView, mIconCenterCoords, mIconTouchDelegate); - } - return mIconTouchDelegate != null && mIconTouchDelegate.onTouchEvent(event); - } - - protected void computeAndSetIconTouchDelegate(TaskViewIcon view, float[] tempCenterCoords, - TransformingTouchDelegate transformingTouchDelegate) { - if (view == null) { - return; - } - float viewHalfWidth = view.getWidth() / 2f; - float viewHalfHeight = view.getHeight() / 2f; - tempCenterCoords[0] = viewHalfWidth; - tempCenterCoords[1] = viewHalfHeight; - getDescendantCoordRelativeToAncestor(view.asView(), mContainer.getDragLayer(), - tempCenterCoords, false); - transformingTouchDelegate.setBounds( - (int) (tempCenterCoords[0] - viewHalfWidth), - (int) (tempCenterCoords[1] - viewHalfHeight), - (int) (tempCenterCoords[0] + viewHalfWidth), - (int) (tempCenterCoords[1] + viewHalfHeight)); - } - - /** - * The modalness of this view is how it should be displayed when it is shown on its own in the - * modal state of overview. - * - * @param modalness [0, 1] 0 being in context with other tasks, 1 being shown on its own. - */ - public void setModalness(float modalness) { - if (mModalness == modalness) { - return; - } - mModalness = modalness; - mIconView.setModalAlpha(1 - modalness); - mDigitalWellBeingToast.updateBannerOffset(modalness); - } - - public DigitalWellBeingToast getDigitalWellBeingToast() { - return mDigitalWellBeingToast; - } - - /** - * Updates this task view to the given {@param task}. - */ - public void bind(Task task, RecentsOrientedState orientedState) { - cancelPendingLoadTasks(); - setupTaskContainers(task); - setOrientationState(orientedState); - } - - protected void setupTaskContainers(Task task) { - mTaskContainers = Collections.singletonList( - new TaskContainer(task, mTaskThumbnailViewDeprecated, mIconView, - STAGE_POSITION_UNDEFINED, mDigitalWellBeingToast)); - if (enableRefactorTaskThumbnail()) { - bindTaskThumbnailView(); - } else { - mTaskThumbnailViewDeprecated.bind(task); - } - } - - private void bindTaskThumbnailView() { - // TODO(b/335649589): TaskView's VM will already have access to TaskThumbnailView's VM - // so there will be no need to access TaskThumbnailView's VM through the TaskThumbnailView - mTaskThumbnailView.getViewModel().bind(new TaskThumbnail(getFirstTask(), isRunningTask())); - } - - /** - * Sets up an on-click listener and the visibility for show_windows icon on top of the task. - */ - public void setUpShowAllInstancesListener() { - if (mTaskContainers.isEmpty()) { - return; - } - String taskPackageName = mTaskContainers.get(0).getTask().key.getPackageName(); - - // icon of the top/left task - View showWindowsView = findViewById(R.id.show_windows); - updateFilterCallback(showWindowsView, getFilterUpdateCallback(taskPackageName)); - } - - /** - * Returns a callback that updates the state of the filter and the recents overview - * - * @param taskPackageName package name of the task to filter by - */ - @Nullable - protected View.OnClickListener getFilterUpdateCallback(String taskPackageName) { - View.OnClickListener cb = (view) -> { - // update and apply a new filter - getRecentsView().setAndApplyFilter(taskPackageName); - }; - - if (!getRecentsView().getFilterState().shouldShowFilterUI(taskPackageName)) { - cb = null; - } - return cb; - } - - /** - * Sets the correct visibility and callback on the provided filterView based on whether - * the callback is null or not - */ - protected void updateFilterCallback(@NonNull View filterView, - @Nullable View.OnClickListener callback) { - // Filtering changes alpha instead of the visibility since visibility - // can be altered separately through RecentsView#resetFromSplitSelectionState() - if (callback == null) { - filterView.setAlpha(0); - } else { - filterView.setAlpha(1); - } - - filterView.setOnClickListener(callback); - } - - /** - * Returns a list of all TaskContainers in the TaskView. - */ - public List getTaskContainers() { - return mTaskContainers; - } - - /** - * Returns the first task bound to this TaskView. - * - * @deprecated Use {@link #mTaskContainers} instead. - */ - @Deprecated - @Nullable - public Task getFirstTask() { - return !mTaskContainers.isEmpty() ? mTaskContainers.get(0).getTask() : null; - } - - /** - * Check if given {@code taskId} is tracked in this view - */ - public boolean containsTaskId(int taskId) { - return getTaskContainerById(taskId) != null; - } - - /** - * Returns a copy of integer array containing taskIds of all tasks in the TaskView. - */ - public int[] getTaskIds() { - return mTaskContainers.stream().mapToInt( - container -> container.getTask().key.id).toArray(); - } - - public boolean containsMultipleTasks() { - return mTaskContainers.size() > 1; - } - - /** - * Returns the TaskContainer corresponding to a given taskId, or null if the TaskView does - * not contain a Task with that ID. - */ - @Nullable - public TaskContainer getTaskContainerById(int taskId) { - return mTaskContainers.stream().filter( - container -> container.getTask().key.id == taskId).findFirst().orElse(null); - } - - /** - * Returns the first thumbnailView of the TaskView. - * - * @deprecated Use {@link #mTaskContainers} instead. - */ - @Deprecated - public TaskThumbnailViewDeprecated getFirstThumbnailView() { - return !mTaskContainers.isEmpty() ? mTaskContainers.get(0).getThumbnailView() - : mTaskThumbnailViewDeprecated; - } - - void refreshThumbnails(@Nullable HashMap thumbnailDatas) { - if (enableRefactorTaskThumbnail()) { - // TODO(b/334825222) add thumbnail logic - return; - } - if (getFirstTask() != null && thumbnailDatas != null) { - final ThumbnailData thumbnailData = thumbnailDatas.get(getFirstTask().key.id); - if (thumbnailData != null) { - mTaskThumbnailViewDeprecated.setThumbnail(getFirstTask(), thumbnailData); - return; - } - } - - mTaskThumbnailViewDeprecated.refresh(); - } - - public TaskThumbnailViewDeprecated[] getThumbnailViews() { - return mTaskContainers.stream().map( - TaskContainer::getThumbnailView).toArray( - TaskThumbnailViewDeprecated[]::new); - } - - /** - * Returns the first iconView of the TaskView. - * - * @deprecated Use {@link #mTaskContainers} instead. - */ - @Deprecated - @Nullable - public TaskViewIcon getFirstIconView() { - return !mTaskContainers.isEmpty() ? mTaskContainers.get(0).getIconView() : null; - } - - @Override - public boolean dispatchTouchEvent(MotionEvent ev) { - RecentsView recentsView = getRecentsView(); - if (recentsView == null || getFirstTask() == null) { - return false; - } - SplitSelectStateController splitSelectStateController = - recentsView.getSplitSelectController(); - // Disable taps for split selection animation unless we have multiple tasks - boolean disableTapsForSplitSelect = - splitSelectStateController.isSplitSelectActive() - && splitSelectStateController.getInitialTaskId() == getFirstTask().key.id - && !containsMultipleTasks(); - if (disableTapsForSplitSelect) { - return false; - } - - if (ev.getAction() == MotionEvent.ACTION_DOWN) { - mLastTouchDownPosition.set(ev.getX(), ev.getY()); - } - return super.dispatchTouchEvent(ev); - } - - private void onClick(View view) { - if (getFirstTask() == null) { - Log.d("b/310064698", "onClick - task is null"); - return; - } - if (confirmSecondSplitSelectApp()) { - Log.d("b/310064698", - Arrays.toString(getTaskIds()) + " - onClick - split select is active"); - return; - } - RunnableList callbackList = launchTasks(); - Log.d("b/310064698", - Arrays.toString(getTaskIds()) + " - onClick - callbackList: " + callbackList); - if (callbackList != null) { - callbackList.add(() -> Log.d("b/310064698", - Arrays.toString(getTaskIds()) + " - onClick - launchCompleted")); - } - mContainer.getStatsLogManager().logger().withItemInfo(getItemInfo()) - .log(LAUNCHER_TASK_LAUNCH_TAP); - } - - /** - * @return {@code true} if user is already in split select mode and this tap was to choose the - * second app. {@code false} otherwise - */ - protected boolean confirmSecondSplitSelectApp() { - int index = getLastSelectedChildTaskIndex(); - if (index >= mTaskContainers.size()) { - return false; - } - TaskContainer container = mTaskContainers.get(index); - if (container != null) { - return getRecentsView().confirmSplitSelect(this, container.getTask(), - container.getIconView().getDrawable(), container.getThumbnailView(), - container.getThumbnailView().getThumbnail(), /* intent */ null, - /* user */ null, container.getItemInfo()); - } - return false; - } - - /** - * Returns the task index of the last selected child task (0 or 1). - * If we contain multiple tasks and this TaskView is used as part of split selection, the - * selected child task index will be that of the remaining task. - */ - protected int getLastSelectedChildTaskIndex() { - return 0; - } - - /** - * Starts the task associated with this view and animates the startup. - * - * @return CompletionStage to indicate the animation completion or null if the launch failed. - */ - @Nullable - public RunnableList launchTaskAnimated() { - if (getFirstTask() != null) { - TestLogging.recordEvent( - TestProtocol.SEQUENCE_MAIN, "startActivityFromRecentsAsync", Arrays.toString( - getTaskIds())); - ActivityOptionsWrapper opts = mContainer.getActivityLaunchOptions(this, null); - opts.options.setLaunchDisplayId( - getDisplay() == null ? DEFAULT_DISPLAY : getDisplay().getDisplayId()); - if (ActivityManagerWrapper.getInstance() - .startActivityFromRecents(getFirstTask().key, opts.options)) { - Log.d(TAG, "launchTaskAnimated - startActivityFromRecents: " + Arrays.toString( - getTaskIds())); - ActiveGestureLog.INSTANCE.trackEvent(EXPECTING_TASK_APPEARED); - RecentsView recentsView = getRecentsView(); - if (recentsView.getRunningTaskViewId() != -1) { - recentsView.onTaskLaunchedInLiveTileMode(); - - // Return a fresh callback in the live tile case, so that it's not accidentally - // triggered by QuickstepTransitionManager.AppLaunchAnimationRunner. - RunnableList callbackList = new RunnableList(); - recentsView.addSideTaskLaunchCallback(callbackList); - return callbackList; - } - if (TaskAnimationManager.ENABLE_SHELL_TRANSITIONS) { - // If the recents transition is running (ie. in live tile mode), then the start - // of a new task will merge into the existing transition and it currently will - // not be run independently, so we need to rely on the onTaskAppeared() call - // for the new task to trigger the side launch callback to flush this runnable - // list (which is usually flushed when the app launch animation finishes) - recentsView.addSideTaskLaunchCallback(opts.onEndCallback); - } - return opts.onEndCallback; - } else { - notifyTaskLaunchFailed(TAG); - return null; - } - } else { - Log.d(TAG, "launchTaskAnimated - getTask() is null" + Arrays.toString(getTaskIds())); - return null; - } - } - - /** - * Starts the task associated with this view without any animation - */ - public void launchTask(@NonNull Consumer callback) { - launchTask(callback, false /* isQuickswitch */); - } - - /** - * Starts the task associated with this view without any animation - */ - public void launchTask(@NonNull Consumer callback, boolean isQuickswitch) { - if (getFirstTask() != null) { - TestLogging.recordEvent( - TestProtocol.SEQUENCE_MAIN, "startActivityFromRecentsAsync", Arrays.toString( - getTaskIds())); - - TaskRemovedDuringLaunchListener failureListener = new TaskRemovedDuringLaunchListener( - getContext().getApplicationContext()); - if (isQuickswitch) { - // We only listen for failures to launch in quickswitch because the during this - // gesture launcher is in the background state, vs other launches which are in - // the actual overview state - failureListener.register(mContainer, getFirstTask().key.id, () -> { - notifyTaskLaunchFailed(TAG); - RecentsView rv = getRecentsView(); - if (rv != null) { - // Disable animations for now, as it is an edge case and the app usually - // covers launcher and also any state transition animation also gets - // clobbered by QuickstepTransitionManager.createWallpaperOpenAnimations - // when launcher shows again - rv.startHome(false /* animated */); - if (rv.mSizeStrategy.getTaskbarController() != null) { - // LauncherTaskbarUIController depends on the launcher state when - // checking whether to handle resume, but that can come in before - // startHome() changes the state, so force-refresh here to ensure the - // taskbar is updated - rv.mSizeStrategy.getTaskbarController().refreshResumedState(); - } - } - }); - } - // Indicate success once the system has indicated that the transition has started - ActivityOptions opts = ActivityOptions.makeCustomTaskAnimation(getContext(), 0, 0, - MAIN_EXECUTOR.getHandler(), - elapsedRealTime -> callback.accept(true), - elapsedRealTime -> failureListener.onTransitionFinished()); - opts.setLaunchDisplayId( - getDisplay() == null ? DEFAULT_DISPLAY : getDisplay().getDisplayId()); - if (isQuickswitch) { - opts.setFreezeRecentTasksReordering(); - } - // TODO(b/334826842) add splash functionality to new TTV - if (!enableRefactorTaskThumbnail()) { - opts.setDisableStartingWindow(mTaskThumbnailViewDeprecated.shouldShowSplashView()); - } - Task.TaskKey key = getFirstTask().key; - UI_HELPER_EXECUTOR.execute(() -> { - if (!ActivityManagerWrapper.getInstance().startActivityFromRecents(key, opts)) { - // If the call to start activity failed, then post the result immediately, - // otherwise, wait for the animation start callback from the activity options - // above - MAIN_EXECUTOR.post(() -> { - notifyTaskLaunchFailed(TAG); - callback.accept(false); - }); - } - Log.d(TAG, - "launchTask - startActivityFromRecents: " + Arrays.toString(getTaskIds())); - }); - } else { - callback.accept(false); - Log.d(TAG, "launchTask - getTask() is null" + Arrays.toString(getTaskIds())); - } - } - - /** - * Launch of the current task (both live and inactive tasks) with an animation. - */ - @Nullable - public RunnableList launchTasks() { - RecentsView recentsView = getRecentsView(); - RemoteTargetHandle[] remoteTargetHandles = recentsView.mRemoteTargetHandles; - if (isRunningTask() && remoteTargetHandles != null) { - if (!mIsClickableAsLiveTile) { - Log.e(TAG, "TaskView is not clickable as a live tile; returning to home."); - return null; - } - - mIsClickableAsLiveTile = false; - RemoteAnimationTargets targets; - if (remoteTargetHandles.length == 1) { - targets = remoteTargetHandles[0].getTransformParams().getTargetSet(); - } else { - RemoteAnimationTarget[] apps = Arrays.stream(remoteTargetHandles) - .flatMap(handle -> Stream.of( - handle.getTransformParams().getTargetSet().apps)) - .toArray(RemoteAnimationTarget[]::new); - RemoteAnimationTarget[] wallpapers = Arrays.stream(remoteTargetHandles) - .flatMap(handle -> Stream.of( - handle.getTransformParams().getTargetSet().wallpapers)) - .toArray(RemoteAnimationTarget[]::new); - targets = new RemoteAnimationTargets(apps, wallpapers, - remoteTargetHandles[0].getTransformParams().getTargetSet().nonApps, - remoteTargetHandles[0].getTransformParams().getTargetSet().targetMode); - } - if (targets == null) { - // If the recents animation is cancelled somehow between the parent if block and - // here, try to launch the task as a non live tile task. - RunnableList runnableList = launchTaskAnimated(); - if (runnableList == null) { - Log.e(TAG, "Recents animation cancelled and cannot launch task as non-live tile" - + "; returning to home"); - } - mIsClickableAsLiveTile = true; - return runnableList; - } - - RunnableList runnableList = new RunnableList(); - AnimatorSet anim = new AnimatorSet(); - TaskViewUtils.composeRecentsLaunchAnimator( - anim, this, targets.apps, - targets.wallpapers, targets.nonApps, true /* launcherClosing */, - recentsView.getStateManager(), recentsView, - recentsView.getDepthController()); - anim.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animator) { - if (getFirstTask() != null - && getFirstTask().key.displayId != getRootViewDisplayId()) { - launchTaskAnimated(); - } - mIsClickableAsLiveTile = true; - runEndCallback(); - } - - @Override - public void onAnimationCancel(Animator animation) { - runEndCallback(); - } - - private void runEndCallback() { - runnableList.executeAllAndDestroy(); - } - }); - anim.start(); - Log.d(TAG, "launchTasks - composeRecentsLaunchAnimator: " + Arrays.toString( - getTaskIds())); - recentsView.onTaskLaunchedInLiveTileMode(); - return runnableList; - } else { - return launchTaskAnimated(); - } - } - - /** - * See {@link TaskDataChanges} - * - * @param visible If this task view will be visible to the user in overview or hidden - */ - public void onTaskListVisibilityChanged(boolean visible) { - onTaskListVisibilityChanged(visible, FLAG_UPDATE_ALL); - } - - /** - * See {@link TaskDataChanges} - * - * @param visible If this task view will be visible to the user in overview or hidden - */ - public void onTaskListVisibilityChanged(boolean visible, @TaskDataChanges int changes) { - if (getFirstTask() == null) { - return; - } - cancelPendingLoadTasks(); - if (visible) { - // These calls are no-ops if the data is already loaded, try and load the high - // resolution thumbnail if the state permits - RecentsModel model = RecentsModel.INSTANCE.get(getContext()); - TaskThumbnailCache thumbnailCache = model.getThumbnailCache(); - TaskIconCache iconCache = model.getIconCache(); - - if (needsUpdate(changes, FLAG_UPDATE_THUMBNAIL)) { - mThumbnailLoadRequest = thumbnailCache.updateThumbnailInBackground( - getFirstTask(), thumbnail -> { - if (!enableRefactorTaskThumbnail()) { - // TODO(b/334825222) add thumbnail state - mTaskThumbnailViewDeprecated.setThumbnail(getFirstTask(), - thumbnail); - } - }); - } - if (needsUpdate(changes, FLAG_UPDATE_ICON)) { - mIconLoadRequest = iconCache.updateIconInBackground(getFirstTask(), - (task) -> { - setIcon(mIconView, task.icon); - if (enableOverviewIconMenu()) { - setText(mIconView, task.title); - } - mDigitalWellBeingToast.initialize(task); - }); - } - if (needsUpdate(changes, FLAG_UPDATE_CORNER_RADIUS)) { - mCurrentFullscreenParams.updateCornerRadius(getContext()); - } - } else { - if (needsUpdate(changes, FLAG_UPDATE_THUMBNAIL)) { - if (!enableRefactorTaskThumbnail()) { - // TODO(b/334825222) add thumbnail state - mTaskThumbnailViewDeprecated.setThumbnail(null, null); - } - // Reset the task thumbnail reference as well (it will be fetched from the cache or - // reloaded next time we need it) - getFirstTask().thumbnail = null; - } - if (needsUpdate(changes, FLAG_UPDATE_ICON)) { - setIcon(mIconView, null); - if (enableOverviewIconMenu()) { - setText(mIconView, null); - } - } - } - } - - protected boolean needsUpdate(@TaskDataChanges int dataChange, @TaskDataChanges int flag) { - return (dataChange & flag) == flag; - } - - protected void cancelPendingLoadTasks() { - if (mThumbnailLoadRequest != null) { - mThumbnailLoadRequest.cancel(); - mThumbnailLoadRequest = null; - } - if (mIconLoadRequest != null) { - mIconLoadRequest.cancel(); - mIconLoadRequest = null; - } - } - - private boolean showTaskMenu(TaskViewIcon iconView) { - if (!getRecentsView().canLaunchFullscreenTask()) { - // Don't show menu when selecting second split screen app - return true; - } - - if (!mContainer.getDeviceProfile().isTablet - && !getRecentsView().isClearAllHidden()) { - getRecentsView().snapToPage(getRecentsView().indexOfChild(this)); - return false; - } else { - mContainer.getStatsLogManager().logger().withItemInfo(getItemInfo()) - .log(LAUNCHER_TASK_ICON_TAP_OR_LONGPRESS); - return showTaskMenuWithContainer(iconView); - } - } - - protected boolean showTaskMenuWithContainer(TaskViewIcon iconView) { - Optional menuContainer = mTaskContainers.stream().filter( - container -> container.getIconView() == iconView).findAny(); - if (menuContainer.isEmpty()) { - return false; - } - DeviceProfile dp = mContainer.getDeviceProfile(); - if (enableOverviewIconMenu() && iconView instanceof IconAppChipView) { - ((IconAppChipView) iconView).revealAnim(/* isRevealing= */ true); - return TaskMenuView.showForTask(menuContainer.get(), - () -> ((IconAppChipView) iconView).revealAnim(/* isRevealing= */ false)); - } else if (dp.isTablet) { - int alignedOptionIndex = 0; - if (getRecentsView().isOnGridBottomRow(menuContainer.get().getTaskView()) - && dp.isLandscape) { - if (Flags.enableGridOnlyOverview()) { - // With no focused task, there is less available space below the tasks, so align - // the arrow to the third option in the menu. - alignedOptionIndex = 2; - } else { - // Bottom row of landscape grid aligns arrow to second option to avoid clipping - alignedOptionIndex = 1; - } - } - return TaskMenuViewWithArrow.Companion.showForTask(menuContainer.get(), - alignedOptionIndex); - } else { - return TaskMenuView.showForTask(menuContainer.get()); - } - } - - protected void setIcon(TaskViewIcon iconView, @Nullable Drawable icon) { - if (icon != null) { - iconView.setDrawable(icon); - iconView.setOnClickListener(v -> { - if (confirmSecondSplitSelectApp()) { - return; - } - showTaskMenu(iconView); - }); - iconView.setOnLongClickListener(v -> { - requestDisallowInterceptTouchEvent(true); - return showTaskMenu(iconView); - }); - } else { - iconView.setDrawable(null); - iconView.setOnClickListener(null); - iconView.setOnLongClickListener(null); - } - } - - protected void setText(TaskViewIcon iconView, CharSequence text) { - iconView.setText(text); - } - - public void setOrientationState(RecentsOrientedState orientationState) { - mIconView.setIconOrientation(orientationState, isGridTask()); - setThumbnailOrientation(orientationState); - } - - protected void setThumbnailOrientation(RecentsOrientedState orientationState) { - DeviceProfile deviceProfile = mContainer.getDeviceProfile(); - int thumbnailTopMargin = deviceProfile.overviewTaskThumbnailTopMarginPx; - - // TODO(b/271468547), we should default to setting trasnlations only on the snapshot instead - // of a hybrid of both margins and translations - LayoutParams snapshotParams = (LayoutParams) getSnapshotView().getLayoutParams(); - snapshotParams.topMargin = thumbnailTopMargin; - getSnapshotView().setLayoutParams(snapshotParams); - - // TODO(b/335606129) Investigate the usage of [TaskOverlay] in the new TaskThumbnailView. - // and if it's still necessary we should support that in the new TTV class. - if (!enableRefactorTaskThumbnail()) { - mTaskThumbnailViewDeprecated.getTaskOverlay().updateOrientationState(orientationState); - } - mDigitalWellBeingToast.initialize(getFirstTask()); - } - - /** - * Returns whether the task is part of overview grid and not being focused. - */ - public boolean isGridTask() { - DeviceProfile deviceProfile = mContainer.getDeviceProfile(); - return deviceProfile.isTablet && !isFocusedTask(); - } - - /** - * Called to animate a smooth transition when going directly from an app into Overview (and - * vice versa). Icons fade in, and DWB banners slide in with a "shift up" animation. - */ - protected void setIconsAndBannersTransitionProgress(float progress, boolean invert) { - if (invert) { - progress = 1 - progress; - } - mFocusTransitionProgress = progress; - float iconScalePercentage = (float) SCALE_ICON_DURATION / DIM_ANIM_DURATION; - float lowerClamp = invert ? 1f - iconScalePercentage : 0; - float upperClamp = invert ? 1 : iconScalePercentage; - float scale = Interpolators.clampToProgress(FAST_OUT_SLOW_IN, lowerClamp, upperClamp) - .getInterpolation(progress); - mIconView.setContentAlpha(scale); - mDigitalWellBeingToast.updateBannerOffset(1f - scale); - } - - public void setIconScaleAnimStartProgress(float startProgress) { - mIconScaleAnimStartProgress = startProgress; - } - - public void animateIconScaleAndDimIntoView() { - if (mIconAndDimAnimator != null) { - mIconAndDimAnimator.cancel(); - } - mIconAndDimAnimator = ObjectAnimator.ofFloat(this, FOCUS_TRANSITION, 1); - mIconAndDimAnimator.setCurrentFraction(mIconScaleAnimStartProgress); - mIconAndDimAnimator.setDuration(DIM_ANIM_DURATION).setInterpolator(LINEAR); - mIconAndDimAnimator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - mIconAndDimAnimator = null; - } - }); - mIconAndDimAnimator.start(); - } - - protected void setIconScaleAndDim(float iconScale) { - setIconScaleAndDim(iconScale, false); - } - - private void setIconScaleAndDim(float iconScale, boolean invert) { - if (mIconAndDimAnimator != null) { - mIconAndDimAnimator.cancel(); - } - setIconsAndBannersTransitionProgress(iconScale, invert); - } - - protected void resetPersistentViewTransforms() { - mNonGridTranslationX = mGridTranslationX = - mGridTranslationY = mBoxTranslationY = mNonGridPivotTranslationX = 0f; - resetViewTransforms(); - } - - protected void resetViewTransforms() { - // fullscreenTranslation and accumulatedTranslation should not be reset, as - // resetViewTransforms is called during Quickswitch scrolling. - mDismissTranslationX = mTaskOffsetTranslationX = - mTaskResistanceTranslationX = mSplitSelectTranslationX = mGridEndTranslationX = 0f; - mDismissTranslationY = mTaskOffsetTranslationY = mTaskResistanceTranslationY = 0f; - if (getRecentsView() == null || !getRecentsView().isSplitSelectionActive()) { - mSplitSelectTranslationY = 0f; - } - - setSnapshotScale(1f); - applyTranslationX(); - applyTranslationY(); - setTranslationZ(0); - setAlpha(mStableAlpha); - setIconScaleAndDim(1); - setColorTint(0, 0); - if (!enableRefactorTaskThumbnail()) { - // TODO(b/335399428) add split select functionality to new TTV - mTaskThumbnailViewDeprecated.resetViewTransforms(); - } - } - - public void setStableAlpha(float parentAlpha) { - mStableAlpha = parentAlpha; - setAlpha(mStableAlpha); - } - - @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) - // TODO(b/334825222): Implement thumbnail/snapshot for the new [TaskThumbnailView]. - if (enableRefactorTaskThumbnail()) { - notifyIsRunningTaskUpdated(); - } else { - mTaskThumbnailViewDeprecated.setThumbnail(getFirstTask(), null); - } - setOverlayEnabled(false); - onTaskListVisibilityChanged(false); - mBorderEnabled = false; - } - - public float getTaskCornerRadius() { - return mCurrentFullscreenParams.mCornerRadius; - } - - @Override - protected void onLayout(boolean changed, int left, int top, int right, int bottom) { - super.onLayout(changed, left, top, right, bottom); - DeviceProfile deviceProfile = mContainer.getDeviceProfile(); - int thumbnailTopMargin = deviceProfile.overviewTaskThumbnailTopMarginPx; - if (deviceProfile.isTablet) { - setPivotX(getLayoutDirection() == LAYOUT_DIRECTION_RTL ? 0 : right - left); - setPivotY(thumbnailTopMargin); - } else { - setPivotX((right - left) * 0.5f); - setPivotY(thumbnailTopMargin + (getHeight() - thumbnailTopMargin) * 0.5f); - } - SYSTEM_GESTURE_EXCLUSION_RECT.get(0).set(0, 0, getWidth(), getHeight()); - setSystemGestureExclusionRects(SYSTEM_GESTURE_EXCLUSION_RECT); - } - - /** - * How much to scale down pages near the edge of the screen. - */ - public static float getEdgeScaleDownFactor(DeviceProfile deviceProfile) { - return deviceProfile.isTablet ? EDGE_SCALE_DOWN_FACTOR_GRID - : EDGE_SCALE_DOWN_FACTOR_CAROUSEL; - } - - private void setNonGridScale(float nonGridScale) { - mNonGridScale = nonGridScale; - applyScale(); - } - - public float getNonGridScale() { - return mNonGridScale; - } - - private void setSnapshotScale(float dismissScale) { - mDismissScale = dismissScale; - applyScale(); - } - - /** - * Moves TaskView between carousel and 2 row grid. - * - * @param gridProgress 0 = carousel; 1 = 2 row grid. - */ - public void setGridProgress(float gridProgress) { - mGridProgress = gridProgress; - applyTranslationX(); - applyTranslationY(); - applyScale(); - } - - private void applyScale() { - float scale = 1; - scale *= getPersistentScale(); - scale *= mDismissScale; - setScaleX(scale); - setScaleY(scale); - if (enableRefactorTaskThumbnail()) { - mTaskViewData.getScale().setValue(scale); - } - updateSnapshotRadius(); - } - - /** - * Returns multiplication of scale that is persistent (e.g. fullscreen and grid), and does not - * change according to a temporary state. - */ - public float getPersistentScale() { - float scale = 1; - scale *= Utilities.mapRange(mGridProgress, mNonGridScale, 1f); - return scale; - } - - /** - * Updates alpha of task thumbnail splash on swipe up/down. - */ - public void setTaskThumbnailSplashAlpha(float taskThumbnailSplashAlpha) { - mTaskThumbnailSplashAlpha = taskThumbnailSplashAlpha; - applyThumbnailSplashAlpha(); - } - - protected void applyThumbnailSplashAlpha() { - if (!enableRefactorTaskThumbnail()) { - // TODO(b/334826842) add splash functionality to new TTV - mTaskThumbnailViewDeprecated.setSplashAlpha(mTaskThumbnailSplashAlpha); - } - } - - protected void refreshTaskThumbnailSplash() { - if (!enableRefactorTaskThumbnail()) { - // TODO(b/334826842) add splash functionality to new TTV - mTaskThumbnailViewDeprecated.refreshSplashView(); - } - } - - private void setSplitSelectTranslationX(float x) { - mSplitSelectTranslationX = x; - applyTranslationX(); - } - - private void setSplitSelectTranslationY(float y) { - mSplitSelectTranslationY = y; - applyTranslationY(); - } - - private void setDismissTranslationX(float x) { - mDismissTranslationX = x; - applyTranslationX(); - } - - private void setDismissTranslationY(float y) { - mDismissTranslationY = y; - applyTranslationY(); - } - - private void setTaskOffsetTranslationX(float x) { - mTaskOffsetTranslationX = x; - applyTranslationX(); - } - - private void setTaskOffsetTranslationY(float y) { - mTaskOffsetTranslationY = y; - applyTranslationY(); - } - - private void setTaskResistanceTranslationX(float x) { - mTaskResistanceTranslationX = x; - applyTranslationX(); - } - - private void setTaskResistanceTranslationY(float y) { - mTaskResistanceTranslationY = y; - applyTranslationY(); - } - - public float getNonGridTranslationX() { - return mNonGridTranslationX; - } - - /** - * Updates X coordinate of non-grid translation. - */ - public void setNonGridTranslationX(float nonGridTranslationX) { - mNonGridTranslationX = nonGridTranslationX; - applyTranslationX(); - } - - public void setGridTranslationX(float gridTranslationX) { - mGridTranslationX = gridTranslationX; - applyTranslationX(); - } - - public float getGridTranslationX() { - return mGridTranslationX; - } - - public void setGridTranslationY(float gridTranslationY) { - mGridTranslationY = gridTranslationY; - applyTranslationY(); - } - - public float getGridTranslationY() { - return mGridTranslationY; - } - - private void setGridEndTranslationX(float gridEndTranslationX) { - mGridEndTranslationX = gridEndTranslationX; - applyTranslationX(); - } - - /** - * Set translation X for non-grid pivot - */ - public void setNonGridPivotTranslationX(float nonGridPivotTranslationX) { - mNonGridPivotTranslationX = nonGridPivotTranslationX; - applyTranslationX(); - } - - public float getScrollAdjustment(boolean gridEnabled) { - float scrollAdjustment = 0; - if (gridEnabled) { - scrollAdjustment += mGridTranslationX; - } else { - scrollAdjustment += getNonGridTranslationX(); - } - return scrollAdjustment; - } - - public float getOffsetAdjustment(boolean gridEnabled) { - return getScrollAdjustment(gridEnabled); - } - - public float getSizeAdjustment(boolean fullscreenEnabled) { - float sizeAdjustment = 1; - if (fullscreenEnabled) { - sizeAdjustment *= mNonGridScale; - } - return sizeAdjustment; - } - - private void setBoxTranslationY(float boxTranslationY) { - mBoxTranslationY = boxTranslationY; - applyTranslationY(); - } - - private void applyTranslationX() { - setTranslationX(mDismissTranslationX + mTaskOffsetTranslationX + mTaskResistanceTranslationX - + mSplitSelectTranslationX + mGridEndTranslationX + getPersistentTranslationX()); - } - - private void applyTranslationY() { - setTranslationY(mDismissTranslationY + mTaskOffsetTranslationY + mTaskResistanceTranslationY - + mSplitSelectTranslationY + getPersistentTranslationY()); - } - - /** - * Returns addition of translationX that is persistent (e.g. fullscreen and grid), and does not - * change according to a temporary state (e.g. task offset). - */ - public float getPersistentTranslationX() { - return getNonGridTrans(mNonGridTranslationX) + getGridTrans(mGridTranslationX) - + getNonGridTrans(mNonGridPivotTranslationX); - } - - /** - * Returns addition of translationY that is persistent (e.g. fullscreen and grid), and does not - * change according to a temporary state (e.g. task offset). - */ - public float getPersistentTranslationY() { - return mBoxTranslationY + getGridTrans(mGridTranslationY); - } - - public FloatProperty getPrimarySplitTranslationProperty() { - return getPagedOrientationHandler().getPrimaryValue( - SPLIT_SELECT_TRANSLATION_X, SPLIT_SELECT_TRANSLATION_Y); - } - - public FloatProperty getSecondarySplitTranslationProperty() { - return getPagedOrientationHandler().getSecondaryValue( - SPLIT_SELECT_TRANSLATION_X, SPLIT_SELECT_TRANSLATION_Y); - } - - public FloatProperty getPrimaryDismissTranslationProperty() { - return getPagedOrientationHandler().getPrimaryValue( - DISMISS_TRANSLATION_X, DISMISS_TRANSLATION_Y); - } - - public FloatProperty getSecondaryDismissTranslationProperty() { - return getPagedOrientationHandler().getSecondaryValue( - DISMISS_TRANSLATION_X, DISMISS_TRANSLATION_Y); - } - - public FloatProperty getPrimaryTaskOffsetTranslationProperty() { - return getPagedOrientationHandler().getPrimaryValue( - TASK_OFFSET_TRANSLATION_X, TASK_OFFSET_TRANSLATION_Y); - } - - public FloatProperty getSecondaryTaskOffsetTranslationProperty() { - return getPagedOrientationHandler().getSecondaryValue( - TASK_OFFSET_TRANSLATION_X, TASK_OFFSET_TRANSLATION_Y); - } - - public FloatProperty getTaskResistanceTranslationProperty() { - return getPagedOrientationHandler().getSecondaryValue( - TASK_RESISTANCE_TRANSLATION_X, TASK_RESISTANCE_TRANSLATION_Y); - } - - @Override - public boolean hasOverlappingRendering() { - // TODO: Clip-out the icon region from the thumbnail, since they are overlapping. - return false; - } - - public boolean isEndQuickswitchCuj() { - return mEndQuickswitchCuj; - } - - public void setEndQuickswitchCuj(boolean endQuickswitchCuj) { - mEndQuickswitchCuj = endQuickswitchCuj; - } - - @Override - public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { - super.onInitializeAccessibilityNodeInfo(info); - - info.addAction( - new AccessibilityNodeInfo.AccessibilityAction(R.string.accessibility_close, - getContext().getText(R.string.accessibility_close))); - - final Context context = getContext(); - for (TaskContainer taskContainer : mTaskContainers) { - for (SystemShortcut s : TraceHelper.allowIpcs( - "TV.a11yInfo", () -> getEnabledShortcuts(this, taskContainer))) { - info.addAction(s.createAccessibilityAction(context)); - } - } - - if (mDigitalWellBeingToast.hasLimit()) { - info.addAction( - new AccessibilityNodeInfo.AccessibilityAction( - R.string.accessibility_app_usage_settings, - getContext().getText(R.string.accessibility_app_usage_settings))); - } - - final RecentsView recentsView = getRecentsView(); - final AccessibilityNodeInfo.CollectionItemInfo itemInfo = - AccessibilityNodeInfo.CollectionItemInfo.obtain( - 0, 1, recentsView.getTaskViewCount() - recentsView.indexOfChild(this) - 1, - 1, false); - info.setCollectionItemInfo(itemInfo); - } - - @Override - public boolean performAccessibilityAction(int action, Bundle arguments) { - if (action == R.string.accessibility_close) { - getRecentsView().dismissTask(this, true /*animateTaskView*/, - true /*removeTask*/); - return true; - } - - if (action == R.string.accessibility_app_usage_settings) { - mDigitalWellBeingToast.openAppUsageSettings(this); - return true; - } - - for (TaskContainer taskContainer : mTaskContainers) { - for (SystemShortcut s : getEnabledShortcuts(this, - taskContainer)) { - if (s.hasHandlerForAction(action)) { - s.onClick(this); - return true; - } - } - } - - return super.performAccessibilityAction(action, arguments); - } - - @Nullable - public RecentsView getRecentsView() { - return (RecentsView) getParent(); - } - - RecentsPagedOrientationHandler getPagedOrientationHandler() { - return getRecentsView().mOrientationState.getOrientationHandler(); - } - - private void notifyTaskLaunchFailed(String tag) { - String msg = "Failed to launch task"; - if (getFirstTask() != null) { - msg += " (task=" + getFirstTask().key.baseIntent + " userId=" - + getFirstTask().key.userId + ")"; - } - Log.w(tag, msg); - Toast.makeText(getContext(), R.string.activity_not_available, LENGTH_SHORT).show(); - } - - /** - * Hides the icon and shows insets when this TaskView is about to be shown fullscreen. - * - * @param progress: 0 = show icon and no insets; 1 = don't show icon and show full insets. - */ - public void setFullscreenProgress(float progress) { - progress = Utilities.boundToRange(progress, 0, 1); - mFullscreenProgress = progress; - mIconView.setVisibility(progress < 1 ? VISIBLE : INVISIBLE); - mTaskThumbnailViewDeprecated.getTaskOverlay().setFullscreenProgress(progress); - - RecentsView recentsView = mContainer.getOverviewPanel(); - // Animate icons and DWB banners in/out, except in QuickSwitch state, when tiles are - // oversized and banner would look disproportionately large. - if (recentsView.getStateManager().getState() != BACKGROUND_APP) { - setIconsAndBannersTransitionProgress(progress, true); - } - - updateSnapshotRadius(); - } - - protected void updateSnapshotRadius() { - updateCurrentFullscreenParams(); - mTaskThumbnailViewDeprecated.setFullscreenParams(mCurrentFullscreenParams); - } - - void updateCurrentFullscreenParams() { - updateFullscreenParams(mCurrentFullscreenParams); - } - - protected void updateFullscreenParams(TaskView.FullscreenDrawParams fullscreenParams) { - if (getRecentsView() == null) { - return; - } - fullscreenParams.setProgress( - mFullscreenProgress, getRecentsView().getScaleX(), getScaleX()); - } - - /** - * Updates TaskView scaling and translation required to support variable width if enabled, while - * ensuring TaskView fits into screen in fullscreen. - */ - void updateTaskSize() { - ViewGroup.LayoutParams params = getLayoutParams(); - float nonGridScale; - float boxTranslationY; - int expectedWidth; - int expectedHeight; - DeviceProfile deviceProfile = mContainer.getDeviceProfile(); - final int thumbnailPadding = deviceProfile.overviewTaskThumbnailTopMarginPx; - final Rect lastComputedTaskSize = getRecentsView().getLastComputedTaskSize(); - final int taskWidth = lastComputedTaskSize.width(); - final int taskHeight = lastComputedTaskSize.height(); - if (deviceProfile.isTablet) { - int boxWidth; - int boxHeight; - boolean isFocusedTask = isFocusedTask(); - if (isFocusedTask) { - // Task will be focused and should use focused task size. Use focusTaskRatio - // that is associated with the original orientation of the focused task. - boxWidth = taskWidth; - boxHeight = taskHeight; - } else { - // Otherwise task is in grid, and should use lastComputedGridTaskSize. - Rect lastComputedGridTaskSize = getRecentsView().getLastComputedGridTaskSize(); - boxWidth = lastComputedGridTaskSize.width(); - boxHeight = lastComputedGridTaskSize.height(); - } - - // Bound width/height to the box size. - expectedWidth = boxWidth; - expectedHeight = boxHeight + thumbnailPadding; - - // Scale to to fit task Rect. - if (enableGridOnlyOverview()) { - final Rect lastComputedCarouselTaskSize = - getRecentsView().getLastComputedCarouselTaskSize(); - nonGridScale = lastComputedCarouselTaskSize.width() / (float) taskWidth; - } else { - nonGridScale = taskWidth / (float) boxWidth; - } - - // Align to top of task Rect. - boxTranslationY = (expectedHeight - thumbnailPadding - taskHeight) / 2.0f; - } else { - nonGridScale = 1f; - boxTranslationY = 0f; - expectedWidth = enableOverviewIconMenu() ? taskWidth : LayoutParams.MATCH_PARENT; - expectedHeight = enableOverviewIconMenu() - ? taskHeight + thumbnailPadding - : LayoutParams.MATCH_PARENT; - } - - setNonGridScale(nonGridScale); - setBoxTranslationY(boxTranslationY); - if (params.width != expectedWidth || params.height != expectedHeight) { - params.width = expectedWidth; - params.height = expectedHeight; - setLayoutParams(params); - } - } - - private float getGridTrans(float endTranslation) { - return Utilities.mapRange(mGridProgress, 0, endTranslation); - } - - private float getNonGridTrans(float endTranslation) { - return endTranslation - getGridTrans(endTranslation); - } - - public boolean isRunningTask() { - if (getRecentsView() == null) { - return false; - } - return this == getRecentsView().getRunningTaskView(); - } - - public boolean isFocusedTask() { - if (getRecentsView() == null) { - return false; - } - return this == getRecentsView().getFocusedTaskView(); - } - - public void setShowScreenshot(boolean showScreenshot) { - mShowScreenshot = showScreenshot; - } - - public boolean showScreenshot() { - if (!isRunningTask()) { - return true; - } - return mShowScreenshot; - } - - public void setOverlayEnabled(boolean overlayEnabled) { - // TODO(b/335606129) Investigate the usage of [TaskOverlay] in the new TaskThumbnailView. - // and if it's still necessary we should support that in the new TTV class. - if (!enableRefactorTaskThumbnail()) { - mTaskThumbnailViewDeprecated.setOverlayEnabled(overlayEnabled); - } - } - - public void initiateSplitSelect(SplitPositionOption splitPositionOption) { - getRecentsView().initiateSplitSelect(this, splitPositionOption.stagePosition, - getLogEventForPosition(splitPositionOption.stagePosition)); - } - - /** - * Set a color tint on the snapshot and supporting views. - */ - public void setColorTint(float amount, int tintColor) { - if (!enableRefactorTaskThumbnail()) { - // TODO(b/334832108) Add scrim to new TTV - mTaskThumbnailViewDeprecated.setDimAlpha(amount); - } - mIconView.setIconColorTint(tintColor, amount); - mDigitalWellBeingToast.setBannerColorTint(tintColor, amount); - } - - - private int getRootViewDisplayId() { - Display display = getRootView().getDisplay(); - return display != null ? display.getDisplayId() : DEFAULT_DISPLAY; - } - - /** - * Sets visibility for the thumbnail and associated elements (DWB banners and action chips). - * IconView is unaffected. - * - * @param taskId is only used when setting visibility to a non-{@link View#VISIBLE} value - */ - void setThumbnailVisibility(int visibility, int taskId) { - for (int i = 0; i < getChildCount(); i++) { - View child = getChildAt(i); - if (child != mIconView) { - child.setVisibility(visibility); - } - } - } - - private View getSnapshotView() { - return enableRefactorTaskThumbnail() ? mTaskThumbnailView : mTaskThumbnailViewDeprecated; - } - - /** - * We update and subsequently draw these in {@link #setFullscreenProgress(float)}. - */ - public static class FullscreenDrawParams implements SafeCloseable { - - private float mCornerRadius; - private float mWindowCornerRadius; - - public float mCurrentDrawnCornerRadius; - - public FullscreenDrawParams(Context context) { - updateCornerRadius(context); - } - - /** Recomputes the start and end corner radius for the given Context. */ - public void updateCornerRadius(Context context) { - mCornerRadius = computeTaskCornerRadius(context); - mWindowCornerRadius = computeWindowCornerRadius(context); - } - - @VisibleForTesting - public float computeTaskCornerRadius(Context context) { - return TaskCornerRadius.get(context); - } - - @VisibleForTesting - public float computeWindowCornerRadius(Context context) { - return QuickStepContract.getWindowCornerRadius(context); - } - - /** - * Sets the progress in range [0, 1] - */ - public void setProgress(float fullscreenProgress, float parentScale, float taskViewScale) { - mCurrentDrawnCornerRadius = - Utilities.mapRange(fullscreenProgress, mCornerRadius, mWindowCornerRadius) - / parentScale / taskViewScale; - } - - @Override - public void close() { - } - } - - /** - * Holder for all Task dependent information. - */ - public class TaskContainer { - private final TaskThumbnailViewDeprecated mThumbnailView; - private final Task mTask; - private final TaskViewIcon mIconView; - /** Defaults to STAGE_POSITION_UNDEFINED if in not a split screen task view */ - private @SplitConfigurationOptions.StagePosition int mStagePosition; - @IdRes - private final int mA11yNodeId; - private final DigitalWellBeingToast mDigitalWellBeingToast; - - public TaskContainer(Task task, TaskThumbnailViewDeprecated thumbnailView, - TaskViewIcon iconView, int stagePosition, - DigitalWellBeingToast digitalWellBeingToast) { - this.mTask = task; - this.mThumbnailView = thumbnailView; - this.mIconView = iconView; - this.mStagePosition = stagePosition; - this.mA11yNodeId = (stagePosition == STAGE_POSITION_BOTTOM_OR_RIGHT) ? - R.id.split_bottomRight_appInfo : R.id.split_topLeft_appInfo; - this.mDigitalWellBeingToast = digitalWellBeingToast; - } - - public TaskThumbnailViewDeprecated getThumbnailView() { - return mThumbnailView; - } - - public Task getTask() { - return mTask; - } - - public WorkspaceItemInfo getItemInfo() { - return TaskView.this.getItemInfo(mTask); - } - - public TaskView getTaskView() { - return TaskView.this; - } - - public TaskViewIcon getIconView() { - return mIconView; - } - - public int getStagePosition() { - return mStagePosition; - } - - void setStagePosition(@SplitConfigurationOptions.StagePosition int stagePosition) { - this.mStagePosition = stagePosition; - } - - public int getA11yNodeId() { - return mA11yNodeId; - } - - public DigitalWellBeingToast getDigitalWellBeingToast() { - return mDigitalWellBeingToast; - } - } -} diff --git a/quickstep/src/com/android/quickstep/views/TaskView.kt b/quickstep/src/com/android/quickstep/views/TaskView.kt new file mode 100644 index 0000000000..d19f09c953 --- /dev/null +++ b/quickstep/src/com/android/quickstep/views/TaskView.kt @@ -0,0 +1,1633 @@ +/* + * Copyright (C) 2017 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 android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.animation.AnimatorSet +import android.animation.ObjectAnimator +import android.annotation.IdRes +import android.app.ActivityOptions +import android.content.Context +import android.content.Intent +import android.graphics.Canvas +import android.graphics.PointF +import android.graphics.Rect +import android.graphics.drawable.Drawable +import android.os.Bundle +import android.util.AttributeSet +import android.util.FloatProperty +import android.util.Log +import android.view.Display +import android.view.MotionEvent +import android.view.View +import android.view.View.OnClickListener +import android.view.ViewGroup +import android.view.ViewStub +import android.view.accessibility.AccessibilityNodeInfo +import android.widget.FrameLayout +import android.widget.Toast +import androidx.annotation.IntDef +import androidx.annotation.VisibleForTesting +import androidx.core.view.children +import androidx.core.view.updateLayoutParams +import com.android.app.animation.Interpolators +import com.android.launcher3.Flags.enableCursorHoverStates +import com.android.launcher3.Flags.enableFocusOutline +import com.android.launcher3.Flags.enableGridOnlyOverview +import com.android.launcher3.Flags.enableOverviewIconMenu +import com.android.launcher3.Flags.enableRefactorTaskThumbnail +import com.android.launcher3.Flags.privateSpaceRestrictAccessibilityDrag +import com.android.launcher3.LauncherSettings +import com.android.launcher3.LauncherState +import com.android.launcher3.R +import com.android.launcher3.Utilities +import com.android.launcher3.config.FeatureFlags.ENABLE_KEYBOARD_QUICK_SWITCH +import com.android.launcher3.logging.StatsLogManager.LauncherEvent +import com.android.launcher3.model.data.ItemInfoWithIcon +import com.android.launcher3.model.data.WorkspaceItemInfo +import com.android.launcher3.pm.UserCache +import com.android.launcher3.testing.TestLogging +import com.android.launcher3.testing.shared.TestProtocol +import com.android.launcher3.util.CancellableTask +import com.android.launcher3.util.Executors +import com.android.launcher3.util.RunnableList +import com.android.launcher3.util.SafeCloseable +import com.android.launcher3.util.SplitConfigurationOptions +import com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT +import com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_UNDEFINED +import com.android.launcher3.util.SplitConfigurationOptions.SplitPositionOption +import com.android.launcher3.util.SplitConfigurationOptions.StagePosition +import com.android.launcher3.util.TraceHelper +import com.android.launcher3.util.TransformingTouchDelegate +import com.android.launcher3.util.ViewPool +import com.android.launcher3.util.rects.set +import com.android.quickstep.RecentsModel +import com.android.quickstep.RemoteAnimationTargets +import com.android.quickstep.TaskAnimationManager +import com.android.quickstep.TaskOverlayFactory +import com.android.quickstep.TaskUtils +import com.android.quickstep.TaskViewUtils +import com.android.quickstep.orientation.RecentsPagedOrientationHandler +import com.android.quickstep.task.thumbnail.TaskThumbnail +import com.android.quickstep.task.thumbnail.TaskThumbnailView +import com.android.quickstep.task.viewmodel.TaskViewData +import com.android.quickstep.util.ActiveGestureErrorDetector +import com.android.quickstep.util.ActiveGestureLog +import com.android.quickstep.util.BorderAnimator +import com.android.quickstep.util.BorderAnimator.Companion.createSimpleBorderAnimator +import com.android.quickstep.util.RecentsOrientedState +import com.android.quickstep.util.TaskCornerRadius +import com.android.quickstep.util.TaskRemovedDuringLaunchListener +import com.android.systemui.shared.recents.model.Task +import com.android.systemui.shared.recents.model.ThumbnailData +import com.android.systemui.shared.system.ActivityManagerWrapper +import com.android.systemui.shared.system.QuickStepContract + +/** A task in the Recents view. */ +open class TaskView +@JvmOverloads +constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0, + defStyleRes: Int = 0, + focusBorderAnimator: BorderAnimator? = null, + hoverBorderAnimator: BorderAnimator? = null +) : FrameLayout(context, attrs), ViewPool.Reusable { + /** + * Used in conjunction with [onTaskListVisibilityChanged], providing more granularity on which + * components of this task require an update + */ + @Retention(AnnotationRetention.SOURCE) + @IntDef(FLAG_UPDATE_ALL, FLAG_UPDATE_ICON, FLAG_UPDATE_THUMBNAIL, FLAG_UPDATE_CORNER_RADIUS) + annotation class TaskDataChanges + + /** Type of task view */ + @Retention(AnnotationRetention.SOURCE) + @IntDef(Type.SINGLE, Type.GROUPED, Type.DESKTOP) + annotation class Type { + companion object { + const val SINGLE = 1 + const val GROUPED = 2 + const val DESKTOP = 3 + } + } + + val taskViewData = TaskViewData() + val taskIds: IntArray + /** Returns a copy of integer array containing taskIds of all tasks in the TaskView. */ + get() = taskContainers.map { it.task.key.id }.toIntArray() + val thumbnailViews: Array + get() = taskContainers.map { it.thumbnailView }.toTypedArray() + val isGridTask: Boolean + /** Returns whether the task is part of overview grid and not being focused. */ + get() = container.deviceProfile.isTablet && !isFocusedTask + val isRunningTask: Boolean + get() = this === recentsView?.runningTaskView + val isFocusedTask: Boolean + get() = this === recentsView?.focusedTaskView + val taskCornerRadius: Float + get() = currentFullscreenParams.cornerRadius + val recentsView: RecentsView<*, *>? + get() = parent as? RecentsView<*, *> + val pagedOrientationHandler: RecentsPagedOrientationHandler + get() = orientedState.orientationHandler + + @get:Deprecated("Use {@link #mTaskContainers} instead.") + val firstTask: Task + /** Returns the first task bound to this TaskView. */ + get() = taskContainers[0].task + @get:Deprecated("Use {@link #mTaskContainers} instead.") + val firstThumbnailView: TaskThumbnailViewDeprecated + /** Returns the first thumbnailView of the TaskView. */ + get() = taskContainers[0].thumbnailView + @get:Deprecated("Use {@link #mTaskContainers} instead.") + val firstIconView: TaskViewIcon + /** Returns the first iconView of the TaskView. */ + get() = taskContainers[0].iconView + protected val currentFullscreenParams = FullscreenDrawParams(context) + protected val container: RecentsViewContainer = + RecentsViewContainer.containerFromContext(context) + @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) + val digitalWellBeingToast = DigitalWellBeingToast(container, this) + protected val mLastTouchDownPosition = PointF() + protected val snapshotView: View + get() = + if (enableRefactorTaskThumbnail()) taskThumbnailView!! else taskThumbnailViewDeprecated + + // Derived view properties + protected val persistentScale: Float + /** + * Returns multiplication of scale that is persistent (e.g. fullscreen and grid), and does + * not change according to a temporary state. + */ + get() = Utilities.mapRange(gridProgress, nonGridScale, 1f) + protected val persistentTranslationX: Float + /** + * Returns addition of translationX that is persistent (e.g. fullscreen and grid), and does + * not change according to a temporary state (e.g. task offset). + */ + get() = + (getNonGridTrans(nonGridTranslationX) + + getGridTrans(this.gridTranslationX) + + getNonGridTrans(nonGridPivotTranslationX)) + protected val persistentTranslationY: Float + /** + * Returns addition of translationY that is persistent (e.g. fullscreen and grid), and does + * not change according to a temporary state (e.g. task offset). + */ + get() = boxTranslationY + getGridTrans(gridTranslationY) + protected val primarySplitTranslationProperty: FloatProperty + get() = + pagedOrientationHandler.getPrimaryValue( + SPLIT_SELECT_TRANSLATION_X, + SPLIT_SELECT_TRANSLATION_Y + ) + protected val secondarySplitTranslationProperty: FloatProperty + get() = + pagedOrientationHandler.getSecondaryValue( + SPLIT_SELECT_TRANSLATION_X, + SPLIT_SELECT_TRANSLATION_Y + ) + protected val primaryDismissTranslationProperty: FloatProperty + get() = + pagedOrientationHandler.getPrimaryValue(DISMISS_TRANSLATION_X, DISMISS_TRANSLATION_Y) + protected val secondaryDismissTranslationProperty: FloatProperty + get() = + pagedOrientationHandler.getSecondaryValue(DISMISS_TRANSLATION_X, DISMISS_TRANSLATION_Y) + protected val primaryTaskOffsetTranslationProperty: FloatProperty + get() = + pagedOrientationHandler.getPrimaryValue( + TASK_OFFSET_TRANSLATION_X, + TASK_OFFSET_TRANSLATION_Y + ) + protected val secondaryTaskOffsetTranslationProperty: FloatProperty + get() = + pagedOrientationHandler.getSecondaryValue( + TASK_OFFSET_TRANSLATION_X, + TASK_OFFSET_TRANSLATION_Y + ) + protected val taskResistanceTranslationProperty: FloatProperty + get() = + pagedOrientationHandler.getSecondaryValue( + TASK_RESISTANCE_TRANSLATION_X, + TASK_RESISTANCE_TRANSLATION_Y + ) + + private val mIconCenterCoordinates = FloatArray(2) + private val mFocusBorderAnimator: BorderAnimator? + private val mHoverBorderAnimator: BorderAnimator? + private val rootViewDisplayId: Int + get() = rootView.display?.displayId ?: Display.DEFAULT_DISPLAY + + /** Returns a list of all TaskContainers in the TaskView. */ + lateinit var taskContainers: List + protected set + lateinit var orientedState: RecentsOrientedState + protected lateinit var taskThumbnailViewDeprecated: TaskThumbnailViewDeprecated + protected lateinit var iconView: TaskViewIcon + /** + * This technically can be a vanilla [android.view.TouchDelegate] class, however that class + * requires setting the touch bounds at construction, so we'd repeatedly be created many + * instances unnecessarily as scrolling occurs, whereas [TransformingTouchDelegate] allows touch + * delegated bounds only to be updated. + */ + private lateinit var iconTouchDelegate: TransformingTouchDelegate + private var taskThumbnailView: TaskThumbnailView? = null + + var taskViewId = -1 + var isEndQuickSwitchCuj = false + + // Various animation progress variables. + // progress: 0 = show icon and no insets; 1 = don't show icon and show full insets. + protected var fullscreenProgress = 0f + set(value) { + field = Utilities.boundToRange(value, 0f, 1f) + onFullscreenProgressChanged(field) + } + // gridProgress 0 = carousel; 1 = 2 row grid. + protected var gridProgress = 0f + set(value) { + field = value + onGridProgressChanged() + } + /** + * The modalness of this view is how it should be displayed when it is shown on its own in the + * modal state of overview. 0 being in context with other tasks, 1 being shown on its own. + */ + protected var modalness = 0f + set(value) { + field = value + onModalnessUpdated(field) + } + protected var taskThumbnailSplashAlpha = 0f + set(value) { + field = value + applyThumbnailSplashAlpha() + } + protected var nonGridScale = 1f + set(value) { + field = value + applyScale() + } + private var dismissScale = 1f + set(value) { + field = value + applyScale() + } + private var dismissTranslationX = 0f + set(value) { + field = value + applyTranslationX() + } + private var dismissTranslationY = 0f + set(value) { + field = value + applyTranslationY() + } + private var taskOffsetTranslationX = 0f + set(value) { + field = value + applyTranslationX() + } + private var taskOffsetTranslationY = 0f + set(value) { + field = value + applyTranslationY() + } + private var taskResistanceTranslationX = 0f + set(value) { + field = value + applyTranslationX() + } + private var taskResistanceTranslationY = 0f + set(value) { + field = value + applyTranslationY() + } + // The following translation variables should only be used in the same orientation as Launcher. + private var boxTranslationY = 0f + set(value) { + field = value + applyTranslationY() + } + // The following grid translations scales with mGridProgress. + protected var gridTranslationX = 0f + set(value) { + field = value + applyTranslationX() + } + var gridTranslationY = 0f + protected set(value) { + field = value + applyTranslationY() + } + // The following grid translation is used to animate closing the gap between grid and clear all. + private var gridEndTranslationX = 0f + set(value) { + field = value + applyTranslationX() + } + // Applied as a complement to gridTranslation, for adjusting the carousel overview and quick + // switch. + protected var nonGridTranslationX = 0f + set(value) { + field = value + applyTranslationX() + } + protected var nonGridPivotTranslationX = 0f + set(value) { + field = value + applyTranslationX() + } + // Used when in SplitScreenSelectState + private var splitSelectTranslationY = 0f + set(value) { + field = value + applyTranslationY() + } + private var splitSelectTranslationX = 0f + set(value) { + field = value + applyTranslationX() + } + protected var stableAlpha = 1f + set(value) { + field = value + alpha = stableAlpha + } + protected var shouldShowScreenshot = false + get() = !isRunningTask || field + /** Enable or disable showing border on hover and focus change */ + @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) + var borderEnabled = false + set(value) { + if (field == value) { + return + } + field = value + // Set the animation correctly in case it misses the hover/focus event during state + // transition + mHoverBorderAnimator?.setBorderVisibility(visible = field && isHovered, animated = true) + mFocusBorderAnimator?.setBorderVisibility(visible = field && isFocused, animated = true) + } + protected var iconScaleAnimStartProgress = 0f + private var focusTransitionProgress = 1f + + private var iconAndDimAnimator: ObjectAnimator? = null + // The current background requests to load the task thumbnail and icon + private var thumbnailLoadRequest: CancellableTask<*>? = null + private var iconLoadRequest: CancellableTask<*>? = null + private var isClickableAsLiveTile = true + + init { + setOnClickListener { view: View -> onClick(view) } + val keyboardFocusHighlightEnabled = + (ENABLE_KEYBOARD_QUICK_SWITCH.get() || enableFocusOutline()) + val cursorHoverStatesEnabled = enableCursorHoverStates() + setWillNotDraw(!keyboardFocusHighlightEnabled && !cursorHoverStatesEnabled) + context.obtainStyledAttributes(attrs, R.styleable.TaskView, defStyleAttr, defStyleRes).use { + mFocusBorderAnimator = + focusBorderAnimator + ?: if (keyboardFocusHighlightEnabled) + createSimpleBorderAnimator( + currentFullscreenParams.cornerRadius.toInt(), + context.resources.getDimensionPixelSize( + R.dimen.keyboard_quick_switch_border_width + ), + { bounds: Rect -> getThumbnailBounds(bounds) }, + this, + it.getColor( + R.styleable.TaskView_focusBorderColor, + BorderAnimator.DEFAULT_BORDER_COLOR + ) + ) + else null + mHoverBorderAnimator = + hoverBorderAnimator + ?: if (cursorHoverStatesEnabled) + createSimpleBorderAnimator( + currentFullscreenParams.cornerRadius.toInt(), + context.resources.getDimensionPixelSize( + R.dimen.task_hover_border_width + ), + { bounds: Rect -> getThumbnailBounds(bounds) }, + this, + it.getColor( + R.styleable.TaskView_hoverBorderColor, + BorderAnimator.DEFAULT_BORDER_COLOR + ) + ) + else null + } + } + + override fun onFinishInflate() { + super.onFinishInflate() + taskThumbnailViewDeprecated = findViewById(R.id.snapshot)!! + if (enableRefactorTaskThumbnail()) { + val indexOfSnapshotView = indexOfChild(taskThumbnailViewDeprecated) + taskThumbnailView = + TaskThumbnailView(mContext).apply { + layoutParams = taskThumbnailViewDeprecated.layoutParams + addView(this, indexOfSnapshotView) + } + taskThumbnailViewDeprecated.visibility = GONE + } + val iconViewStub = + findViewById(R.id.icon)!!.apply { + layoutResource = + if (enableOverviewIconMenu()) R.layout.icon_app_chip_view + else R.layout.icon_view + } + iconView = iconViewStub.inflate() as TaskViewIcon + iconTouchDelegate = TransformingTouchDelegate(iconView.asView()) + } + + @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) + public override fun onFocusChanged( + gainFocus: Boolean, + direction: Int, + previouslyFocusedRect: Rect? + ) { + super.onFocusChanged(gainFocus, direction, previouslyFocusedRect) + if (borderEnabled) { + mFocusBorderAnimator?.setBorderVisibility(gainFocus, /* animated= */ true) + } + } + + override fun onHoverEvent(event: MotionEvent): Boolean { + if (borderEnabled) { + when (event.action) { + MotionEvent.ACTION_HOVER_ENTER -> + mHoverBorderAnimator?.setBorderVisibility(visible = true, animated = true) + MotionEvent.ACTION_HOVER_EXIT -> + mHoverBorderAnimator?.setBorderVisibility(visible = false, animated = true) + else -> {} + } + } + return super.onHoverEvent(event) + } + + // avoid triggering hover event on child elements which would cause HOVER_EXIT for this + // task view + override fun onInterceptHoverEvent(event: MotionEvent) = + if (enableCursorHoverStates()) true else super.onInterceptHoverEvent(event) + + override fun dispatchTouchEvent(ev: MotionEvent): Boolean { + val recentsView = recentsView ?: return false + val splitSelectStateController = recentsView.splitSelectController + // Disable taps for split selection animation unless we have multiple tasks + if ( + splitSelectStateController.isSplitSelectActive && + splitSelectStateController.initialTaskId == firstTask.key.id && + !containsMultipleTasks() + ) { + return false + } + if (ev.action == MotionEvent.ACTION_DOWN) { + mLastTouchDownPosition.apply { + x = ev.x + y = ev.y + } + } + return super.dispatchTouchEvent(ev) + } + + override fun draw(canvas: Canvas) { + // Draw border first so any child views outside of the thumbnail bounds are drawn above it. + mFocusBorderAnimator?.drawBorder(canvas) + mHoverBorderAnimator?.drawBorder(canvas) + super.draw(canvas) + } + + override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) { + super.onLayout(changed, left, top, right, bottom) + val thumbnailTopMargin = container.deviceProfile.overviewTaskThumbnailTopMarginPx + if (container.deviceProfile.isTablet) { + pivotX = (if (layoutDirection == LAYOUT_DIRECTION_RTL) 0 else right - left).toFloat() + pivotY = thumbnailTopMargin.toFloat() + } else { + pivotX = (right - left) * 0.5f + pivotY = thumbnailTopMargin + (height - thumbnailTopMargin) * 0.5f + } + systemGestureExclusionRects = + SYSTEM_GESTURE_EXCLUSION_RECT.onEach { + it.right = width + it.bottom = height + } + } + + override fun onRecycle() { + resetPersistentViewTransforms() + // Clear any references to the thumbnail (it will be re-read either from the cache or the + // system on next bind) + // TODO(b/334825222): Implement thumbnail/snapshot for the new [TaskThumbnailView]. + if (enableRefactorTaskThumbnail()) { + notifyIsRunningTaskUpdated() + } else { + taskThumbnailViewDeprecated.setThumbnail(firstTask, null) + } + setOverlayEnabled(false) + onTaskListVisibilityChanged(false) + borderEnabled = false + } + + // TODO: Clip-out the icon region from the thumbnail, since they are overlapping. + override fun hasOverlappingRendering() = false + + override fun onInitializeAccessibilityNodeInfo(info: AccessibilityNodeInfo) { + super.onInitializeAccessibilityNodeInfo(info) + with(info) { + addAction( + AccessibilityNodeInfo.AccessibilityAction( + R.string.accessibility_close, + context.getText(R.string.accessibility_close) + ) + ) + taskContainers.forEach { + TraceHelper.allowIpcs("TV.a11yInfo") { + TaskOverlayFactory.getEnabledShortcuts(this@TaskView, it).forEach { shortcut -> + addAction(shortcut.createAccessibilityAction(context)) + } + } + } + if (digitalWellBeingToast.hasLimit()) { + addAction( + AccessibilityNodeInfo.AccessibilityAction( + R.string.accessibility_app_usage_settings, + context.getText(R.string.accessibility_app_usage_settings) + ) + ) + } + recentsView?.let { + collectionItemInfo = + AccessibilityNodeInfo.CollectionItemInfo.obtain( + 0, + 1, + it.taskViewCount - it.indexOfChild(this@TaskView) - 1, + 1, + false + ) + } + } + } + + override fun performAccessibilityAction(action: Int, arguments: Bundle?): Boolean { + if (action == R.string.accessibility_close) { + recentsView?.dismissTask(this, true /*animateTaskView*/, true /*removeTask*/) + return true + } + if (action == R.string.accessibility_app_usage_settings) { + digitalWellBeingToast.openAppUsageSettings(this) + return true + } + taskContainers.forEach { + TaskOverlayFactory.getEnabledShortcuts(this, it).forEach { shortcut -> + if (shortcut.hasHandlerForAction(action)) { + shortcut.onClick(this) + return true + } + } + } + return super.performAccessibilityAction(action, arguments) + } + + /** Updates this task view to the given {@param task}. */ + open fun bind( + task: Task, + orientedState: RecentsOrientedState, + taskOverlayFactory: TaskOverlayFactory + ) { + cancelPendingLoadTasks() + setupTaskContainers(task, taskOverlayFactory) + setOrientationState(orientedState) + } + + protected fun setupTaskContainers(task: Task, taskOverlayFactory: TaskOverlayFactory) { + taskContainers = + listOf( + TaskContainer( + task, + taskThumbnailViewDeprecated, + iconView, + STAGE_POSITION_UNDEFINED, + digitalWellBeingToast + ) + ) + if (enableRefactorTaskThumbnail()) { + bindTaskThumbnailView() + } else { + taskThumbnailViewDeprecated.bind(task, taskOverlayFactory) + } + } + + protected fun isTaskContainersInitialized() = this::taskContainers.isInitialized + + fun containsMultipleTasks() = taskContainers.size > 1 + + /** + * Returns the TaskContainer corresponding to a given taskId, or null if the TaskView does not + * contain a Task with that ID. + */ + fun getTaskContainerById(taskId: Int) = taskContainers.firstOrNull { it.task.key.id == taskId } + + /** Check if given `taskId` is tracked in this view */ + fun containsTaskId(taskId: Int) = getTaskContainerById(taskId) != null + + // TODO(b/335649589): TaskView's VM will already have access to TaskThumbnailView's VM + // so there will be no need to access TaskThumbnailView's VM through the TaskThumbnailView + private fun bindTaskThumbnailView() { + taskThumbnailView!!.viewModel.bind(TaskThumbnail(firstTask, isRunningTask)) + } + + open fun setOrientationState(orientationState: RecentsOrientedState) { + this.orientedState = orientationState + iconView.setIconOrientation(orientationState, isGridTask) + setThumbnailOrientation(orientationState) + } + + protected open fun setThumbnailOrientation(orientationState: RecentsOrientedState) { + val thumbnailTopMargin = container.deviceProfile.overviewTaskThumbnailTopMarginPx + + // TODO(b/271468547), we should default to setting translations only on the snapshot instead + // of a hybrid of both margins and translations + snapshotView.updateLayoutParams { topMargin = thumbnailTopMargin } + + // TODO(b/335606129) Investigate the usage of [TaskOverlay] in the new TaskThumbnailView. + // and if it's still necessary we should support that in the new TTV class. + if (!enableRefactorTaskThumbnail()) { + taskThumbnailViewDeprecated.taskOverlay.updateOrientationState(orientationState) + } + digitalWellBeingToast.initialize(firstTask) + } + + /** + * Updates TaskView scaling and translation required to support variable width if enabled, while + * ensuring TaskView fits into screen in fullscreen. + */ + fun updateTaskSize( + lastComputedTaskSize: Rect, + lastComputedGridTaskSize: Rect, + lastComputedCarouselTaskSize: Rect + ) { + val thumbnailPadding = container.deviceProfile.overviewTaskThumbnailTopMarginPx + val taskWidth = lastComputedTaskSize.width() + val taskHeight = lastComputedTaskSize.height() + val nonGridScale: Float + val boxTranslationY: Float + val expectedWidth: Int + val expectedHeight: Int + if (container.deviceProfile.isTablet) { + val boxWidth: Int + val boxHeight: Int + val isFocusedTask = isFocusedTask + if (isFocusedTask) { + // Task will be focused and should use focused task size. Use focusTaskRatio + // that is associated with the original orientation of the focused task. + boxWidth = taskWidth + boxHeight = taskHeight + } else { + // Otherwise task is in grid, and should use lastComputedGridTaskSize. + boxWidth = lastComputedGridTaskSize.width() + boxHeight = lastComputedGridTaskSize.height() + } + + // Bound width/height to the box size. + expectedWidth = boxWidth + expectedHeight = boxHeight + thumbnailPadding + + // Scale to to fit task Rect. + nonGridScale = + if (enableGridOnlyOverview()) { + lastComputedCarouselTaskSize.width() / taskWidth.toFloat() + } else { + taskWidth / boxWidth.toFloat() + } + + // Align to top of task Rect. + boxTranslationY = (expectedHeight - thumbnailPadding - taskHeight) / 2.0f + } else { + nonGridScale = 1f + boxTranslationY = 0f + expectedWidth = if (enableOverviewIconMenu()) taskWidth else LayoutParams.MATCH_PARENT + expectedHeight = + if (enableOverviewIconMenu()) taskHeight + thumbnailPadding + else LayoutParams.MATCH_PARENT + } + this.nonGridScale = nonGridScale + this.boxTranslationY = boxTranslationY + updateLayoutParams { + width = expectedWidth + height = expectedHeight + } + } + + /** Returns the thumbnail's bounds, optionally relative to the screen. */ + @JvmOverloads + open fun getThumbnailBounds(bounds: Rect, relativeToDragLayer: Boolean = false) { + val snapshotView = snapshotView + if (relativeToDragLayer) { + container.dragLayer.getDescendantRectRelativeToSelf(snapshotView, bounds) + } else { + bounds.apply { + set(snapshotView) + offset(Math.round(snapshotView.translationX), Math.round(snapshotView.translationY)) + } + } + } + + /** + * See [TaskDataChanges] + * + * @param visible If this task view will be visible to the user in overview or hidden + */ + fun onTaskListVisibilityChanged(visible: Boolean) { + onTaskListVisibilityChanged(visible, FLAG_UPDATE_ALL) + } + + /** + * See [TaskDataChanges] + * + * @param visible If this task view will be visible to the user in overview or hidden + */ + open fun onTaskListVisibilityChanged(visible: Boolean, @TaskDataChanges changes: Int) { + cancelPendingLoadTasks() + val model = RecentsModel.INSTANCE.get(context) + // These calls are no-ops if the data is already loaded, try and load the high + // resolution thumbnail if the state permits + if (needsUpdate(changes, FLAG_UPDATE_THUMBNAIL)) { + if (visible) { + thumbnailLoadRequest = + model.thumbnailCache.updateThumbnailInBackground(firstTask) { + if (!enableRefactorTaskThumbnail()) { + // TODO(b/334825222) add thumbnail state + taskThumbnailViewDeprecated.setThumbnail(firstTask, it) + } + } + } else { + if (!enableRefactorTaskThumbnail()) { + // TODO(b/334825222) add thumbnail state + taskThumbnailViewDeprecated.setThumbnail(null, null) + } + // Reset the task thumbnail reference as well (it will be fetched from the cache or + // reloaded next time we need it) + firstTask.thumbnail = null + } + } + if (needsUpdate(changes, FLAG_UPDATE_ICON)) { + if (visible) { + iconLoadRequest = + model.iconCache.updateIconInBackground(firstTask) { + setIcon(iconView, it.icon) + if (enableOverviewIconMenu()) { + setText(iconView, it.title) + } + digitalWellBeingToast.initialize(it) + } + } else { + setIcon(iconView, null) + if (enableOverviewIconMenu()) { + setText(iconView, null) + } + } + } + if (needsUpdate(changes, FLAG_UPDATE_CORNER_RADIUS)) { + currentFullscreenParams.updateCornerRadius(context) + } + } + + protected fun needsUpdate(@TaskDataChanges dataChange: Int, @TaskDataChanges flag: Int) = + (dataChange and flag) == flag + + protected open fun cancelPendingLoadTasks() { + thumbnailLoadRequest?.cancel() + thumbnailLoadRequest = null + iconLoadRequest?.cancel() + iconLoadRequest = null + } + + protected fun setIcon(iconView: TaskViewIcon, icon: Drawable?) { + with(iconView) { + if (icon != null) { + setDrawable(icon) + setOnClickListener { + if (!confirmSecondSplitSelectApp()) { + showTaskMenu(this) + } + } + setOnLongClickListener { + requestDisallowInterceptTouchEvent(true) + showTaskMenu(this) + } + } else { + setDrawable(null) + setOnClickListener(null) + setOnLongClickListener(null) + } + } + } + + protected fun setText(iconView: TaskViewIcon, text: CharSequence?) { + iconView.setText(text) + } + + open fun refreshThumbnails(thumbnailDatas: HashMap?) { + if (enableRefactorTaskThumbnail()) { + // TODO(b/334825222) add thumbnail logic + return + } + thumbnailDatas?.get(firstTask.key.id)?.let { + taskThumbnailViewDeprecated.setThumbnail(firstTask, it) + } + ?: { taskThumbnailViewDeprecated.refresh() } + } + + private fun onClick(view: View) { + if (confirmSecondSplitSelectApp()) { + Log.d("b/310064698", "${taskIds.contentToString()} - onClick - split select is active") + return + } + val callbackList = + launchTasks()?.apply { + add { + Log.d("b/310064698", "${taskIds.contentToString()} - onClick - launchCompleted") + } + } + Log.d("b/310064698", "${taskIds.contentToString()} - onClick - callbackList: $callbackList") + container.statsLogManager + .logger() + .withItemInfo(getItemInfo()) + .log(LauncherEvent.LAUNCHER_TASK_LAUNCH_TAP) + } + + /** + * Starts the task associated with this view and animates the startup. + * + * @return CompletionStage to indicate the animation completion or null if the launch failed. + */ + open fun launchTaskAnimated(): RunnableList? { + TestLogging.recordEvent( + TestProtocol.SEQUENCE_MAIN, + "startActivityFromRecentsAsync", + taskIds.contentToString() + ) + val opts = + container.getActivityLaunchOptions(this, null).apply { + options.launchDisplayId = display?.displayId ?: Display.DEFAULT_DISPLAY + } + if ( + ActivityManagerWrapper.getInstance() + .startActivityFromRecents(firstTask.key, opts.options) + ) { + Log.d( + TAG, + "launchTaskAnimated - startActivityFromRecents: ${taskIds.contentToString()}" + ) + ActiveGestureLog.INSTANCE.trackEvent( + ActiveGestureErrorDetector.GestureEvent.EXPECTING_TASK_APPEARED + ) + val recentsView = recentsView ?: return null + if (recentsView.runningTaskViewId != -1) { + recentsView.onTaskLaunchedInLiveTileMode() + + // Return a fresh callback in the live tile case, so that it's not accidentally + // triggered by QuickstepTransitionManager.AppLaunchAnimationRunner. + return RunnableList().also { recentsView.addSideTaskLaunchCallback(it) } + } + if (TaskAnimationManager.ENABLE_SHELL_TRANSITIONS) { + // If the recents transition is running (ie. in live tile mode), then the start + // of a new task will merge into the existing transition and it currently will + // not be run independently, so we need to rely on the onTaskAppeared() call + // for the new task to trigger the side launch callback to flush this runnable + // list (which is usually flushed when the app launch animation finishes) + recentsView.addSideTaskLaunchCallback(opts.onEndCallback) + } + return opts.onEndCallback + } else { + notifyTaskLaunchFailed() + return null + } + } + + /** Starts the task associated with this view without any animation */ + fun launchTask(callback: (launched: Boolean) -> Unit) { + launchTask(callback, isQuickSwitch = false) + } + + /** Starts the task associated with this view without any animation */ + open fun launchTask(callback: (launched: Boolean) -> Unit, isQuickSwitch: Boolean) { + TestLogging.recordEvent( + TestProtocol.SEQUENCE_MAIN, + "startActivityFromRecentsAsync", + taskIds.contentToString() + ) + val failureListener = TaskRemovedDuringLaunchListener(context.applicationContext) + if (isQuickSwitch) { + // We only listen for failures to launch in quickswitch because the during this + // gesture launcher is in the background state, vs other launches which are in + // the actual overview state + failureListener.register(container, firstTask.key.id) { + notifyTaskLaunchFailed() + recentsView?.let { + // Disable animations for now, as it is an edge case and the app usually + // covers launcher and also any state transition animation also gets + // clobbered by QuickstepTransitionManager.createWallpaperOpenAnimations + // when launcher shows again + it.startHome(false /* animated */) + // LauncherTaskbarUIController depends on the launcher state when + // checking whether to handle resume, but that can come in before + // startHome() changes the state, so force-refresh here to ensure the + // taskbar is updated + it.mSizeStrategy.taskbarController?.refreshResumedState() + } + } + } + // Indicate success once the system has indicated that the transition has started + val opts = + ActivityOptions.makeCustomTaskAnimation( + context, + 0, + 0, + Executors.MAIN_EXECUTOR.handler, + { callback(true) } + ) { + failureListener.onTransitionFinished() + } + .apply { + launchDisplayId = display?.displayId ?: Display.DEFAULT_DISPLAY + if (isQuickSwitch) { + setFreezeRecentTasksReordering() + } + // TODO(b/334826842) add splash functionality to new TTV + if (!enableRefactorTaskThumbnail()) { + disableStartingWindow = taskThumbnailViewDeprecated.shouldShowSplashView() + } + } + Executors.UI_HELPER_EXECUTOR.execute { + if ( + !ActivityManagerWrapper.getInstance().startActivityFromRecents(firstTask.key, opts) + ) { + // If the call to start activity failed, then post the result immediately, + // otherwise, wait for the animation start callback from the activity options + // above + Executors.MAIN_EXECUTOR.post { + notifyTaskLaunchFailed() + callback(false) + } + } + Log.d(TAG, "launchTask - startActivityFromRecents: ${taskIds.contentToString()}") + } + } + + /** Launch of the current task (both live and inactive tasks) with an animation. */ + fun launchTasks(): RunnableList? { + val recentsView = recentsView ?: return null + val remoteTargetHandles = recentsView.mRemoteTargetHandles + if (!isRunningTask || remoteTargetHandles == null) { + return launchTaskAnimated() + } + if (!isClickableAsLiveTile) { + Log.e(TAG, "TaskView is not clickable as a live tile; returning to home.") + return null + } + isClickableAsLiveTile = false + val targets = + if (remoteTargetHandles.size == 1) { + remoteTargetHandles[0].transformParams.targetSet + } else { + val apps = + remoteTargetHandles.flatMap { it.transformParams.targetSet.apps.asIterable() } + val wallpapers = + remoteTargetHandles.flatMap { + it.transformParams.targetSet.wallpapers.asIterable() + } + RemoteAnimationTargets( + apps.toTypedArray(), + wallpapers.toTypedArray(), + remoteTargetHandles[0].transformParams.targetSet.nonApps, + remoteTargetHandles[0].transformParams.targetSet.targetMode + ) + } + if (targets == null) { + // If the recents animation is cancelled somehow between the parent if block and + // here, try to launch the task as a non live tile task. + val runnableList = launchTaskAnimated() + if (runnableList == null) { + Log.e( + TAG, + "Recents animation cancelled and cannot launch task as non-live tile" + + "; returning to home" + ) + } + isClickableAsLiveTile = true + return runnableList + } + val runnableList = RunnableList() + AnimatorSet().apply { + TaskViewUtils.composeRecentsLaunchAnimator( + this, + this@TaskView, + targets.apps, + targets.wallpapers, + targets.nonApps, + true /* launcherClosing */, + recentsView.stateManager, + recentsView, + recentsView.depthController + ) + addListener( + object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animator: Animator) { + if (firstTask.key.displayId != rootViewDisplayId) { + launchTaskAnimated() + } + isClickableAsLiveTile = true + runEndCallback() + } + + override fun onAnimationCancel(animation: Animator) { + runEndCallback() + } + + private fun runEndCallback() { + runnableList.executeAllAndDestroy() + } + } + ) + start() + } + Log.d(TAG, "launchTasks - composeRecentsLaunchAnimator: ${taskIds.contentToString()}") + recentsView.onTaskLaunchedInLiveTileMode() + return runnableList + } + + private fun notifyTaskLaunchFailed() { + Log.w( + TAG, + "Failed to launch task (task=${firstTask.key.baseIntent} userId=${firstTask.key.userId})" + ) + Toast.makeText(context, R.string.activity_not_available, Toast.LENGTH_SHORT).show() + } + + fun initiateSplitSelect(splitPositionOption: SplitPositionOption) { + recentsView?.initiateSplitSelect( + this, + splitPositionOption.stagePosition, + SplitConfigurationOptions.getLogEventForPosition(splitPositionOption.stagePosition) + ) + } + + /** + * Returns `true` if user is already in split select mode and this tap was to choose the second + * app. `false` otherwise + */ + protected open fun confirmSecondSplitSelectApp(): Boolean { + val index = getLastSelectedChildTaskIndex() + if (index >= taskContainers.size) { + return false + } + val container = taskContainers[index] + val recentsView = recentsView ?: return false + return recentsView.confirmSplitSelect( + this, + container.task, + container.iconView.drawable, + container.thumbnailView, + container.thumbnailView.thumbnail, /* intent */ + null, /* user */ + null, + container.itemInfo + ) + } + + /** + * Returns the task index of the last selected child task (0 or 1). If we contain multiple tasks + * and this TaskView is used as part of split selection, the selected child task index will be + * that of the remaining task. + */ + protected open fun getLastSelectedChildTaskIndex() = 0 + + private fun showTaskMenu(iconView: TaskViewIcon): Boolean { + val recentsView = recentsView ?: return false + if (!recentsView.canLaunchFullscreenTask()) { + // Don't show menu when selecting second split screen app + return true + } + if (!container.deviceProfile.isTablet && !recentsView.isClearAllHidden) { + recentsView.snapToPage(recentsView.indexOfChild(this)) + return false + } + val menuContainer = taskContainers.firstOrNull { it.iconView === iconView } ?: return false + container.statsLogManager + .logger() + .withItemInfo(menuContainer.itemInfo) + .log(LauncherEvent.LAUNCHER_TASK_ICON_TAP_OR_LONGPRESS) + return showTaskMenuWithContainer(menuContainer) + } + + private fun showTaskMenuWithContainer(menuContainer: TaskContainer): Boolean { + val recentsView = recentsView ?: return false + return if (enableOverviewIconMenu() && menuContainer.iconView is IconAppChipView) { + menuContainer.iconView.revealAnim(/* isRevealing= */ true) + TaskMenuView.showForTask(menuContainer) { + menuContainer.iconView.revealAnim(/* isRevealing= */ false) + } + } else if (container.deviceProfile.isTablet) { + val alignedOptionIndex = + if ( + recentsView.isOnGridBottomRow(menuContainer.taskView) && + container.deviceProfile.isLandscape + ) { + if (enableGridOnlyOverview()) { + // With no focused task, there is less available space below the tasks, so + // align the arrow to the third option in the menu. + 2 + } else { + // Bottom row of landscape grid aligns arrow to second option to avoid + // clipping + 1 + } + } else { + 0 + } + TaskMenuViewWithArrow.showForTask(menuContainer, alignedOptionIndex) + } else { + TaskMenuView.showForTask(menuContainer) + } + } + + @Deprecated("Use {@link #getItemInfo(Task)} instead.") + /** Builds proto for logging. */ + fun getItemInfo() = getItemInfo(firstTask) + + /** Builds proto for logging */ + @VisibleForTesting + fun getItemInfo(task: Task): WorkspaceItemInfo { + return WorkspaceItemInfo().apply { + itemType = LauncherSettings.Favorites.ITEM_TYPE_TASK + container = LauncherSettings.Favorites.CONTAINER_TASKSWITCHER + val componentKey = TaskUtils.getLaunchComponentKeyForTask(task.key) + user = componentKey.user + intent = Intent().setComponent(componentKey.componentName) + title = task.title + recentsView?.let { screenId = it.indexOfChild(this@TaskView) } + if (privateSpaceRestrictAccessibilityDrag()) { + if (UserCache.getInstance(context).getUserInfo(componentKey.user).isPrivate) { + runtimeStatusFlags = runtimeStatusFlags or ItemInfoWithIcon.FLAG_NOT_PINNABLE + } + } + } + } + + /** + * Whether the taskview should take the touch event from parent. Events passed to children that + * might require special handling. + */ + open fun offerTouchToChildren(event: MotionEvent): Boolean { + if (event.action == MotionEvent.ACTION_DOWN) { + computeAndSetIconTouchDelegate(iconView, mIconCenterCoordinates, iconTouchDelegate) + } + return iconTouchDelegate.onTouchEvent(event) + } + + protected fun computeAndSetIconTouchDelegate( + view: TaskViewIcon, + tempCenterCoordinates: FloatArray, + transformingTouchDelegate: TransformingTouchDelegate + ) { + val viewHalfWidth = view.width / 2f + val viewHalfHeight = view.height / 2f + Utilities.getDescendantCoordRelativeToAncestor( + view.asView(), + container.dragLayer, + tempCenterCoordinates.apply { + this[0] = viewHalfWidth + this[1] = viewHalfHeight + }, + false + ) + transformingTouchDelegate.setBounds( + (tempCenterCoordinates[0] - viewHalfWidth).toInt(), + (tempCenterCoordinates[1] - viewHalfHeight).toInt(), + (tempCenterCoordinates[0] + viewHalfWidth).toInt(), + (tempCenterCoordinates[1] + viewHalfHeight).toInt() + ) + } + + /** Sets up an on-click listener and the visibility for show_windows icon on top of the task. */ + open fun setUpShowAllInstancesListener() { + val taskPackageName = taskContainers[0].task.key.packageName + // icon of the top/left task + val showWindowsView = findViewById(R.id.show_windows)!! + updateFilterCallback(showWindowsView, getFilterUpdateCallback(taskPackageName)) + } + + /** + * Returns a callback that updates the state of the filter and the recents overview + * + * @param taskPackageName package name of the task to filter by + */ + protected fun getFilterUpdateCallback(taskPackageName: String?) = + if (recentsView?.filterState?.shouldShowFilterUI(taskPackageName) == true) + OnClickListener { recentsView?.setAndApplyFilter(taskPackageName) } + else null + + /** + * Sets the correct visibility and callback on the provided filterView based on whether the + * callback is null or not + */ + protected fun updateFilterCallback(filterView: View, callback: OnClickListener?) { + // Filtering changes alpha instead of the visibility since visibility + // can be altered separately through RecentsView#resetFromSplitSelectionState() + filterView.alpha = if (callback == null) 0f else 1f + filterView.setOnClickListener(callback) + } + + protected open fun setIconsAndBannersFullscreenProgress(progress: Float) { + // Animate icons and DWB banners in/out, except in QuickSwitch state, when tiles are + // oversized and banner would look disproportionately large. + if (recentsView?.stateManager?.state == LauncherState.BACKGROUND_APP) { + return + } + setIconsAndBannersTransitionProgress(progress, invert = true) + } + + /** + * Called to animate a smooth transition when going directly from an app into Overview (and vice + * versa). Icons fade in, and DWB banners slide in with a "shift up" animation. + */ + protected open fun setIconsAndBannersTransitionProgress(progress: Float, invert: Boolean) { + focusTransitionProgress = if (invert) 1 - progress else progress + getIconContentScale(invert).let { iconContentScale -> + iconView.setContentAlpha(iconContentScale) + digitalWellBeingToast.updateBannerOffset(1f - iconContentScale) + } + } + + protected fun getIconContentScale(invert: Boolean): Float { + val iconScalePercentage = SCALE_ICON_DURATION.toFloat() / DIM_ANIM_DURATION + val lowerClamp = if (invert) 1f - iconScalePercentage else 0f + val upperClamp = if (invert) 1f else iconScalePercentage + return Interpolators.clampToProgress(Interpolators.FAST_OUT_SLOW_IN, lowerClamp, upperClamp) + .getInterpolation(focusTransitionProgress) + } + + fun animateIconScaleAndDimIntoView() { + iconAndDimAnimator?.cancel() + iconAndDimAnimator = + ObjectAnimator.ofFloat(this, FOCUS_TRANSITION, 1f).apply { + setCurrentFraction(iconScaleAnimStartProgress) + setDuration(DIM_ANIM_DURATION).interpolator = Interpolators.LINEAR + addListener( + object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + iconAndDimAnimator = null + } + } + ) + start() + } + } + + fun setIconScaleAndDim(iconScale: Float) { + iconAndDimAnimator?.cancel() + setIconsAndBannersTransitionProgress(iconScale, invert = false) + } + + /** Set a color tint on the snapshot and supporting views. */ + open fun setColorTint(amount: Float, tintColor: Int) { + if (!enableRefactorTaskThumbnail()) { + // TODO(b/334832108) Add scrim to new TTV + taskThumbnailViewDeprecated.setDimAlpha(amount) + } + iconView.setIconColorTint(tintColor, amount) + digitalWellBeingToast.setBannerColorTint(tintColor, amount) + } + + /** + * Sets visibility for the thumbnail and associated elements (DWB banners and action chips). + * IconView is unaffected. + * + * @param taskId is only used when setting visibility to a non-[View.VISIBLE] value + */ + open fun setThumbnailVisibility(visibility: Int, taskId: Int) { + children.filterNot { it === iconView }.forEach { it.visibility = visibility } + } + + open fun setOverlayEnabled(overlayEnabled: Boolean) { + // TODO(b/335606129) Investigate the usage of [TaskOverlay] in the new TaskThumbnailView. + // and if it's still necessary we should support that in the new TTV class. + if (!enableRefactorTaskThumbnail()) { + taskThumbnailViewDeprecated.setOverlayEnabled(overlayEnabled) + } + } + + protected open fun refreshTaskThumbnailSplash() { + if (!enableRefactorTaskThumbnail()) { + // TODO(b/334826842) add splash functionality to new TTV + taskThumbnailViewDeprecated.refreshSplashView() + } + } + + protected fun getScrollAdjustment(gridEnabled: Boolean) = + if (gridEnabled) gridTranslationX else nonGridTranslationX + + protected fun getOffsetAdjustment(gridEnabled: Boolean) = getScrollAdjustment(gridEnabled) + + fun getSizeAdjustment(fullscreenEnabled: Boolean) = if (fullscreenEnabled) nonGridScale else 1f + + private fun applyScale() { + val scale = persistentScale * dismissScale + scaleX = scale + scaleY = scale + if (enableRefactorTaskThumbnail()) { + taskViewData.scale.value = scale + } + updateSnapshotRadius() + } + + protected open fun applyThumbnailSplashAlpha() { + if (!enableRefactorTaskThumbnail()) { + // TODO(b/334826842) add splash functionality to new TTV + taskThumbnailViewDeprecated.setSplashAlpha(taskThumbnailSplashAlpha) + } + } + + private fun applyTranslationX() { + translationX = + dismissTranslationX + + taskOffsetTranslationX + + taskResistanceTranslationX + + splitSelectTranslationX + + gridEndTranslationX + + persistentTranslationX + } + + private fun applyTranslationY() { + translationY = + dismissTranslationY + + taskOffsetTranslationY + + taskResistanceTranslationY + + splitSelectTranslationY + + persistentTranslationY + } + + private fun onGridProgressChanged() { + applyTranslationX() + applyTranslationY() + applyScale() + } + + protected open fun onFullscreenProgressChanged(fullscreenProgress: Float) { + iconView.setVisibility(if (fullscreenProgress < 1) VISIBLE else INVISIBLE) + taskThumbnailViewDeprecated.taskOverlay.setFullscreenProgress(fullscreenProgress) + setIconsAndBannersFullscreenProgress(fullscreenProgress) + updateSnapshotRadius() + } + + protected open fun updateSnapshotRadius() { + updateCurrentFullscreenParams() + taskThumbnailViewDeprecated.setFullscreenParams(currentFullscreenParams) + } + + protected fun updateCurrentFullscreenParams() { + updateFullscreenParams(currentFullscreenParams) + } + + protected fun updateFullscreenParams(fullscreenParams: FullscreenDrawParams) { + recentsView?.let { fullscreenParams.setProgress(fullscreenProgress, it.scaleX, scaleX) } + } + + private fun onModalnessUpdated(modalness: Float) { + iconView.setModalAlpha(1 - modalness) + digitalWellBeingToast.updateBannerOffset(modalness) + } + + /** Updates [TaskThumbnailView] to reflect the latest [Task] state (i.e., task isRunning). */ + fun notifyIsRunningTaskUpdated() { + // TODO(b/335649589): TaskView's VM will already have access to TaskThumbnailView's VM + // so there will be no need to access TaskThumbnailView's VM through the TaskThumbnailView + if (taskContainers.isNotEmpty()) { + bindTaskThumbnailView() + } + } + + fun resetPersistentViewTransforms() { + nonGridTranslationX = 0f + gridTranslationX = 0f + gridTranslationY = 0f + boxTranslationY = 0f + nonGridPivotTranslationX = 0f + resetViewTransforms() + } + + open fun resetViewTransforms() { + // fullscreenTranslation and accumulatedTranslation should not be reset, as + // resetViewTransforms is called during QuickSwitch scrolling. + dismissTranslationX = 0f + taskOffsetTranslationX = 0f + taskResistanceTranslationX = 0f + splitSelectTranslationX = 0f + gridEndTranslationX = 0f + dismissTranslationY = 0f + taskOffsetTranslationY = 0f + taskResistanceTranslationY = 0f + if (recentsView?.isSplitSelectionActive != true) { + splitSelectTranslationY = 0f + } + dismissScale = 1f + translationZ = 0f + alpha = stableAlpha + setIconScaleAndDim(1f) + setColorTint(0f, 0) + if (!enableRefactorTaskThumbnail()) { + // TODO(b/335399428) add split select functionality to new TTV + taskThumbnailViewDeprecated.resetViewTransforms() + } + } + + private fun getGridTrans(endTranslation: Float) = + Utilities.mapRange(gridProgress, 0f, endTranslation) + + private fun getNonGridTrans(endTranslation: Float) = + endTranslation - getGridTrans(endTranslation) + + /** We update and subsequently draw these in [fullscreenProgress]. */ + open class FullscreenDrawParams(context: Context) : SafeCloseable { + var cornerRadius = 0f + private var windowCornerRadius = 0f + var currentDrawnCornerRadius = 0f + + init { + updateCornerRadius(context) + } + + /** Recomputes the start and end corner radius for the given Context. */ + fun updateCornerRadius(context: Context) { + cornerRadius = computeTaskCornerRadius(context) + windowCornerRadius = computeWindowCornerRadius(context) + } + + @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) + open fun computeTaskCornerRadius(context: Context): Float { + return TaskCornerRadius.get(context) + } + + @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) + open fun computeWindowCornerRadius(context: Context): Float { + return QuickStepContract.getWindowCornerRadius(context) + } + + /** Sets the progress in range [0, 1] */ + fun setProgress(fullscreenProgress: Float, parentScale: Float, taskViewScale: Float) { + currentDrawnCornerRadius = + Utilities.mapRange(fullscreenProgress, cornerRadius, windowCornerRadius) / + parentScale / + taskViewScale + } + + override fun close() {} + } + + /** Holder for all Task dependent information. */ + inner class TaskContainer( + val task: Task, + val thumbnailView: TaskThumbnailViewDeprecated, + val iconView: TaskViewIcon, + /** Defaults to STAGE_POSITION_UNDEFINED if in not a split screen task view */ + @field:StagePosition var stagePosition: Int, + val digitalWellBeingToast: DigitalWellBeingToast? + ) { + @IdRes + val a11yNodeId: Int = + if (stagePosition == STAGE_POSITION_BOTTOM_OR_RIGHT) R.id.split_bottomRight_appInfo + else R.id.split_topLeft_appInfo + + val itemInfo: WorkspaceItemInfo + get() = getItemInfo(task) + val taskView: TaskView + get() = this@TaskView + } + + companion object { + private const val TAG = "TaskView" + const val FLAG_UPDATE_ICON = 1 + const val FLAG_UPDATE_THUMBNAIL = FLAG_UPDATE_ICON shl 1 + const val FLAG_UPDATE_CORNER_RADIUS = FLAG_UPDATE_THUMBNAIL shl 1 + const val FLAG_UPDATE_ALL = + (FLAG_UPDATE_ICON or FLAG_UPDATE_THUMBNAIL or FLAG_UPDATE_CORNER_RADIUS) + + /** The maximum amount that a task view can be scrimmed, dimmed or tinted. */ + const val MAX_PAGE_SCRIM_ALPHA = 0.4f + const val SCALE_ICON_DURATION: Long = 120 + private const val DIM_ANIM_DURATION: Long = 700 + private val SYSTEM_GESTURE_EXCLUSION_RECT = listOf(Rect()) + + @JvmField + val FOCUS_TRANSITION: FloatProperty = + object : FloatProperty("focusTransition") { + override fun setValue(taskView: TaskView, v: Float) { + taskView.setIconsAndBannersTransitionProgress(v, false /* invert */) + } + + override fun get(taskView: TaskView) = taskView.focusTransitionProgress + } + private val SPLIT_SELECT_TRANSLATION_X: FloatProperty = + object : FloatProperty("splitSelectTranslationX") { + override fun setValue(taskView: TaskView, v: Float) { + taskView.splitSelectTranslationX = v + } + + override fun get(taskView: TaskView) = taskView.splitSelectTranslationX + } + private val SPLIT_SELECT_TRANSLATION_Y: FloatProperty = + object : FloatProperty("splitSelectTranslationY") { + override fun setValue(taskView: TaskView, v: Float) { + taskView.splitSelectTranslationY = v + } + + override fun get(taskView: TaskView) = taskView.splitSelectTranslationY + } + private val DISMISS_TRANSLATION_X: FloatProperty = + object : FloatProperty("dismissTranslationX") { + override fun setValue(taskView: TaskView, v: Float) { + taskView.dismissTranslationX = v + } + + override fun get(taskView: TaskView) = taskView.dismissTranslationX + } + private val DISMISS_TRANSLATION_Y: FloatProperty = + object : FloatProperty("dismissTranslationY") { + override fun setValue(taskView: TaskView, v: Float) { + taskView.dismissTranslationY = v + } + + override fun get(taskView: TaskView) = taskView.dismissTranslationY + } + private val TASK_OFFSET_TRANSLATION_X: FloatProperty = + object : FloatProperty("taskOffsetTranslationX") { + override fun setValue(taskView: TaskView, v: Float) { + taskView.taskOffsetTranslationX = v + } + + override fun get(taskView: TaskView) = taskView.taskOffsetTranslationX + } + private val TASK_OFFSET_TRANSLATION_Y: FloatProperty = + object : FloatProperty("taskOffsetTranslationY") { + override fun setValue(taskView: TaskView, v: Float) { + taskView.taskOffsetTranslationY = v + } + + override fun get(taskView: TaskView) = taskView.taskOffsetTranslationY + } + private val TASK_RESISTANCE_TRANSLATION_X: FloatProperty = + object : FloatProperty("taskResistanceTranslationX") { + override fun setValue(taskView: TaskView, v: Float) { + taskView.taskResistanceTranslationX = v + } + + override fun get(taskView: TaskView) = taskView.taskResistanceTranslationX + } + private val TASK_RESISTANCE_TRANSLATION_Y: FloatProperty = + object : FloatProperty("taskResistanceTranslationY") { + override fun setValue(taskView: TaskView, v: Float) { + taskView.taskResistanceTranslationY = v + } + + override fun get(taskView: TaskView) = taskView.taskResistanceTranslationY + } + @JvmField + val GRID_END_TRANSLATION_X: FloatProperty = + object : FloatProperty("gridEndTranslationX") { + override fun setValue(taskView: TaskView, v: Float) { + taskView.gridEndTranslationX = v + } + + override fun get(taskView: TaskView) = taskView.gridEndTranslationX + } + @JvmField + val DISMISS_SCALE: FloatProperty = + object : FloatProperty("dismissScale") { + override fun setValue(taskView: TaskView, v: Float) { + taskView.dismissScale = v + } + + override fun get(taskView: TaskView) = taskView.dismissScale + } + } +} diff --git a/quickstep/tests/src/com/android/quickstep/DesktopSystemShortcutTest.kt b/quickstep/tests/src/com/android/quickstep/DesktopSystemShortcutTest.kt index d59aafb5de..044325f706 100644 --- a/quickstep/tests/src/com/android/quickstep/DesktopSystemShortcutTest.kt +++ b/quickstep/tests/src/com/android/quickstep/DesktopSystemShortcutTest.kt @@ -30,7 +30,9 @@ import com.android.launcher3.model.data.WorkspaceItemInfo import com.android.launcher3.uioverrides.QuickstepLauncher import com.android.launcher3.util.SplitConfigurationOptions import com.android.quickstep.views.LauncherRecentsView +import com.android.quickstep.views.TaskThumbnailViewDeprecated import com.android.quickstep.views.TaskView +import com.android.quickstep.views.TaskViewIcon import com.android.systemui.shared.recents.model.Task import com.android.systemui.shared.recents.model.Task.TaskKey import com.android.window.flags.Flags @@ -58,6 +60,8 @@ class DesktopSystemShortcutTest { private val taskView: TaskView = mock() private val workspaceItemInfo: WorkspaceItemInfo = mock() private val abstractFloatingViewHelper: AbstractFloatingViewHelper = mock() + private val thumbnailView: TaskThumbnailViewDeprecated = mock() + private val iconView: TaskViewIcon = mock() private val factory: TaskShortcutFactory = DesktopSystemShortcut.createFactory(abstractFloatingViewHelper) @@ -90,8 +94,8 @@ class DesktopSystemShortcutTest { val taskContainer = taskView.TaskContainer( task, - null, - null, + thumbnailView, + iconView, SplitConfigurationOptions.STAGE_POSITION_UNDEFINED, null ) @@ -112,8 +116,8 @@ class DesktopSystemShortcutTest { val taskContainer = taskView.TaskContainer( task, - null, - null, + thumbnailView, + iconView, SplitConfigurationOptions.STAGE_POSITION_UNDEFINED, null ) @@ -135,8 +139,8 @@ class DesktopSystemShortcutTest { val taskContainer = taskView.TaskContainer( task, - null, - null, + thumbnailView, + iconView, SplitConfigurationOptions.STAGE_POSITION_UNDEFINED, null ) @@ -156,8 +160,8 @@ class DesktopSystemShortcutTest { val taskContainer = taskView.TaskContainer( task, - null, - null, + thumbnailView, + iconView, SplitConfigurationOptions.STAGE_POSITION_UNDEFINED, null ) @@ -177,8 +181,8 @@ class DesktopSystemShortcutTest { val taskContainer = taskView.TaskContainer( task, - null, - null, + thumbnailView, + iconView, SplitConfigurationOptions.STAGE_POSITION_UNDEFINED, null ) diff --git a/quickstep/tests/src/com/android/quickstep/FullscreenDrawParamsTest.kt b/quickstep/tests/src/com/android/quickstep/FullscreenDrawParamsTest.kt index db06b6bd7d..5d62a4c749 100644 --- a/quickstep/tests/src/com/android/quickstep/FullscreenDrawParamsTest.kt +++ b/quickstep/tests/src/com/android/quickstep/FullscreenDrawParamsTest.kt @@ -53,7 +53,7 @@ class FullscreenDrawParamsTest : FakeInvariantDeviceProfileTest() { ) val expectedRadius = TaskCornerRadius.get(context) - assertThat(params.mCurrentDrawnCornerRadius).isEqualTo(expectedRadius) + assertThat(params.currentDrawnCornerRadius).isEqualTo(expectedRadius) } @Test @@ -67,7 +67,7 @@ class FullscreenDrawParamsTest : FakeInvariantDeviceProfileTest() { ) val expectedRadius = QuickStepContract.getWindowCornerRadius(context) - assertThat(params.mCurrentDrawnCornerRadius).isEqualTo(expectedRadius) + assertThat(params.currentDrawnCornerRadius).isEqualTo(expectedRadius) } @Test @@ -81,7 +81,7 @@ class FullscreenDrawParamsTest : FakeInvariantDeviceProfileTest() { ) val expectedRadius = TaskCornerRadius.get(context) - assertThat(params.mCurrentDrawnCornerRadius).isEqualTo(expectedRadius) + assertThat(params.currentDrawnCornerRadius).isEqualTo(expectedRadius) } @Test @@ -95,7 +95,7 @@ class FullscreenDrawParamsTest : FakeInvariantDeviceProfileTest() { ) val expectedRadius = QuickStepContract.getWindowCornerRadius(context) - assertThat(params.mCurrentDrawnCornerRadius).isEqualTo(expectedRadius) + assertThat(params.currentDrawnCornerRadius).isEqualTo(expectedRadius) } @Test @@ -117,7 +117,7 @@ class FullscreenDrawParamsTest : FakeInvariantDeviceProfileTest() { /* parentScale= */ 1.0f, /* taskViewScale= */ 1.0f ) - assertThat(spyParams.mCurrentDrawnCornerRadius).isEqualTo(display1TaskRadius) + assertThat(spyParams.currentDrawnCornerRadius).isEqualTo(display1TaskRadius) spyParams.updateCornerRadius(display2Context) spyParams.setProgress( @@ -125,7 +125,7 @@ class FullscreenDrawParamsTest : FakeInvariantDeviceProfileTest() { /* parentScale= */ 1.0f, /* taskViewScale= */ 1.0f ) - assertThat(spyParams.mCurrentDrawnCornerRadius).isEqualTo(display2TaskRadius) + assertThat(spyParams.currentDrawnCornerRadius).isEqualTo(display2TaskRadius) } @Test @@ -147,7 +147,7 @@ class FullscreenDrawParamsTest : FakeInvariantDeviceProfileTest() { /* parentScale= */ 1.0f, /* taskViewScale= */ 1.0f ) - assertThat(spyParams.mCurrentDrawnCornerRadius).isEqualTo(display1WindowRadius) + assertThat(spyParams.currentDrawnCornerRadius).isEqualTo(display1WindowRadius) spyParams.updateCornerRadius(display2Context) spyParams.setProgress( @@ -155,6 +155,6 @@ class FullscreenDrawParamsTest : FakeInvariantDeviceProfileTest() { /* parentScale= */ 1.0f, /* taskViewScale= */ 1.0f, ) - assertThat(spyParams.mCurrentDrawnCornerRadius).isEqualTo(display2WindowRadius) + assertThat(spyParams.currentDrawnCornerRadius).isEqualTo(display2WindowRadius) } } diff --git a/src/com/android/launcher3/util/rects/Rects.kt b/src/com/android/launcher3/util/rects/Rects.kt new file mode 100644 index 0000000000..ac1f3f85f2 --- /dev/null +++ b/src/com/android/launcher3/util/rects/Rects.kt @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2024 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.launcher3.util.rects + +import android.graphics.Rect +import android.view.View + +/** Copy the coordinates of the [view] relative to its parent into this rectangle. */ +fun Rect.set(view: View) { + set(view.left, view.top, view.right, view.bottom) +} diff --git a/tests/src/com/android/launcher3/FakeInvariantDeviceProfileTest.kt b/tests/src/com/android/launcher3/FakeInvariantDeviceProfileTest.kt index 13d749931a..0538870132 100644 --- a/tests/src/com/android/launcher3/FakeInvariantDeviceProfileTest.kt +++ b/tests/src/com/android/launcher3/FakeInvariantDeviceProfileTest.kt @@ -46,7 +46,7 @@ import org.mockito.kotlin.whenever @IgnoreLimit(ignoreLimit = BuildConfig.IS_STUDIO_BUILD) abstract class FakeInvariantDeviceProfileTest { - protected var context: Context? = null + protected lateinit var context: Context protected var inv: InvariantDeviceProfile? = null protected val info: Info = mock() protected var windowBounds: WindowBounds? = null @@ -257,10 +257,10 @@ abstract class FakeInvariantDeviceProfileTest { } protected fun initializeVarsForTwoPanel( - isLandscape: Boolean = false, - isGestureMode: Boolean = true, - rows: Int = 4, - cols: Int = 4, + isLandscape: Boolean = false, + isGestureMode: Boolean = true, + rows: Int = 4, + cols: Int = 4, ) { val (x, y) = if (isLandscape) Pair(2208, 1840) else Pair(1840, 2208)