Snap for 12313714 from fae91d5e57 to 24Q4-release

Change-Id: I1d508a219251bd568de38361a51367668e433b44
This commit is contained in:
Android Build Coastguard Worker
2024-09-03 23:21:02 +00:00
20 changed files with 414 additions and 317 deletions
+1
View File
@@ -52,6 +52,7 @@ filegroup {
"tests/src/com/android/quickstep/TaplOverviewIconTest.java",
"tests/src/com/android/quickstep/TaplTestsQuickstep.java",
"tests/src/com/android/quickstep/TaplTestsSplitscreen.java",
"tests/src/com/android/quickstep/util/SplitScreenTestUtils.kt",
"tests/src/com/android/launcher3/testcomponent/ExcludeFromRecentsTestActivity.java",
],
}
@@ -14,7 +14,7 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
<TextView
<com.android.quickstep.views.DigitalWellBeingToast
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
style="@style/TextTitle"
+4
View File
@@ -47,4 +47,8 @@
android:inflatedId="@id/icon"
android:layout_height="wrap_content"
android:layout_width="wrap_content" />
<include layout="@layout/digital_wellbeing_toast"
android:id="@+id/digital_wellbeing_toast"
android:visibility="invisible"/>
</com.android.quickstep.views.TaskView>
+8
View File
@@ -73,4 +73,12 @@
android:inflatedId="@id/bottomRight_icon"
android:layout_height="wrap_content"
android:layout_width="wrap_content" />
<include layout="@layout/digital_wellbeing_toast"
android:id="@+id/digital_wellbeing_toast"
android:visibility="invisible"/>
<include layout="@layout/digital_wellbeing_toast"
android:id="@+id/bottomRight_digital_wellbeing_toast"
android:visibility="invisible"/>
</com.android.quickstep.views.GroupedTaskView>
@@ -152,12 +152,12 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba
/**
* How long to delay the icon/stash handle alpha.
*/
private static final long TASKBAR_STASH_ALPHA_START_DELAY = 33;
public static final long TASKBAR_STASH_ALPHA_START_DELAY = 33;
/**
* How long the icon/stash handle alpha animation plays.
*/
private static final long TASKBAR_STASH_ALPHA_DURATION = 50;
public static final long TASKBAR_STASH_ALPHA_DURATION = 50;
/**
* How long to delay the icon/stash handle alpha for the home to app taskbar animation.
@@ -179,9 +179,6 @@ interface BubbleStashController {
/** How long to stash/unstash. */
const val BAR_STASH_DURATION = InsetsController.ANIMATION_DURATION_RESIZE.toLong()
const val BAR_STASH_ALPHA_DURATION = 50L
const val BAR_STASH_ALPHA_DELAY = 33L
/** How long to translate Y coordinate of the BubbleBar. */
const val BAR_TRANSLATION_DURATION = 300L
}
@@ -31,10 +31,10 @@ import com.android.launcher3.R
import com.android.launcher3.anim.AnimatedFloat
import com.android.launcher3.anim.SpringAnimationBuilder
import com.android.launcher3.taskbar.TaskbarInsetsController
import com.android.launcher3.taskbar.TaskbarStashController.TASKBAR_STASH_ALPHA_DURATION
import com.android.launcher3.taskbar.TaskbarStashController.TASKBAR_STASH_ALPHA_START_DELAY
import com.android.launcher3.taskbar.bubbles.BubbleBarViewController
import com.android.launcher3.taskbar.bubbles.BubbleStashedHandleViewController
import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController.Companion.BAR_STASH_ALPHA_DELAY
import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController.Companion.BAR_STASH_ALPHA_DURATION
import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController.Companion.BAR_STASH_DURATION
import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController.Companion.BAR_TRANSLATION_DURATION
import com.android.launcher3.taskbar.bubbles.stashing.BubbleStashController.ControllersAfterInitAction
@@ -285,8 +285,8 @@ class TransientBubbleStashController(
private fun createStashAnimator(isStashed: Boolean, duration: Long): AnimatorSet {
val animatorSet = AnimatorSet()
val alphaDuration = if (isStashed) duration else BAR_STASH_ALPHA_DURATION
val alphaDelay = if (isStashed) BAR_STASH_ALPHA_DELAY else 0L
val alphaDuration = if (isStashed) duration else TASKBAR_STASH_ALPHA_DURATION
val alphaDelay = if (isStashed) TASKBAR_STASH_ALPHA_START_DELAY else 0L
animatorSet.play(
createStashAlphaAnimator(isStashed).apply {
this.duration = max(0L, alphaDuration - alphaDelay)
@@ -37,6 +37,7 @@ import android.widget.LinearLayout
import androidx.annotation.VisibleForTesting
import androidx.core.util.component1
import androidx.core.util.component2
import androidx.core.view.updateLayoutParams
import com.android.launcher3.DeviceProfile
import com.android.launcher3.Flags
import com.android.launcher3.LauncherAnimUtils
@@ -242,7 +243,30 @@ open class LandscapePagedViewHandler : RecentsPagedOrientationHandler {
lp.height = ViewGroup.LayoutParams.WRAP_CONTENT
}
override fun getDwbLayoutTranslations(
override fun updateDwbBannerLayout(
taskViewWidth: Int,
taskViewHeight: Int,
isGroupedTaskView: Boolean,
deviceProfile: DeviceProfile,
snapshotViewWidth: Int,
snapshotViewHeight: Int,
banner: View
) {
banner.pivotX = 0f
banner.pivotY = 0f
banner.rotation = degreesRotated
banner.updateLayoutParams<FrameLayout.LayoutParams> {
gravity = Gravity.TOP or if (banner.isLayoutRtl) Gravity.END else Gravity.START
width =
if (isGroupedTaskView) {
snapshotViewHeight
} else {
taskViewHeight - deviceProfile.overviewTaskThumbnailTopMarginPx
}
}
}
override fun getDwbBannerTranslations(
taskViewWidth: Int,
taskViewHeight: Int,
splitBounds: SplitBounds?,
@@ -252,39 +276,25 @@ open class LandscapePagedViewHandler : RecentsPagedOrientationHandler {
banner: View
): Pair<Float, Float> {
val snapshotParams = thumbnailViews[0].layoutParams as FrameLayout.LayoutParams
val isRtl = banner.layoutDirection == View.LAYOUT_DIRECTION_RTL
val translationX = banner.height.toFloat()
val bannerParams = banner.layoutParams as FrameLayout.LayoutParams
bannerParams.gravity = Gravity.TOP or if (isRtl) Gravity.END else Gravity.START
banner.pivotX = 0f
banner.pivotY = 0f
banner.rotation = degreesRotated
if (splitBounds == null) {
// Single, fullscreen case
bannerParams.width = taskViewHeight - snapshotParams.topMargin
return Pair(translationX, snapshotParams.topMargin.toFloat())
}
// Set correct width and translations
val translationY: Float
if (desiredTaskId == splitBounds.leftTopTaskId) {
bannerParams.width = thumbnailViews[0].measuredHeight
if (splitBounds == null) {
translationY = snapshotParams.topMargin.toFloat()
} else {
bannerParams.width = thumbnailViews[1].measuredHeight
val topLeftTaskPlusDividerPercent =
if (splitBounds.appsStackedVertically) {
splitBounds.topTaskPercent + splitBounds.dividerHeightPercent
} else {
splitBounds.leftTaskPercent + splitBounds.dividerWidthPercent
}
translationY =
snapshotParams.topMargin +
(taskViewHeight - snapshotParams.topMargin) * topLeftTaskPlusDividerPercent
if (desiredTaskId == splitBounds.leftTopTaskId) {
translationY = snapshotParams.topMargin.toFloat()
} else {
val topLeftTaskPlusDividerPercent =
if (splitBounds.appsStackedVertically) {
splitBounds.topTaskPercent + splitBounds.dividerHeightPercent
} else {
splitBounds.leftTaskPercent + splitBounds.dividerWidthPercent
}
translationY =
snapshotParams.topMargin +
(taskViewHeight - snapshotParams.topMargin) * topLeftTaskPlusDividerPercent
}
}
return Pair(translationX, translationY)
}
@@ -300,6 +310,7 @@ open class LandscapePagedViewHandler : RecentsPagedOrientationHandler {
if (isRtl) displacement < 0 else displacement > 0
override fun getTaskDragDisplacementFactor(isRtl: Boolean): Int = if (isRtl) 1 else -1
/* -------------------- */
override fun getChildBounds(
@@ -243,54 +243,54 @@ public class PortraitPagedViewHandler extends DefaultPagedViewHandler implements
}
@Override
public Pair<Float, Float> getDwbLayoutTranslations(int taskViewWidth,
int taskViewHeight, SplitBounds splitBounds, DeviceProfile deviceProfile,
View[] thumbnailViews, int desiredTaskId, View banner) {
float translationX = 0;
float translationY = 0;
public void updateDwbBannerLayout(int taskViewWidth, int taskViewHeight,
boolean isGroupedTaskView, @NonNull DeviceProfile deviceProfile,
int snapshotViewWidth, int snapshotViewHeight, @NonNull View banner) {
FrameLayout.LayoutParams bannerParams = (FrameLayout.LayoutParams) banner.getLayoutParams();
banner.setPivotX(0);
banner.setPivotY(0);
banner.setRotation(getDegreesRotated());
if (splitBounds == null) {
// Single, fullscreen case
if (isGroupedTaskView) {
bannerParams.gravity =
BOTTOM | (deviceProfile.isLeftRightSplit ? START : CENTER_HORIZONTAL);
bannerParams.width = snapshotViewWidth;
} else {
bannerParams.width = MATCH_PARENT;
bannerParams.gravity = BOTTOM | CENTER_HORIZONTAL;
return new Pair<>(translationX, translationY);
}
banner.setLayoutParams(bannerParams);
}
bannerParams.gravity =
BOTTOM | (deviceProfile.isLeftRightSplit ? START : CENTER_HORIZONTAL);
// Set correct width
if (desiredTaskId == splitBounds.leftTopTaskId) {
bannerParams.width = thumbnailViews[0].getMeasuredWidth();
} else {
bannerParams.width = thumbnailViews[1].getMeasuredWidth();
}
// Set translations
if (deviceProfile.isLeftRightSplit) {
if (desiredTaskId == splitBounds.rightBottomTaskId) {
float leftTopTaskPercent = splitBounds.appsStackedVertically
? splitBounds.topTaskPercent
: splitBounds.leftTaskPercent;
float dividerThicknessPercent = splitBounds.appsStackedVertically
? splitBounds.dividerHeightPercent
: splitBounds.dividerWidthPercent;
translationX = ((taskViewWidth * leftTopTaskPercent)
+ (taskViewWidth * dividerThicknessPercent));
}
} else {
if (desiredTaskId == splitBounds.leftTopTaskId) {
FrameLayout.LayoutParams snapshotParams =
(FrameLayout.LayoutParams) thumbnailViews[0]
.getLayoutParams();
float bottomRightTaskPlusDividerPercent = splitBounds.appsStackedVertically
? (1f - splitBounds.topTaskPercent)
: (1f - splitBounds.leftTaskPercent);
translationY = -((taskViewHeight - snapshotParams.topMargin)
* bottomRightTaskPlusDividerPercent);
@NonNull
@Override
public Pair<Float, Float> getDwbBannerTranslations(int taskViewWidth,
int taskViewHeight, SplitBounds splitBounds, @NonNull DeviceProfile deviceProfile,
@NonNull View[] thumbnailViews, int desiredTaskId, @NonNull View banner) {
float translationX = 0;
float translationY = 0;
if (splitBounds != null) {
if (deviceProfile.isLeftRightSplit) {
if (desiredTaskId == splitBounds.rightBottomTaskId) {
float leftTopTaskPercent = splitBounds.appsStackedVertically
? splitBounds.topTaskPercent
: splitBounds.leftTaskPercent;
float dividerThicknessPercent = splitBounds.appsStackedVertically
? splitBounds.dividerHeightPercent
: splitBounds.dividerWidthPercent;
translationX = ((taskViewWidth * leftTopTaskPercent)
+ (taskViewWidth * dividerThicknessPercent));
}
} else {
if (desiredTaskId == splitBounds.leftTopTaskId) {
FrameLayout.LayoutParams snapshotParams =
(FrameLayout.LayoutParams) thumbnailViews[0]
.getLayoutParams();
float bottomRightTaskPlusDividerPercent = splitBounds.appsStackedVertically
? (1f - splitBounds.topTaskPercent)
: (1f - splitBounds.leftTaskPercent);
translationY = -((taskViewHeight - snapshotParams.topMargin)
* bottomRightTaskPlusDividerPercent);
}
}
}
return new Pair<>(translationX, translationY);
@@ -199,6 +199,7 @@ interface RecentsPagedOrientationHandler : PagedOrientationHandler {
parentWidth: Int,
parentHeight: Int
): Pair<Point, Point>
// Overview TaskMenuView methods
/** Sets layout params on a task's app icon. Only use this when app chip is disabled. */
fun setTaskIconParams(
@@ -294,13 +295,24 @@ interface RecentsPagedOrientationHandler : PagedOrientationHandler {
deviceProfile: DeviceProfile
)
/** Layout a Digital Wellbeing Banner on its parent. TaskView. */
fun updateDwbBannerLayout(
taskViewWidth: Int,
taskViewHeight: Int,
isGroupedTaskView: Boolean,
deviceProfile: DeviceProfile,
snapshotViewWidth: Int,
snapshotViewHeight: Int,
banner: View
)
/**
* Calculates the position where a Digital Wellbeing Banner should be placed on its parent
* Calculates the translations where a Digital Wellbeing Banner should be apply on its parent
* TaskView.
*
* @return A Pair of Floats representing the proper x and y translations.
*/
fun getDwbLayoutTranslations(
fun getDwbBannerTranslations(
taskViewWidth: Int,
taskViewHeight: Int,
splitBounds: SplitConfigurationOptions.SplitBounds?,
@@ -309,6 +321,7 @@ interface RecentsPagedOrientationHandler : PagedOrientationHandler {
desiredTaskId: Int,
banner: View
): Pair<Float, Float>
// The following are only used by TaskViewTouchHandler.
/** @return Either VERTICAL or HORIZONTAL. */
@@ -28,6 +28,7 @@ import android.view.View.MeasureSpec
import android.widget.FrameLayout
import androidx.core.util.component1
import androidx.core.util.component2
import androidx.core.view.updateLayoutParams
import com.android.launcher3.DeviceProfile
import com.android.launcher3.Flags
import com.android.launcher3.R
@@ -125,7 +126,30 @@ class SeascapePagedViewHandler : LandscapePagedViewHandler() {
}
}
override fun getDwbLayoutTranslations(
override fun updateDwbBannerLayout(
taskViewWidth: Int,
taskViewHeight: Int,
isGroupedTaskView: Boolean,
deviceProfile: DeviceProfile,
snapshotViewWidth: Int,
snapshotViewHeight: Int,
banner: View
) {
banner.pivotX = 0f
banner.pivotY = 0f
banner.rotation = degreesRotated
banner.updateLayoutParams<FrameLayout.LayoutParams> {
gravity = Gravity.BOTTOM or if (banner.isLayoutRtl) Gravity.END else Gravity.START
width =
if (isGroupedTaskView) {
snapshotViewHeight
} else {
taskViewHeight - deviceProfile.overviewTaskThumbnailTopMarginPx
}
}
}
override fun getDwbBannerTranslations(
taskViewWidth: Int,
taskViewHeight: Int,
splitBounds: SplitBounds?,
@@ -135,39 +159,26 @@ class SeascapePagedViewHandler : LandscapePagedViewHandler() {
banner: View
): Pair<Float, Float> {
val snapshotParams = thumbnailViews[0].layoutParams as FrameLayout.LayoutParams
val isRtl = banner.layoutDirection == View.LAYOUT_DIRECTION_RTL
val bannerParams = banner.layoutParams as FrameLayout.LayoutParams
bannerParams.gravity = Gravity.BOTTOM or if (isRtl) Gravity.END else Gravity.START
banner.pivotX = 0f
banner.pivotY = 0f
banner.rotation = degreesRotated
val translationX: Float = (taskViewWidth - banner.height).toFloat()
if (splitBounds == null) {
// Single, fullscreen case
bannerParams.width = taskViewHeight - snapshotParams.topMargin
return Pair(translationX, banner.height.toFloat())
}
// Set correct width and translations
val translationY: Float
if (desiredTaskId == splitBounds.leftTopTaskId) {
bannerParams.width = thumbnailViews[0].measuredHeight
val bottomRightTaskPlusDividerPercent =
if (splitBounds.appsStackedVertically) {
1f - splitBounds.topTaskPercent
} else {
1f - splitBounds.leftTaskPercent
}
translationY =
banner.height -
(taskViewHeight - snapshotParams.topMargin) * bottomRightTaskPlusDividerPercent
} else {
bannerParams.width = thumbnailViews[1].measuredHeight
if (splitBounds == null) {
translationY = banner.height.toFloat()
} else {
if (desiredTaskId == splitBounds.leftTopTaskId) {
val bottomRightTaskPlusDividerPercent =
if (splitBounds.appsStackedVertically) {
1f - splitBounds.topTaskPercent
} else {
1f - splitBounds.leftTaskPercent
}
translationY =
banner.height -
(taskViewHeight - snapshotParams.topMargin) *
bottomRightTaskPlusDividerPercent
} else {
translationY = banner.height.toFloat()
}
}
return Pair(translationX, translationY)
}
@@ -339,6 +350,7 @@ class SeascapePagedViewHandler : LandscapePagedViewHandler() {
if (isRtl) displacement > 0 else displacement < 0
override fun getTaskDragDisplacementFactor(isRtl: Boolean): Int = if (isRtl) -1 else 1
/* -------------------- */
override fun getSplitIconsPosition(
@@ -15,8 +15,10 @@
*/
package com.android.quickstep.views
import android.annotation.SuppressLint
import android.app.ActivityOptions
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.content.pm.LauncherApps
import android.content.pm.LauncherApps.AppUsageLimit
@@ -27,46 +29,58 @@ import android.icu.util.Measure
import android.icu.util.MeasureUnit
import android.os.UserHandle
import android.provider.Settings
import android.util.AttributeSet
import android.util.Log
import android.view.View
import android.view.ViewGroup.MarginLayoutParams
import android.view.ViewOutlineProvider
import android.view.accessibility.AccessibilityNodeInfo
import android.widget.FrameLayout
import android.widget.TextView
import androidx.annotation.StringRes
import androidx.annotation.VisibleForTesting
import androidx.core.util.component1
import androidx.core.util.component2
import androidx.core.view.updateLayoutParams
import com.android.launcher3.R
import com.android.launcher3.Utilities
import com.android.launcher3.util.Executors
import com.android.launcher3.util.SplitConfigurationOptions
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.SplitConfigurationOptions.StagePosition
import com.android.quickstep.TaskUtils
import com.android.systemui.shared.recents.model.Task
import java.time.Duration
import java.util.Locale
class DigitalWellBeingToast(
private val container: RecentsViewContainer,
private val taskView: TaskView
) {
private val launcherApps: LauncherApps? =
container.asContext().getSystemService(LauncherApps::class.java)
@SuppressLint("AppCompatCustomView")
class DigitalWellBeingToast
@JvmOverloads
constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0,
defStyleRes: Int = 0
) : TextView(context, attrs, defStyleAttr, defStyleRes) {
private val recentsViewContainer =
RecentsViewContainer.containerFromContext<RecentsViewContainer>(context)
private val launcherApps: LauncherApps? = context.getSystemService(LauncherApps::class.java)
private val bannerHeight =
container
.asContext()
.resources
.getDimensionPixelSize(R.dimen.digital_wellbeing_toast_height)
context.resources.getDimensionPixelSize(R.dimen.digital_wellbeing_toast_height)
private lateinit var task: Task
private lateinit var taskView: TaskView
private lateinit var snapshotView: View
@StagePosition private var stagePosition = STAGE_POSITION_UNDEFINED
private var appRemainingTimeMs: Long = 0
private var banner: View? = null
private var oldBannerOutlineProvider: ViewOutlineProvider? = null
private var splitOffsetTranslationY = 0f
private var splitOffsetTranslationX = 0f
set(value) {
if (field != value) {
field = value
updateTranslationY()
}
}
private var isDestroyed = false
@@ -76,65 +90,63 @@ class DigitalWellBeingToast(
set(value) {
if (field != value) {
field = value
banner?.let {
updateTranslationY()
it.invalidateOutline()
}
updateTranslationY()
}
}
init {
setOnClickListener(::openAppUsageSettings)
outlineProvider =
object : ViewOutlineProvider() {
override fun getOutline(view: View, outline: Outline) {
BACKGROUND.getOutline(view, outline)
val verticalTranslation = splitOffsetTranslationY - translationY
outline.offset(0, Math.round(verticalTranslation))
}
}
clipToOutline = true
}
private fun setNoLimit() {
hasLimit = false
taskView.contentDescription = task.titleDescription
replaceBanner(null)
setContentDescription(appUsageLimitTimeMs = -1, appRemainingTimeMs = -1)
visibility = INVISIBLE
appRemainingTimeMs = -1
}
private fun setLimit(appUsageLimitTimeMs: Long, appRemainingTimeMs: Long) {
this.appRemainingTimeMs = appRemainingTimeMs
hasLimit = true
val toast =
container.viewCache
.getView<TextView>(
R.layout.digital_wellbeing_toast,
container.asContext(),
taskView
)
.apply {
text =
Utilities.prefixTextWithIcon(
container.asContext(),
R.drawable.ic_hourglass_top,
getBannerText()
)
setOnClickListener(::openAppUsageSettings)
}
replaceBanner(toast)
taskView.contentDescription =
getContentDescriptionForTask(task, appUsageLimitTimeMs, appRemainingTimeMs)
text = Utilities.prefixTextWithIcon(context, R.drawable.ic_hourglass_top, getBannerText())
visibility = VISIBLE
setContentDescription(appUsageLimitTimeMs, appRemainingTimeMs)
}
fun initialize(task: Task) {
private fun setContentDescription(appUsageLimitTimeMs: Long, appRemainingTimeMs: Long) {
val contentDescription =
getContentDescriptionForTask(task, appUsageLimitTimeMs, appRemainingTimeMs)
snapshotView.contentDescription = contentDescription
}
fun initialize() {
check(!isDestroyed) { "Cannot re-initialize a destroyed toast" }
this.task = task
setupTranslations()
Executors.ORDERED_BG_EXECUTOR.execute {
var usageLimit: AppUsageLimit? = null
try {
usageLimit =
launcherApps?.getAppUsageLimit(
this.task.topComponent.packageName,
UserHandle.of(this.task.key.userId)
task.topComponent.packageName,
UserHandle.of(task.key.userId)
)
} catch (e: Exception) {
Log.e(TAG, "Error initializing digital well being toast", e)
}
val appUsageLimitTimeMs = usageLimit?.totalUsageLimit ?: -1
val appRemainingTimeMs = usageLimit?.usageRemaining ?: -1
taskView.post {
if (isDestroyed) {
return@post
}
if (isDestroyed) return@post
if (appUsageLimitTimeMs < 0 || appRemainingTimeMs < 0) {
setNoLimit()
} else {
@@ -144,20 +156,36 @@ class DigitalWellBeingToast(
}
}
/** Mark the DWB toast as destroyed and remove banner from TaskView. */
/** Bind the DWB toast to its dependencies. */
fun bind(
task: Task,
taskView: TaskView,
snapshotView: View,
@StagePosition stagePosition: Int
) {
this.task = task
this.taskView = taskView
this.snapshotView = snapshotView
this.stagePosition = stagePosition
isDestroyed = false
}
/** Mark the DWB toast as destroyed and hide it. */
fun destroy() {
visibility = INVISIBLE
isDestroyed = true
taskView.post { replaceBanner(null) }
}
private fun getSplitBannerConfig(): SplitBannerConfig {
val splitBounds = splitBounds
return when {
splitBounds == null || !container.deviceProfile.isTablet || taskView.isLargeTile ->
SplitBannerConfig.SPLIT_BANNER_FULLSCREEN
splitBounds == null ||
!recentsViewContainer.deviceProfile.isTablet ||
taskView.isLargeTile -> SplitBannerConfig.SPLIT_BANNER_FULLSCREEN
// For portrait grid only height of task changes, not width. So we keep the text the
// same
!container.deviceProfile.isLeftRightSplit -> SplitBannerConfig.SPLIT_GRID_BANNER_LARGE
!recentsViewContainer.deviceProfile.isLeftRightSplit ->
SplitBannerConfig.SPLIT_GRID_BANNER_LARGE
// For landscape grid, for 30% width we only show icon, otherwise show icon and time
task.key.id == splitBounds.leftTopTaskId ->
if (splitBounds.leftTaskPercent < THRESHOLD_LEFT_ICON_ONLY)
@@ -193,8 +221,7 @@ class DigitalWellBeingToast(
MeasureFormat.getInstance(Locale.getDefault(), MeasureFormat.FormatWidth.WIDE)
.formatMeasures(Measure(minutes, MeasureUnit.MINUTE))
// Use a specific string for usage less than one minute but non-zero.
duration > Duration.ZERO ->
container.asContext().getString(durationLessThanOneMinuteStringId)
duration > Duration.ZERO -> context.getString(durationLessThanOneMinuteStringId)
// Otherwise, return 0-minute string.
else ->
MeasureFormat.getInstance(Locale.getDefault(), MeasureFormat.FormatWidth.WIDE)
@@ -208,6 +235,7 @@ class DigitalWellBeingToast(
* [.SPLIT_BANNER_FULLSCREEN]
*/
@JvmOverloads
@VisibleForTesting
fun getBannerText(
remainingTime: Long = appRemainingTimeMs,
forContentDesc: Boolean = false
@@ -226,7 +254,7 @@ class DigitalWellBeingToast(
val splitBannerConfig = getSplitBannerConfig()
return when {
forContentDesc || splitBannerConfig == SplitBannerConfig.SPLIT_BANNER_FULLSCREEN ->
container.asContext().getString(R.string.time_left_for_app, readableDuration)
context.getString(R.string.time_left_for_app, readableDuration)
// show no text
splitBannerConfig == SplitBannerConfig.SPLIT_GRID_BANNER_SMALL -> ""
// SPLIT_GRID_BANNER_LARGE only show time
@@ -241,7 +269,7 @@ class DigitalWellBeingToast(
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
try {
val options = ActivityOptions.makeScaleUpAnimation(view, 0, 0, view.width, view.height)
container.asContext().startActivity(intent, options.toBundle())
context.startActivity(intent, options.toBundle())
// TODO: add WW logging on the app usage settings click.
} catch (e: ActivityNotFoundException) {
@@ -259,99 +287,77 @@ class DigitalWellBeingToast(
appRemainingTimeMs: Long
): String? =
if (appUsageLimitTimeMs >= 0 && appRemainingTimeMs >= 0)
container
.asContext()
.getString(
R.string.task_contents_description_with_remaining_time,
task.titleDescription,
getBannerText(appRemainingTimeMs, true /* forContentDesc */)
)
context.getString(
R.string.task_contents_description_with_remaining_time,
task.titleDescription,
getBannerText(appRemainingTimeMs, true /* forContentDesc */)
)
else task.titleDescription
private fun replaceBanner(view: View?) {
resetOldBanner()
setBanner(view)
}
private fun resetOldBanner() {
val banner = banner ?: return
banner.outlineProvider = oldBannerOutlineProvider
taskView.removeView(banner)
banner.setOnClickListener(null)
container.viewCache.recycleView(R.layout.digital_wellbeing_toast, banner)
}
private fun setBanner(banner: View?) {
this.banner = banner
if (banner != null && taskView.recentsView != null) {
setupAndAddBanner()
setBannerOutline()
fun setupLayout() {
val snapshotWidth: Int
val snapshotHeight: Int
val splitBounds = splitBounds
if (splitBounds == null) {
snapshotWidth = taskView.layoutParams.width
snapshotHeight =
taskView.layoutParams.height -
recentsViewContainer.deviceProfile.overviewTaskThumbnailTopMarginPx
} else {
val groupedTaskSize =
taskView.pagedOrientationHandler.getGroupedTaskViewSizes(
recentsViewContainer.deviceProfile,
splitBounds,
taskView.layoutParams.width,
taskView.layoutParams.height
)
if (stagePosition == STAGE_POSITION_TOP_OR_LEFT) {
snapshotWidth = groupedTaskSize.first.x
snapshotHeight = groupedTaskSize.first.y
} else {
snapshotWidth = groupedTaskSize.second.x
snapshotHeight = groupedTaskSize.second.y
}
}
taskView.pagedOrientationHandler.updateDwbBannerLayout(
taskView.layoutParams.width,
taskView.layoutParams.height,
taskView is GroupedTaskView,
recentsViewContainer.deviceProfile,
snapshotWidth,
snapshotHeight,
this
)
}
private fun setupAndAddBanner() {
val banner = banner ?: return
banner.updateLayoutParams<FrameLayout.LayoutParams> {
bottomMargin =
(taskView.firstSnapshotView.layoutParams as MarginLayoutParams).bottomMargin
}
private fun setupTranslations() {
val (translationX, translationY) =
taskView.pagedOrientationHandler.getDwbLayoutTranslations(
taskView.measuredWidth,
taskView.measuredHeight,
taskView.pagedOrientationHandler.getDwbBannerTranslations(
taskView.layoutParams.width,
taskView.layoutParams.height,
splitBounds,
container.deviceProfile,
recentsViewContainer.deviceProfile,
taskView.snapshotViews,
task.key.id,
banner
this
)
splitOffsetTranslationX = translationX
splitOffsetTranslationY = translationY
updateTranslationY()
updateTranslationX()
taskView.addView(banner)
}
private fun setBannerOutline() {
val banner = banner ?: return
// TODO(b\273367585) to investigate why mBanner.getOutlineProvider() can be null
val oldBannerOutlineProvider =
if (banner.outlineProvider != null) banner.outlineProvider
else ViewOutlineProvider.BACKGROUND
this.oldBannerOutlineProvider = oldBannerOutlineProvider
banner.outlineProvider =
object : ViewOutlineProvider() {
override fun getOutline(view: View, outline: Outline) {
oldBannerOutlineProvider.getOutline(view, outline)
val verticalTranslation = -view.translationY + splitOffsetTranslationY
outline.offset(0, Math.round(verticalTranslation))
}
}
banner.clipToOutline = true
this.translationX = translationX
this.splitOffsetTranslationY = translationY
}
private fun updateTranslationY() {
banner?.translationY = bannerOffsetPercentage * bannerHeight + splitOffsetTranslationY
translationY = bannerOffsetPercentage * bannerHeight + splitOffsetTranslationY
invalidateOutline()
}
private fun updateTranslationX() {
banner?.translationX = splitOffsetTranslationX
}
fun setBannerColorTint(color: Int, amount: Float) {
val banner = banner ?: return
fun setColorTint(color: Int, amount: Float) {
if (amount == 0f) {
banner.setLayerType(View.LAYER_TYPE_NONE, null)
setLayerType(View.LAYER_TYPE_NONE, null)
}
val layerPaint = Paint()
layerPaint.setColorFilter(Utilities.makeColorTintingColorFilter(color, amount))
banner.setLayerType(View.LAYER_TYPE_HARDWARE, layerPaint)
banner.setLayerPaint(layerPaint)
}
fun setBannerVisibility(visibility: Int) {
banner?.visibility = visibility
setLayerType(View.LAYER_TYPE_HARDWARE, layerPaint)
setLayerPaint(layerPaint)
}
private fun getAccessibilityActionId(): Int =
@@ -361,9 +367,8 @@ class DigitalWellBeingToast(
fun getDWBAccessibilityAction(): AccessibilityNodeInfo.AccessibilityAction? {
if (!hasLimit) return null
val context = container.asContext()
val label =
if ((taskView.containsMultipleTasks()))
if (taskView.containsMultipleTasks())
context.getString(
R.string.split_app_usage_settings,
TaskUtils.getTitle(context, task)
@@ -115,6 +115,7 @@ class GroupedTaskView @JvmOverloads constructor(context: Context, attrs: Attribu
R.id.snapshot,
R.id.icon,
R.id.show_windows,
R.id.digital_wellbeing_toast,
STAGE_POSITION_TOP_OR_LEFT,
taskOverlayFactory
),
@@ -123,6 +124,7 @@ class GroupedTaskView @JvmOverloads constructor(context: Context, attrs: Attribu
R.id.bottomright_snapshot,
R.id.bottomRight_icon,
R.id.show_windows_right,
R.id.bottomRight_digital_wellbeing_toast,
STAGE_POSITION_BOTTOM_OR_RIGHT,
taskOverlayFactory
)
@@ -211,7 +213,7 @@ class GroupedTaskView @JvmOverloads constructor(context: Context, attrs: Attribu
splitBoundsConfig = splitBounds
taskContainers.forEach {
it.digitalWellBeingToast?.splitBounds = splitBoundsConfig
it.digitalWellBeingToast?.initialize(it.task)
it.digitalWellBeingToast?.initialize()
}
invalidate()
}
@@ -146,6 +146,7 @@ class TaskContainer(
}
fun bind() {
digitalWellBeingToast?.bind(task, taskView, snapshotView, stagePosition)
if (enableRefactorTaskThumbnail()) {
bindThumbnailView()
} else {
@@ -171,4 +172,18 @@ class TaskContainer(
thumbnailViewDeprecated.setOverlayEnabled(enabled)
}
}
fun addChildForAccessibility(outChildren: ArrayList<View>) {
addAccessibleChildToList(iconView.asView(), outChildren)
addAccessibleChildToList(snapshotView, outChildren)
showWindowsView?.let { addAccessibleChildToList(it, outChildren) }
}
private fun addAccessibleChildToList(view: View, outChildren: ArrayList<View>) {
if (view.includeForAccessibility()) {
outChildren.add(view)
} else {
view.addChildrenForAccessibility(outChildren)
}
}
}
@@ -702,6 +702,7 @@ constructor(
R.id.snapshot,
R.id.icon,
R.id.show_windows,
R.id.digital_wellbeing_toast,
STAGE_POSITION_UNDEFINED,
taskOverlayFactory
)
@@ -715,6 +716,7 @@ constructor(
@IdRes thumbnailViewId: Int,
@IdRes iconViewId: Int,
@IdRes showWindowViewId: Int,
@IdRes digitalWellbeingBannerId: Int,
@StagePosition stagePosition: Int,
taskOverlayFactory: TaskOverlayFactory,
): TaskContainer {
@@ -730,6 +732,7 @@ constructor(
thumbnailViewDeprecated
}
val iconView = getOrInflateIconView(iconViewId)
val digitalWellBeingToast = findViewById<DigitalWellBeingToast>(digitalWellbeingBannerId)!!
return TaskContainer(
this,
task,
@@ -737,7 +740,7 @@ constructor(
iconView,
TransformingTouchDelegate(iconView.asView()),
stagePosition,
DigitalWellBeingToast(container, this),
digitalWellBeingToast,
findViewById(showWindowViewId)!!,
taskOverlayFactory
)
@@ -775,7 +778,7 @@ constructor(
protected open fun setThumbnailOrientation(orientationState: RecentsOrientedState) {
taskContainers.forEach {
it.overlay.updateOrientationState(orientationState)
it.digitalWellBeingToast?.initialize(it.task)
it.digitalWellBeingToast?.initialize()
}
}
@@ -827,10 +830,8 @@ constructor(
} else {
nonGridScale = 1f
boxTranslationY = 0f
expectedWidth = if (enableOverviewIconMenu()) taskWidth else LayoutParams.MATCH_PARENT
expectedHeight =
if (enableOverviewIconMenu()) taskHeight + thumbnailPadding
else LayoutParams.MATCH_PARENT
expectedWidth = taskWidth
expectedHeight = taskHeight + thumbnailPadding
}
this.nonGridScale = nonGridScale
this.boxTranslationY = boxTranslationY
@@ -847,6 +848,7 @@ constructor(
taskContainers[0].snapshotView.updateLayoutParams<LayoutParams> {
topMargin = container.deviceProfile.overviewTaskThumbnailTopMarginPx
}
taskContainers.forEach { it.digitalWellBeingToast?.setupLayout() }
}
/** Returns the thumbnail's bounds, optionally relative to the screen. */
@@ -939,7 +941,7 @@ constructor(
if (enableOverviewIconMenu()) {
setText(taskContainer.iconView, taskContainer.task.title)
}
taskContainer.digitalWellBeingToast?.initialize(taskContainer.task)
taskContainer.digitalWellBeingToast?.initialize()
}
protected open fun onIconUnloaded(taskContainer: TaskContainer) {
@@ -1444,7 +1446,7 @@ constructor(
it.thumbnailViewDeprecated.dimAlpha = amount
}
it.iconView.setIconColorTint(tintColor, amount)
it.digitalWellBeingToast?.setBannerColorTint(tintColor, amount)
it.digitalWellBeingToast?.setColorTint(tintColor, amount)
}
}
@@ -1458,7 +1460,7 @@ constructor(
taskContainers.forEach {
if (visibility == VISIBLE || it.task.key.id == taskId) {
it.snapshotView.visibility = visibility
it.digitalWellBeingToast?.setBannerVisibility(visibility)
it.digitalWellBeingToast?.visibility = visibility
it.showWindowsView?.visibility = visibility
it.overlay.setVisibility(visibility)
}
@@ -1662,6 +1664,12 @@ constructor(
return thumbnailBounds.contains(x.toInt(), y.toInt())
}
override fun addChildrenForAccessibility(outChildren: ArrayList<View>) {
(if (isLayoutRtl) taskContainers.reversed() else taskContainers).forEach {
it.addChildForAccessibility(outChildren)
}
}
companion object {
private const val TAG = "TaskView"
const val FLAG_UPDATE_ICON = 1
@@ -15,20 +15,18 @@
*/
package com.android.quickstep;
import static com.android.launcher3.util.rule.TestStabilityRule.LOCAL;
import static com.android.launcher3.util.rule.TestStabilityRule.PLATFORM_POSTSUBMIT;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import android.content.Intent;
import android.platform.test.annotations.PlatinumTest;
import com.android.launcher3.tapl.Overview;
import com.android.launcher3.tapl.OverviewTask.OverviewSplitTask;
import com.android.launcher3.tapl.OverviewTaskMenu;
import com.android.launcher3.ui.AbstractLauncherUiTest;
import com.android.launcher3.uioverrides.QuickstepLauncher;
import com.android.launcher3.util.rule.TestStabilityRule;
import com.android.quickstep.util.SplitScreenTestUtils;
import org.junit.Test;
@@ -70,39 +68,17 @@ public class TaplOverviewIconTest extends AbstractLauncherUiTest<QuickstepLaunch
@Test
public void testSplitTaskTapBothIconMenus() {
createAndLaunchASplitPair();
Overview overview = SplitScreenTestUtils.createAndLaunchASplitPairInOverview(mLauncher);
OverviewTaskMenu taskMenu =
mLauncher.goHome().switchToOverview().getCurrentTask().tapMenu();
OverviewTaskMenu taskMenu = overview.getCurrentTask().tapMenu();
assertTrue("App info item not appearing in expanded task menu.",
taskMenu.hasMenuItem("App info"));
taskMenu.touchOutsideTaskMenuToDismiss();
OverviewTaskMenu splitMenu =
mLauncher.goHome().switchToOverview().getCurrentTask().tapMenu(
OverviewTaskMenu splitMenu = overview.getCurrentTask().tapMenu(
OverviewSplitTask.SPLIT_BOTTOM_OR_RIGHT);
assertTrue("App info item not appearing in expanded split task's menu.",
splitMenu.hasMenuItem("App info"));
splitMenu.touchOutsideTaskMenuToDismiss();
}
private void createAndLaunchASplitPair() {
clearAllRecentTasks();
startTestActivity(2);
startTestActivity(3);
if (mLauncher.isTablet() && !mLauncher.isGridOnlyOverviewEnabled()) {
mLauncher.goHome().switchToOverview().getOverviewActions()
.clickSplit()
.getTestActivityTask(2)
.open();
} else {
mLauncher.goHome().switchToOverview().getCurrentTask()
.tapMenu()
.tapSplitMenuItem()
.getCurrentTask()
.open();
}
}
}
}
@@ -290,9 +290,6 @@ public class TaplTestsQuickstep extends AbstractQuickStepTest {
}
}
// Staging; will be promoted to presubmit if stable
@TestStabilityRule.Stability(flavors = LOCAL | PLATFORM_POSTSUBMIT)
@Test
@NavigationModeSwitch
@PortraitLandscape
@@ -481,7 +478,8 @@ public class TaplTestsQuickstep extends AbstractQuickStepTest {
// assertTrue("Launcher internal state didn't remain in Overview",
// isInState(() -> LauncherState.OVERVIEW));
// overview.getCurrentTask().dismiss();
// executeOnLauncher(launcher -> assertTrue("Grid did not rebalance after multiple dismissals",
// executeOnLauncher(launcher -> assertTrue("Grid did not rebalance after multiple
// dismissals",
// (Math.abs(getTopRowTaskCountForTablet(launcher) - getBottomRowTaskCountForTablet(
// launcher)) <= 1)));
@@ -592,7 +590,8 @@ public class TaplTestsQuickstep extends AbstractQuickStepTest {
if (overview.hasTasks()) {
currentTask = overview.getCurrentTask();
assertFalse("Found ExcludeFromRecentsTestActivity after entering Overview from Home",
currentTask.containsContentDescription("ExcludeFromRecents")
currentTask.containsContentDescription(
"ExcludeFromRecents")
|| currentTask.containsContentDescription(null));
} else {
// Presumably the test started with 0 tasks and remains that way after going home.
@@ -17,8 +17,6 @@ package com.android.quickstep;
import static com.android.launcher3.config.FeatureFlags.enableSplitContextually;
import static com.android.launcher3.util.rule.TestStabilityRule.LOCAL;
import static com.android.launcher3.util.rule.TestStabilityRule.PLATFORM_POSTSUBMIT;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
@@ -33,7 +31,7 @@ import androidx.test.platform.app.InstrumentationRegistry;
import com.android.launcher3.tapl.Overview;
import com.android.launcher3.tapl.Taskbar;
import com.android.launcher3.tapl.TaskbarAppIcon;
import com.android.launcher3.util.rule.TestStabilityRule;
import com.android.quickstep.util.SplitScreenTestUtils;
import com.android.wm.shell.Flags;
import org.junit.After;
@@ -110,9 +108,8 @@ public class TaplTestsSplitscreen extends AbstractQuickStepTest {
assumeTrue("App pairs feature is currently not enabled, no test needed",
Flags.enableAppPairs());
createAndLaunchASplitPair();
Overview overview = SplitScreenTestUtils.createAndLaunchASplitPairInOverview(mLauncher);
Overview overview = mLauncher.goHome().switchToOverview();
if (mLauncher.isGridOnlyOverviewEnabled() || !mLauncher.isTablet()) {
assertTrue("Save app pair menu item is missing",
overview.getCurrentTask()
@@ -156,24 +153,4 @@ public class TaplTestsSplitscreen extends AbstractQuickStepTest {
TaskbarAppIcon firstApp = taskbar.getAppIcon(firstAppName);
firstApp.launchIntoSplitScreen();
}
private void createAndLaunchASplitPair() {
clearAllRecentTasks();
startTestActivity(2);
startTestActivity(3);
if (mLauncher.isTablet() && !mLauncher.isGridOnlyOverviewEnabled()) {
mLauncher.goHome().switchToOverview().getOverviewActions()
.clickSplit()
.getTestActivityTask(2)
.open();
} else {
mLauncher.goHome().switchToOverview().getCurrentTask()
.tapMenu()
.tapSplitMenuItem()
.getCurrentTask()
.open();
}
}
}
@@ -0,0 +1,60 @@
/*
* 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.quickstep.util
import androidx.test.uiautomator.By
import com.android.launcher3.tapl.LauncherInstrumentation
import com.android.launcher3.tapl.Overview
import com.android.launcher3.tapl.OverviewTask
import com.android.launcher3.ui.AbstractLauncherUiTest
object SplitScreenTestUtils {
/** Creates 2 tasks and makes a split mode pair. Also asserts the accessibility labels. */
@JvmStatic
fun createAndLaunchASplitPairInOverview(launcher: LauncherInstrumentation): Overview {
clearAllRecentTasks(launcher)
AbstractLauncherUiTest.startTestActivity(2)
AbstractLauncherUiTest.startTestActivity(3)
val overView = launcher.goHome().switchToOverview()
if (launcher.isTablet && !launcher.isGridOnlyOverviewEnabled) {
overView.overviewActions.clickSplit().getTestActivityTask(2).open()
} else {
overView.currentTask.tapMenu().tapSplitMenuItem().currentTask.open()
}
val overviewWithSplitPair = launcher.goHome().switchToOverview()
val currentTask = overviewWithSplitPair.currentTask
currentTask.containsContentDescription(
By.pkg(AbstractLauncherUiTest.getAppPackageName()).text("TestActivity3").toString(),
OverviewTask.OverviewSplitTask.SPLIT_TOP_OR_LEFT
)
currentTask.containsContentDescription(
By.pkg(AbstractLauncherUiTest.getAppPackageName()).text("TestActivity2").toString(),
OverviewTask.OverviewSplitTask.SPLIT_BOTTOM_OR_RIGHT
)
return overviewWithSplitPair
}
private fun clearAllRecentTasks(launcher: LauncherInstrumentation) {
if (launcher.recentTasks.isNotEmpty()) {
launcher.goHome().switchToOverview().dismissAllTasks()
}
}
}
@@ -284,8 +284,9 @@ public final class OverviewTask {
*
* TODO(b/326565120): remove Nullable support once the bug causing it to be null is fixed.
*/
public boolean containsContentDescription(@Nullable String expected) {
String actual = mTask.getContentDescription();
public boolean containsContentDescription(@Nullable String expected,
OverviewSplitTask overviewSplitTask) {
String actual = findObjectInTask(overviewSplitTask.snapshotRes).getContentDescription();
if (actual == null && expected == null) {
return true;
}
@@ -295,6 +296,14 @@ public final class OverviewTask {
return actual.contains(expected);
}
/**
* Returns whether the given String is contained in this Task's contentDescription. Also returns
* true if both Strings are null
*/
public boolean containsContentDescription(@Nullable String expected) {
return containsContentDescription(expected, DEFAULT);
}
private TaskViewType getType() {
String resourceName = mTask.getResourceName();
if (resourceName.endsWith("task_view_grouped")) {