Snap for 12205827 from 6edc79057e to 24Q4-release
Change-Id: I3654f7dfe7f907743134ab9926a1d5b0e5cbb58e
This commit is contained in:
@@ -41,5 +41,6 @@
|
||||
android:textColor="?androidprv:attr/textColorOnAccent"
|
||||
android:layout_marginStart="@dimen/split_instructions_start_margin_cancel"
|
||||
android:text="@string/toast_split_select_app_cancel"
|
||||
android:textStyle="bold"
|
||||
android:visibility="gone"/>
|
||||
</com.android.quickstep.views.SplitInstructionsView>
|
||||
@@ -19,7 +19,7 @@
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
|
||||
xmlns:launcher="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/task"
|
||||
android:id="@+id/task_view_single"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clipChildren="false"
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
|
||||
xmlns:launcher="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/task"
|
||||
android:id="@+id/task_view_desktop"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clipChildren="true"
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
|
||||
xmlns:launcher="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/task"
|
||||
android:id="@+id/task_view_grouped"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clipChildren="false"
|
||||
|
||||
@@ -237,7 +237,7 @@
|
||||
<!-- Label for toast with instructions for split screen selection mode. [CHAR_LIMIT=50] -->
|
||||
<string name="toast_split_select_app">Tap another app to use split screen</string>
|
||||
<string name="toast_contextual_split_select_app">Choose another app to use split screen</string>
|
||||
<string name="toast_split_select_app_cancel"><b>Cancel</b></string>
|
||||
<string name="toast_split_select_app_cancel">Cancel</string>
|
||||
<string name="toast_split_select_cont_desc">Exit split screen selection</string>
|
||||
<!-- Label for toast when app selected for split isn't supported. [CHAR_LIMIT=50] -->
|
||||
<string name="toast_split_app_unsupported">Choose another app to use split screen</string>
|
||||
|
||||
@@ -303,7 +303,8 @@ public class BubbleBarViewController {
|
||||
|
||||
/** Whether a new bubble is animating. */
|
||||
public boolean isAnimatingNewBubble() {
|
||||
return mBarView.isAnimatingNewBubble();
|
||||
return mBarView.isAnimatingNewBubble()
|
||||
|| (mBubbleBarViewAnimator != null && mBubbleBarViewAnimator.hasAnimatingBubble());
|
||||
}
|
||||
|
||||
/** The horizontal margin of the bubble bar from the edge of the screen. */
|
||||
@@ -575,14 +576,14 @@ public class BubbleBarViewController {
|
||||
}
|
||||
boolean persistentTaskbarOrOnHome = mBubbleStashController.isBubblesShowingOnHome()
|
||||
|| !mBubbleStashController.isTransientTaskBar();
|
||||
if (persistentTaskbarOrOnHome && !isExpanding && !isExpanded()) {
|
||||
mBubbleBarViewAnimator.animateBubbleBarForCollapsed(bubble);
|
||||
if (persistentTaskbarOrOnHome && !isExpanded()) {
|
||||
mBubbleBarViewAnimator.animateBubbleBarForCollapsed(bubble, isExpanding);
|
||||
return;
|
||||
}
|
||||
|
||||
// only animate the new bubble if we're in an app, have handle view and not auto expanding
|
||||
if (isInApp && !isExpanding && mBubbleStashController.getHasHandleView() && !isExpanded()) {
|
||||
mBubbleBarViewAnimator.animateBubbleInForStashed(bubble);
|
||||
if (isInApp && mBubbleStashController.getHasHandleView() && !isExpanded()) {
|
||||
mBubbleBarViewAnimator.animateBubbleInForStashed(bubble, isExpanding);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -626,6 +627,10 @@ public class BubbleBarViewController {
|
||||
* from SystemUI.
|
||||
*/
|
||||
public void setExpandedFromSysui(boolean isExpanded) {
|
||||
if (isAnimatingNewBubble() && isExpanded) {
|
||||
mBubbleBarViewAnimator.expandedWhileAnimating();
|
||||
return;
|
||||
}
|
||||
if (!isExpanded) {
|
||||
mBubbleStashController.stashBubbleBar();
|
||||
} else {
|
||||
|
||||
+5
@@ -245,6 +245,11 @@ public class BubbleStashedHandleViewController {
|
||||
mStashedHandleView.setTranslationY(transY);
|
||||
}
|
||||
|
||||
/** Returns the translation of the stashed handle. */
|
||||
public float getTranslationY() {
|
||||
return mStashedHandleView.getTranslationY();
|
||||
}
|
||||
|
||||
/**
|
||||
* Used by {@link BubbleStashController} to animate the handle when stashing or un stashing.
|
||||
*/
|
||||
|
||||
+139
-25
@@ -43,6 +43,8 @@ constructor(
|
||||
private val bubbleBarBounceDistanceInPx =
|
||||
bubbleBarView.resources.getDimensionPixelSize(R.dimen.bubblebar_bounce_distance)
|
||||
|
||||
fun hasAnimatingBubble() = animatingBubble != null
|
||||
|
||||
private companion object {
|
||||
/** The time to show the flyout. */
|
||||
const val FLYOUT_DELAY_MS: Long = 2500
|
||||
@@ -58,8 +60,33 @@ constructor(
|
||||
private data class AnimatingBubble(
|
||||
val bubbleView: BubbleView,
|
||||
val showAnimation: Runnable,
|
||||
val hideAnimation: Runnable
|
||||
)
|
||||
val hideAnimation: Runnable,
|
||||
val expand: Boolean,
|
||||
val state: State = State.CREATED
|
||||
) {
|
||||
|
||||
/**
|
||||
* The state of the animation.
|
||||
*
|
||||
* The animation is initially created but will be scheduled later using the [Scheduler].
|
||||
*
|
||||
* The normal uninterrupted cycle is for the bubble notification to animate in, then be in a
|
||||
* transient state and eventually to animate out.
|
||||
*
|
||||
* However different events, such as touch and external signals, may cause the animation to
|
||||
* end earlier.
|
||||
*/
|
||||
enum class State {
|
||||
/** The animation is created but not started yet. */
|
||||
CREATED,
|
||||
/** The bubble notification is animating in. */
|
||||
ANIMATING_IN,
|
||||
/** The bubble notification is now fully showing and waiting to be hidden. */
|
||||
IN,
|
||||
/** The bubble notification is animating out. */
|
||||
ANIMATING_OUT
|
||||
}
|
||||
}
|
||||
|
||||
/** An interface for scheduling jobs. */
|
||||
interface Scheduler {
|
||||
@@ -97,15 +124,18 @@ constructor(
|
||||
)
|
||||
|
||||
/** Animates a bubble for the state where the bubble bar is stashed. */
|
||||
fun animateBubbleInForStashed(b: BubbleBarBubble) {
|
||||
fun animateBubbleInForStashed(b: BubbleBarBubble, isExpanding: Boolean) {
|
||||
// TODO b/346400677: handle animations for the same bubble interrupting each other
|
||||
if (animatingBubble?.bubbleView?.bubble?.key == b.key) return
|
||||
val bubbleView = b.view
|
||||
val animator = PhysicsAnimator.getInstance(bubbleView)
|
||||
if (animator.isRunning()) animator.cancel()
|
||||
// the animation of a new bubble is divided into 2 parts. The first part shows the bubble
|
||||
// and the second part hides it after a delay.
|
||||
val showAnimation = buildHandleToBubbleBarAnimation()
|
||||
val hideAnimation = buildBubbleBarToHandleAnimation()
|
||||
animatingBubble = AnimatingBubble(bubbleView, showAnimation, hideAnimation)
|
||||
val hideAnimation = if (isExpanding) Runnable {} else buildBubbleBarToHandleAnimation()
|
||||
animatingBubble =
|
||||
AnimatingBubble(bubbleView, showAnimation, hideAnimation, expand = isExpanding)
|
||||
scheduler.post(showAnimation)
|
||||
scheduler.postDelayed(FLYOUT_DELAY_MS, hideAnimation)
|
||||
}
|
||||
@@ -125,6 +155,7 @@ constructor(
|
||||
* visible which helps avoiding further updates when we re-enter the second part.
|
||||
*/
|
||||
private fun buildHandleToBubbleBarAnimation() = Runnable {
|
||||
moveToState(AnimatingBubble.State.ANIMATING_IN)
|
||||
// prepare the bubble bar for the animation
|
||||
bubbleBarView.onAnimatingBubbleStarted()
|
||||
bubbleBarView.visibility = VISIBLE
|
||||
@@ -138,9 +169,12 @@ constructor(
|
||||
// handle. when the handle becomes invisible and we start animating in the bubble bar,
|
||||
// the translation y is offset by this value to make the transition from the handle to the
|
||||
// bar smooth.
|
||||
val offset: Float = bubbleStashController.getDiffBetweenHandleAndBarCenters()
|
||||
val stashedHandleTranslationY: Float =
|
||||
val offset = bubbleStashController.getDiffBetweenHandleAndBarCenters()
|
||||
val stashedHandleTranslationYForAnimation =
|
||||
bubbleStashController.getStashedHandleTranslationForNewBubbleAnimation()
|
||||
val stashedHandleTranslationY =
|
||||
bubbleStashController.getHandleTranslationY() ?: return@Runnable
|
||||
val translationTracker = TranslationTracker(stashedHandleTranslationY)
|
||||
|
||||
// this is the total distance that both the stashed handle and the bubble will be traveling
|
||||
// at the end of the animation the bubble bar will be positioned in the same place when it
|
||||
@@ -150,15 +184,14 @@ constructor(
|
||||
animator.setDefaultSpringConfig(springConfig)
|
||||
animator.spring(DynamicAnimation.TRANSLATION_Y, totalTranslationY)
|
||||
animator.addUpdateListener { handle, values ->
|
||||
val ty: Float =
|
||||
values[DynamicAnimation.TRANSLATION_Y]?.value ?: return@addUpdateListener
|
||||
val ty = values[DynamicAnimation.TRANSLATION_Y]?.value ?: return@addUpdateListener
|
||||
when {
|
||||
ty >= stashedHandleTranslationY -> {
|
||||
ty >= stashedHandleTranslationYForAnimation -> {
|
||||
// we're in the first leg of the animation. only animate the handle. the bubble
|
||||
// bar remains hidden during this part of the animation
|
||||
|
||||
// map the path [0, stashedHandleTranslationY] to [0,1]
|
||||
val fraction = ty / stashedHandleTranslationY
|
||||
val fraction = ty / stashedHandleTranslationYForAnimation
|
||||
handle.alpha = 1 - fraction
|
||||
}
|
||||
ty >= totalTranslationY -> {
|
||||
@@ -172,8 +205,8 @@ constructor(
|
||||
if (bubbleBarView.alpha != 1f) {
|
||||
// map the path [stashedHandleTranslationY, totalTranslationY] to [0, 1]
|
||||
val fraction =
|
||||
(ty - stashedHandleTranslationY) /
|
||||
(totalTranslationY - stashedHandleTranslationY)
|
||||
(ty - stashedHandleTranslationYForAnimation) /
|
||||
(totalTranslationY - stashedHandleTranslationYForAnimation)
|
||||
bubbleBarView.alpha = fraction
|
||||
bubbleBarView.scaleY =
|
||||
BUBBLE_ANIMATION_INITIAL_SCALE_Y +
|
||||
@@ -193,18 +226,16 @@ constructor(
|
||||
bubbleStashController.updateTaskbarTouchRegion()
|
||||
}
|
||||
}
|
||||
translationTracker.updateTyAndExpandIfNeeded(ty)
|
||||
}
|
||||
animator.addEndListener { _, _, _, canceled, _, _, _ ->
|
||||
// if the show animation was canceled, also cancel the hide animation. this is typically
|
||||
// canceled in this class, but could potentially be canceled elsewhere.
|
||||
if (canceled) {
|
||||
val hideAnimation = animatingBubble?.hideAnimation ?: return@addEndListener
|
||||
scheduler.cancel(hideAnimation)
|
||||
animatingBubble = null
|
||||
bubbleBarView.onAnimatingBubbleCompleted()
|
||||
bubbleBarView.relativePivotY = 1f
|
||||
if (canceled || animatingBubble?.expand == true) {
|
||||
cancelHideAnimation()
|
||||
return@addEndListener
|
||||
}
|
||||
moveToState(AnimatingBubble.State.IN)
|
||||
// the bubble bar is now fully settled in. update taskbar touch region so it's touchable
|
||||
bubbleStashController.updateTaskbarTouchRegion()
|
||||
}
|
||||
@@ -227,7 +258,8 @@ constructor(
|
||||
*/
|
||||
private fun buildBubbleBarToHandleAnimation() = Runnable {
|
||||
if (animatingBubble == null) return@Runnable
|
||||
val offset = bubbleStashController.getStashedHandleTranslationForNewBubbleAnimation()
|
||||
moveToState(AnimatingBubble.State.ANIMATING_OUT)
|
||||
val offset = bubbleStashController.getDiffBetweenHandleAndBarCenters()
|
||||
val stashedHandleTranslationY =
|
||||
bubbleStashController.getStashedHandleTranslationForNewBubbleAnimation()
|
||||
// this is the total distance that both the stashed handle and the bar will be traveling
|
||||
@@ -281,6 +313,8 @@ constructor(
|
||||
|
||||
/** Animates to the initial state of the bubble bar, when there are no previous bubbles. */
|
||||
fun animateToInitialState(b: BubbleBarBubble, isInApp: Boolean, isExpanding: Boolean) {
|
||||
// TODO b/346400677: handle animations for the same bubble interrupting each other
|
||||
if (animatingBubble?.bubbleView?.bubble?.key == b.key) return
|
||||
val bubbleView = b.view
|
||||
val animator = PhysicsAnimator.getInstance(bubbleView)
|
||||
if (animator.isRunning()) animator.cancel()
|
||||
@@ -300,12 +334,14 @@ constructor(
|
||||
bubbleStashController.updateTaskbarTouchRegion()
|
||||
}
|
||||
}
|
||||
animatingBubble = AnimatingBubble(bubbleView, showAnimation, hideAnimation)
|
||||
animatingBubble =
|
||||
AnimatingBubble(bubbleView, showAnimation, hideAnimation, expand = isExpanding)
|
||||
scheduler.post(showAnimation)
|
||||
scheduler.postDelayed(FLYOUT_DELAY_MS, hideAnimation)
|
||||
}
|
||||
|
||||
private fun buildBubbleBarSpringInAnimation() = Runnable {
|
||||
moveToState(AnimatingBubble.State.ANIMATING_IN)
|
||||
// prepare the bubble bar for the animation
|
||||
bubbleBarView.onAnimatingBubbleStarted()
|
||||
bubbleBarView.translationY = bubbleBarView.height.toFloat()
|
||||
@@ -314,18 +350,31 @@ constructor(
|
||||
bubbleBarView.scaleX = 1f
|
||||
bubbleBarView.scaleY = 1f
|
||||
|
||||
val translationTracker = TranslationTracker(bubbleBarView.translationY)
|
||||
|
||||
val animator = PhysicsAnimator.getInstance(bubbleBarView)
|
||||
animator.setDefaultSpringConfig(springConfig)
|
||||
animator.spring(DynamicAnimation.TRANSLATION_Y, bubbleStashController.bubbleBarTranslationY)
|
||||
animator.addUpdateListener { _, _ -> bubbleStashController.updateTaskbarTouchRegion() }
|
||||
animator.addUpdateListener { _, values ->
|
||||
val ty = values[DynamicAnimation.TRANSLATION_Y]?.value ?: return@addUpdateListener
|
||||
translationTracker.updateTyAndExpandIfNeeded(ty)
|
||||
bubbleStashController.updateTaskbarTouchRegion()
|
||||
}
|
||||
animator.addEndListener { _, _, _, _, _, _, _ ->
|
||||
if (animatingBubble?.expand == true) {
|
||||
cancelHideAnimation()
|
||||
} else {
|
||||
moveToState(AnimatingBubble.State.IN)
|
||||
}
|
||||
// the bubble bar is now fully settled in. update taskbar touch region so it's touchable
|
||||
bubbleStashController.updateTaskbarTouchRegion()
|
||||
}
|
||||
animator.start()
|
||||
}
|
||||
|
||||
fun animateBubbleBarForCollapsed(b: BubbleBarBubble) {
|
||||
fun animateBubbleBarForCollapsed(b: BubbleBarBubble, isExpanding: Boolean) {
|
||||
// TODO b/346400677: handle animations for the same bubble interrupting each other
|
||||
if (animatingBubble?.bubbleView?.bubble?.key == b.key) return
|
||||
val bubbleView = b.view
|
||||
val animator = PhysicsAnimator.getInstance(bubbleView)
|
||||
if (animator.isRunning()) animator.cancel()
|
||||
@@ -336,7 +385,8 @@ constructor(
|
||||
bubbleBarView.onAnimatingBubbleCompleted()
|
||||
bubbleStashController.updateTaskbarTouchRegion()
|
||||
}
|
||||
animatingBubble = AnimatingBubble(bubbleView, showAnimation, hideAnimation)
|
||||
animatingBubble =
|
||||
AnimatingBubble(bubbleView, showAnimation, hideAnimation, expand = isExpanding)
|
||||
scheduler.post(showAnimation)
|
||||
scheduler.postDelayed(FLYOUT_DELAY_MS, hideAnimation)
|
||||
}
|
||||
@@ -347,17 +397,29 @@ constructor(
|
||||
* the bubble bar moves back to its initial position with a spring animation.
|
||||
*/
|
||||
private fun buildBubbleBarBounceAnimation() = Runnable {
|
||||
moveToState(AnimatingBubble.State.ANIMATING_IN)
|
||||
bubbleBarView.onAnimatingBubbleStarted()
|
||||
val ty = bubbleStashController.bubbleBarTranslationY
|
||||
|
||||
val springBackAnimation = PhysicsAnimator.getInstance(bubbleBarView)
|
||||
springBackAnimation.setDefaultSpringConfig(springConfig)
|
||||
springBackAnimation.spring(DynamicAnimation.TRANSLATION_Y, ty)
|
||||
springBackAnimation.addEndListener { _, _, _, _, _, _, _ ->
|
||||
if (animatingBubble?.expand == true) {
|
||||
bubbleBarView.isExpanded = true
|
||||
cancelHideAnimation()
|
||||
} else {
|
||||
moveToState(AnimatingBubble.State.IN)
|
||||
}
|
||||
}
|
||||
|
||||
// animate the bubble bar up and start the spring back down animation when it ends.
|
||||
ObjectAnimator.ofFloat(bubbleBarView, View.TRANSLATION_Y, ty - bubbleBarBounceDistanceInPx)
|
||||
.withDuration(BUBBLE_BAR_BOUNCE_ANIMATION_DURATION_MS)
|
||||
.withEndAction { springBackAnimation.start() }
|
||||
.withEndAction {
|
||||
if (animatingBubble?.expand == true) bubbleBarView.isExpanded = true
|
||||
springBackAnimation.start()
|
||||
}
|
||||
.start()
|
||||
}
|
||||
|
||||
@@ -386,6 +448,25 @@ constructor(
|
||||
)
|
||||
}
|
||||
|
||||
fun expandedWhileAnimating() {
|
||||
val animatingBubble = animatingBubble ?: return
|
||||
this.animatingBubble = animatingBubble.copy(expand = true)
|
||||
// if we're fully in and waiting to hide, cancel the hide animation and clean up
|
||||
if (animatingBubble.state == AnimatingBubble.State.IN) {
|
||||
bubbleBarView.isExpanded = true
|
||||
cancelHideAnimation()
|
||||
}
|
||||
}
|
||||
|
||||
private fun cancelHideAnimation() {
|
||||
val hideAnimation = animatingBubble?.hideAnimation ?: return
|
||||
scheduler.cancel(hideAnimation)
|
||||
animatingBubble = null
|
||||
bubbleBarView.onAnimatingBubbleCompleted()
|
||||
bubbleBarView.relativePivotY = 1f
|
||||
bubbleStashController.showBubbleBarImmediate()
|
||||
}
|
||||
|
||||
private fun <T> PhysicsAnimator<T>?.cancelIfRunning() {
|
||||
if (this?.isRunning() == true) cancel()
|
||||
}
|
||||
@@ -405,4 +486,37 @@ constructor(
|
||||
)
|
||||
return this
|
||||
}
|
||||
|
||||
private fun moveToState(state: AnimatingBubble.State) {
|
||||
val animatingBubble = this.animatingBubble ?: return
|
||||
this.animatingBubble = animatingBubble.copy(state = state)
|
||||
}
|
||||
|
||||
/**
|
||||
* Tracks the translation Y of the bubble bar during the animation. When the bubble bar expands
|
||||
* as part of the animation, the expansion should start after the bubble bar reaches the peak
|
||||
* position.
|
||||
*/
|
||||
private inner class TranslationTracker(initialTy: Float) {
|
||||
private var previousTy = initialTy
|
||||
private var startedExpanding = false
|
||||
private var reachedPeak = false
|
||||
|
||||
fun updateTyAndExpandIfNeeded(ty: Float) {
|
||||
if (!reachedPeak) {
|
||||
// the bubble bar is positioned at the bottom of the screen and moves up using
|
||||
// negative ty values. the peak is reached the first time we see a value that is
|
||||
// greater than the previous.
|
||||
if (ty > previousTy) {
|
||||
reachedPeak = true
|
||||
}
|
||||
}
|
||||
val expand = animatingBubble?.expand ?: false
|
||||
if (reachedPeak && expand && !startedExpanding) {
|
||||
bubbleBarView.isExpanded = true
|
||||
startedExpanding = true
|
||||
}
|
||||
previousTy = ty
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,6 +143,9 @@ interface BubbleStashController {
|
||||
/** Set the translation Y for the stashed handle. */
|
||||
fun setHandleTranslationY(translationY: Float)
|
||||
|
||||
/** Returns the translation of the handle. */
|
||||
fun getHandleTranslationY(): Float?
|
||||
|
||||
/**
|
||||
* Returns bubble bar Y position according to [isBubblesShowingOnHome] and
|
||||
* [isBubblesShowingOnOverview] values. Default implementation only analyse
|
||||
|
||||
+2
@@ -198,6 +198,8 @@ class PersistentBubbleStashController(
|
||||
// no op since does not have a handle view
|
||||
}
|
||||
|
||||
override fun getHandleTranslationY(): Float? = null
|
||||
|
||||
private fun updateExpandedState(expand: Boolean) {
|
||||
if (bubbleBarViewController.isHiddenForNoBubbles) {
|
||||
// If there are no bubbles the bar is invisible, nothing to do here.
|
||||
|
||||
+2
@@ -242,6 +242,8 @@ class TransientBubbleStashController(
|
||||
bubbleStashedHandleViewController?.setTranslationYForSwipe(translationY)
|
||||
}
|
||||
|
||||
override fun getHandleTranslationY(): Float? = bubbleStashedHandleViewController?.translationY
|
||||
|
||||
private fun getStashTranslation(): Float {
|
||||
return (bubbleBarViewController.bubbleBarCollapsedHeight - stashedHeight) / 2f
|
||||
}
|
||||
|
||||
@@ -30,6 +30,8 @@ import android.view.ViewGroup
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import com.android.launcher3.Flags.enableRefactorTaskThumbnail
|
||||
import com.android.launcher3.R
|
||||
import com.android.launcher3.testing.TestLogging
|
||||
import com.android.launcher3.testing.shared.TestProtocol
|
||||
import com.android.launcher3.util.RunnableList
|
||||
import com.android.launcher3.util.SplitConfigurationOptions
|
||||
import com.android.launcher3.util.TransformingTouchDelegate
|
||||
@@ -213,7 +215,15 @@ class DesktopTaskView @JvmOverloads constructor(context: Context, attrs: Attribu
|
||||
}
|
||||
|
||||
override fun needsUpdate(dataChange: Int, flag: Int) =
|
||||
if (flag == FLAG_UPDATE_THUMBNAIL) super.needsUpdate(dataChange, flag) else false
|
||||
if (flag == FLAG_UPDATE_CORNER_RADIUS) false else super.needsUpdate(dataChange, flag)
|
||||
|
||||
override fun onIconLoaded(taskContainer: TaskContainer) {
|
||||
// Update contentDescription of snapshotView only, individual task icon is unused.
|
||||
taskContainer.snapshotView.contentDescription = taskContainer.task.titleDescription
|
||||
}
|
||||
|
||||
// Ignoring [onIconUnloaded] as all tasks shares the same Desktop icon
|
||||
override fun onIconUnloaded(taskContainer: TaskContainer) {}
|
||||
|
||||
// thumbnailView is laid out differently and is handled in onMeasure
|
||||
override fun updateThumbnailSize() {}
|
||||
@@ -228,6 +238,11 @@ class DesktopTaskView @JvmOverloads constructor(context: Context, attrs: Attribu
|
||||
|
||||
override fun launchTaskAnimated(): RunnableList? {
|
||||
val recentsView = recentsView ?: return null
|
||||
TestLogging.recordEvent(
|
||||
TestProtocol.SEQUENCE_MAIN,
|
||||
"launchDesktopFromRecents",
|
||||
taskIds.contentToString()
|
||||
)
|
||||
val endCallback = RunnableList()
|
||||
val desktopController = recentsView.desktopRecentsController
|
||||
checkNotNull(desktopController) { "recentsController is null" }
|
||||
|
||||
@@ -901,18 +901,11 @@ constructor(
|
||||
it.task.icon = icon
|
||||
it.task.titleDescription = contentDescription
|
||||
it.task.title = title
|
||||
setIcon(it.iconView, icon)
|
||||
if (enableOverviewIconMenu()) {
|
||||
setText(it.iconView, title)
|
||||
}
|
||||
it.digitalWellBeingToast?.initialize(it.task)
|
||||
onIconLoaded(it)
|
||||
}
|
||||
?.also { request -> pendingIconLoadRequests.add(request) }
|
||||
} else {
|
||||
setIcon(it.iconView, null)
|
||||
if (enableOverviewIconMenu()) {
|
||||
setText(it.iconView, null)
|
||||
}
|
||||
onIconUnloaded(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -931,6 +924,21 @@ constructor(
|
||||
pendingIconLoadRequests.clear()
|
||||
}
|
||||
|
||||
protected open fun onIconLoaded(taskContainer: TaskContainer) {
|
||||
setIcon(taskContainer.iconView, taskContainer.task.icon)
|
||||
if (enableOverviewIconMenu()) {
|
||||
setText(taskContainer.iconView, taskContainer.task.title)
|
||||
}
|
||||
taskContainer.digitalWellBeingToast?.initialize(taskContainer.task)
|
||||
}
|
||||
|
||||
protected open fun onIconUnloaded(taskContainer: TaskContainer) {
|
||||
setIcon(taskContainer.iconView, null)
|
||||
if (enableOverviewIconMenu()) {
|
||||
setText(taskContainer.iconView, null)
|
||||
}
|
||||
}
|
||||
|
||||
protected fun setIcon(iconView: TaskViewIcon, icon: Drawable?) {
|
||||
with(iconView) {
|
||||
if (icon != null) {
|
||||
@@ -1152,6 +1160,11 @@ constructor(
|
||||
isClickableAsLiveTile = true
|
||||
return runnableList
|
||||
}
|
||||
TestLogging.recordEvent(
|
||||
TestProtocol.SEQUENCE_MAIN,
|
||||
"composeRecentsLaunchAnimator",
|
||||
taskIds.contentToString()
|
||||
)
|
||||
val runnableList = RunnableList()
|
||||
with(AnimatorSet()) {
|
||||
TaskViewUtils.composeRecentsLaunchAnimator(
|
||||
|
||||
+341
-13
@@ -84,7 +84,7 @@ class BubbleBarViewAnimatorTest {
|
||||
BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler)
|
||||
|
||||
InstrumentationRegistry.getInstrumentation().runOnMainSync {
|
||||
animator.animateBubbleInForStashed(bubble)
|
||||
animator.animateBubbleInForStashed(bubble, isExpanding = false)
|
||||
}
|
||||
|
||||
// let the animation start and wait for it to complete
|
||||
@@ -128,7 +128,7 @@ class BubbleBarViewAnimatorTest {
|
||||
BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler)
|
||||
|
||||
InstrumentationRegistry.getInstrumentation().runOnMainSync {
|
||||
animator.animateBubbleInForStashed(bubble)
|
||||
animator.animateBubbleInForStashed(bubble, isExpanding = false)
|
||||
}
|
||||
|
||||
// let the animation start and wait for it to complete
|
||||
@@ -171,7 +171,7 @@ class BubbleBarViewAnimatorTest {
|
||||
BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler)
|
||||
|
||||
InstrumentationRegistry.getInstrumentation().runOnMainSync {
|
||||
animator.animateBubbleInForStashed(bubble)
|
||||
animator.animateBubbleInForStashed(bubble, isExpanding = false)
|
||||
}
|
||||
|
||||
// wait for the animation to start
|
||||
@@ -211,7 +211,7 @@ class BubbleBarViewAnimatorTest {
|
||||
BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler)
|
||||
|
||||
InstrumentationRegistry.getInstrumentation().runOnMainSync {
|
||||
animator.animateBubbleInForStashed(bubble)
|
||||
animator.animateBubbleInForStashed(bubble, isExpanding = false)
|
||||
}
|
||||
|
||||
// let the animation start and wait for it to complete
|
||||
@@ -252,7 +252,7 @@ class BubbleBarViewAnimatorTest {
|
||||
BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler)
|
||||
|
||||
InstrumentationRegistry.getInstrumentation().runOnMainSync {
|
||||
animator.animateBubbleInForStashed(bubble)
|
||||
animator.animateBubbleInForStashed(bubble, isExpanding = false)
|
||||
}
|
||||
|
||||
// wait for the animation to start
|
||||
@@ -269,6 +269,123 @@ class BubbleBarViewAnimatorTest {
|
||||
assertThat(animatorScheduler.delayedBlock).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun animateBubbleInForStashed_autoExpanding() {
|
||||
setUpBubbleBar()
|
||||
setUpBubbleStashController()
|
||||
|
||||
val handle = View(context)
|
||||
val handleAnimator = PhysicsAnimator.getInstance(handle)
|
||||
whenever(bubbleStashController.getStashedHandlePhysicsAnimator()).thenReturn(handleAnimator)
|
||||
|
||||
val animator =
|
||||
BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler)
|
||||
|
||||
InstrumentationRegistry.getInstrumentation().runOnMainSync {
|
||||
animator.animateBubbleInForStashed(bubble, isExpanding = true)
|
||||
}
|
||||
|
||||
// wait for the animation to start
|
||||
InstrumentationRegistry.getInstrumentation().runOnMainSync {}
|
||||
PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
|
||||
|
||||
assertThat(handle.alpha).isEqualTo(0)
|
||||
assertThat(handle.translationY)
|
||||
.isEqualTo(DIFF_BETWEEN_HANDLE_AND_BAR_CENTERS + BAR_TRANSLATION_Y_FOR_TASKBAR)
|
||||
assertThat(bubbleBarView.visibility).isEqualTo(VISIBLE)
|
||||
assertThat(bubbleBarView.scaleX).isEqualTo(1)
|
||||
assertThat(bubbleBarView.scaleY).isEqualTo(1)
|
||||
assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR)
|
||||
assertThat(bubbleBarView.isAnimatingNewBubble).isFalse()
|
||||
assertThat(bubbleBarView.isExpanded).isTrue()
|
||||
|
||||
// verify there is no hide animation
|
||||
assertThat(animatorScheduler.delayedBlock).isNull()
|
||||
|
||||
verify(bubbleStashController).showBubbleBarImmediate()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun animateBubbleInForStashed_expandedWhileAnimatingIn() {
|
||||
setUpBubbleBar()
|
||||
setUpBubbleStashController()
|
||||
|
||||
val handle = View(context)
|
||||
val handleAnimator = PhysicsAnimator.getInstance(handle)
|
||||
whenever(bubbleStashController.getStashedHandlePhysicsAnimator()).thenReturn(handleAnimator)
|
||||
|
||||
val animator =
|
||||
BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler)
|
||||
|
||||
InstrumentationRegistry.getInstrumentation().runOnMainSync {
|
||||
animator.animateBubbleInForStashed(bubble, isExpanding = false)
|
||||
}
|
||||
|
||||
// wait for the animation to start
|
||||
InstrumentationRegistry.getInstrumentation().runOnMainSync {}
|
||||
PhysicsAnimatorTestUtils.blockUntilFirstAnimationFrameWhereTrue(handleAnimator) { true }
|
||||
|
||||
handleAnimator.assertIsRunning()
|
||||
assertThat(bubbleBarView.isAnimatingNewBubble).isTrue()
|
||||
// verify the hide bubble animation is pending
|
||||
assertThat(animatorScheduler.delayedBlock).isNotNull()
|
||||
|
||||
InstrumentationRegistry.getInstrumentation().runOnMainSync {
|
||||
animator.expandedWhileAnimating()
|
||||
}
|
||||
|
||||
// let the animation finish
|
||||
PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
|
||||
|
||||
// verify that the hide animation was canceled
|
||||
assertThat(animatorScheduler.delayedBlock).isNull()
|
||||
|
||||
assertThat(handle.alpha).isEqualTo(0)
|
||||
assertThat(handle.translationY)
|
||||
.isEqualTo(DIFF_BETWEEN_HANDLE_AND_BAR_CENTERS + BAR_TRANSLATION_Y_FOR_TASKBAR)
|
||||
verifyBubbleBarIsExpandedWithTranslation(BAR_TRANSLATION_Y_FOR_TASKBAR)
|
||||
assertThat(bubbleBarView.isAnimatingNewBubble).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun animateBubbleInForStashed_expandedWhileFullyIn() {
|
||||
setUpBubbleBar()
|
||||
setUpBubbleStashController()
|
||||
|
||||
val handle = View(context)
|
||||
val handleAnimator = PhysicsAnimator.getInstance(handle)
|
||||
whenever(bubbleStashController.getStashedHandlePhysicsAnimator()).thenReturn(handleAnimator)
|
||||
|
||||
val animator =
|
||||
BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler)
|
||||
|
||||
InstrumentationRegistry.getInstrumentation().runOnMainSync {
|
||||
animator.animateBubbleInForStashed(bubble, isExpanding = false)
|
||||
}
|
||||
|
||||
// wait for the animation to start
|
||||
InstrumentationRegistry.getInstrumentation().runOnMainSync {}
|
||||
// wait for the animation to end
|
||||
PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
|
||||
|
||||
assertThat(bubbleBarView.isAnimatingNewBubble).isTrue()
|
||||
// verify the hide bubble animation is pending
|
||||
assertThat(animatorScheduler.delayedBlock).isNotNull()
|
||||
|
||||
InstrumentationRegistry.getInstrumentation().runOnMainSync {
|
||||
animator.expandedWhileAnimating()
|
||||
}
|
||||
|
||||
// verify that the hide animation was canceled
|
||||
assertThat(animatorScheduler.delayedBlock).isNull()
|
||||
|
||||
assertThat(handle.alpha).isEqualTo(0)
|
||||
assertThat(handle.translationY)
|
||||
.isEqualTo(DIFF_BETWEEN_HANDLE_AND_BAR_CENTERS + BAR_TRANSLATION_Y_FOR_TASKBAR)
|
||||
verifyBubbleBarIsExpandedWithTranslation(BAR_TRANSLATION_Y_FOR_TASKBAR)
|
||||
assertThat(bubbleBarView.isAnimatingNewBubble).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun animateToInitialState_inApp() {
|
||||
setUpBubbleBar()
|
||||
@@ -336,17 +453,11 @@ class BubbleBarViewAnimatorTest {
|
||||
PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
|
||||
|
||||
barAnimator.assertIsNotRunning()
|
||||
assertThat(bubbleBarView.isAnimatingNewBubble).isTrue()
|
||||
assertThat(bubbleBarView.alpha).isEqualTo(1)
|
||||
assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR)
|
||||
|
||||
assertThat(animatorScheduler.delayedBlock).isNotNull()
|
||||
InstrumentationRegistry.getInstrumentation().runOnMainSync(animatorScheduler.delayedBlock!!)
|
||||
|
||||
assertThat(bubbleBarView.isAnimatingNewBubble).isFalse()
|
||||
assertThat(bubbleBarView.alpha).isEqualTo(1)
|
||||
assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_TASKBAR)
|
||||
|
||||
assertThat(animatorScheduler.delayedBlock).isNull()
|
||||
verify(bubbleStashController).showBubbleBarImmediate()
|
||||
}
|
||||
|
||||
@@ -384,6 +495,79 @@ class BubbleBarViewAnimatorTest {
|
||||
verify(bubbleStashController).showBubbleBarImmediate()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun animateToInitialState_expandedWhileAnimatingIn() {
|
||||
setUpBubbleBar()
|
||||
setUpBubbleStashController()
|
||||
whenever(bubbleStashController.bubbleBarTranslationY)
|
||||
.thenReturn(BAR_TRANSLATION_Y_FOR_HOTSEAT)
|
||||
|
||||
val animator =
|
||||
BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler)
|
||||
|
||||
InstrumentationRegistry.getInstrumentation().runOnMainSync {
|
||||
animator.animateToInitialState(bubble, isInApp = false, isExpanding = false)
|
||||
}
|
||||
|
||||
val bubbleBarAnimator = PhysicsAnimator.getInstance(bubbleBarView)
|
||||
|
||||
// wait for the animation to start
|
||||
InstrumentationRegistry.getInstrumentation().runOnMainSync {}
|
||||
PhysicsAnimatorTestUtils.blockUntilFirstAnimationFrameWhereTrue(bubbleBarAnimator) { true }
|
||||
|
||||
bubbleBarAnimator.assertIsRunning()
|
||||
assertThat(bubbleBarView.isAnimatingNewBubble).isTrue()
|
||||
// verify the hide bubble animation is pending
|
||||
assertThat(animatorScheduler.delayedBlock).isNotNull()
|
||||
|
||||
InstrumentationRegistry.getInstrumentation().runOnMainSync {
|
||||
animator.expandedWhileAnimating()
|
||||
}
|
||||
|
||||
// let the animation finish
|
||||
PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
|
||||
|
||||
// verify that the hide animation was canceled
|
||||
assertThat(animatorScheduler.delayedBlock).isNull()
|
||||
|
||||
verifyBubbleBarIsExpandedWithTranslation(BAR_TRANSLATION_Y_FOR_HOTSEAT)
|
||||
assertThat(bubbleBarView.isAnimatingNewBubble).isFalse()
|
||||
verify(bubbleStashController).showBubbleBarImmediate()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun animateToInitialState_expandedWhileFullyIn() {
|
||||
setUpBubbleBar()
|
||||
setUpBubbleStashController()
|
||||
whenever(bubbleStashController.bubbleBarTranslationY)
|
||||
.thenReturn(BAR_TRANSLATION_Y_FOR_HOTSEAT)
|
||||
|
||||
val animator =
|
||||
BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler)
|
||||
|
||||
InstrumentationRegistry.getInstrumentation().runOnMainSync {
|
||||
animator.animateToInitialState(bubble, isInApp = false, isExpanding = false)
|
||||
}
|
||||
|
||||
// wait for the animation to start
|
||||
InstrumentationRegistry.getInstrumentation().runOnMainSync {}
|
||||
PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
|
||||
|
||||
assertThat(bubbleBarView.isAnimatingNewBubble).isTrue()
|
||||
// verify the hide bubble animation is pending
|
||||
assertThat(animatorScheduler.delayedBlock).isNotNull()
|
||||
|
||||
InstrumentationRegistry.getInstrumentation().runOnMainSync {
|
||||
animator.expandedWhileAnimating()
|
||||
}
|
||||
|
||||
// verify that the hide animation was canceled
|
||||
assertThat(animatorScheduler.delayedBlock).isNull()
|
||||
|
||||
verifyBubbleBarIsExpandedWithTranslation(BAR_TRANSLATION_Y_FOR_HOTSEAT)
|
||||
assertThat(bubbleBarView.isAnimatingNewBubble).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun animateBubbleBarForCollapsed() {
|
||||
setUpBubbleBar()
|
||||
@@ -397,7 +581,7 @@ class BubbleBarViewAnimatorTest {
|
||||
BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler)
|
||||
|
||||
InstrumentationRegistry.getInstrumentation().runOnMainSync {
|
||||
animator.animateBubbleBarForCollapsed(bubble)
|
||||
animator.animateBubbleBarForCollapsed(bubble, isExpanding = false)
|
||||
}
|
||||
|
||||
InstrumentationRegistry.getInstrumentation().runOnMainSync {}
|
||||
@@ -424,6 +608,142 @@ class BubbleBarViewAnimatorTest {
|
||||
verify(bubbleStashController).showBubbleBarImmediate()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun animateBubbleBarForCollapsed_autoExpanding() {
|
||||
setUpBubbleBar()
|
||||
setUpBubbleStashController()
|
||||
whenever(bubbleStashController.bubbleBarTranslationY)
|
||||
.thenReturn(BAR_TRANSLATION_Y_FOR_HOTSEAT)
|
||||
|
||||
val barAnimator = PhysicsAnimator.getInstance(bubbleBarView)
|
||||
|
||||
val animator =
|
||||
BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler)
|
||||
|
||||
InstrumentationRegistry.getInstrumentation().runOnMainSync {
|
||||
animator.animateBubbleBarForCollapsed(bubble, isExpanding = true)
|
||||
}
|
||||
|
||||
InstrumentationRegistry.getInstrumentation().runOnMainSync {}
|
||||
// verify we started animating
|
||||
assertThat(bubbleBarView.isAnimatingNewBubble).isTrue()
|
||||
|
||||
// advance the animation handler by the duration of the initial lift
|
||||
InstrumentationRegistry.getInstrumentation().runOnMainSync {
|
||||
animatorTestRule.advanceTimeBy(250)
|
||||
}
|
||||
|
||||
// the lift animation is complete; the spring back animation should start now
|
||||
InstrumentationRegistry.getInstrumentation().runOnMainSync {}
|
||||
barAnimator.assertIsRunning()
|
||||
PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
|
||||
|
||||
// verify there is no hide animation
|
||||
assertThat(animatorScheduler.delayedBlock).isNull()
|
||||
|
||||
assertThat(bubbleBarView.isAnimatingNewBubble).isFalse()
|
||||
assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_HOTSEAT)
|
||||
assertThat(bubbleBarView.isExpanded).isTrue()
|
||||
verify(bubbleStashController).showBubbleBarImmediate()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun animateBubbleBarForCollapsed_expandingWhileAnimatingIn() {
|
||||
setUpBubbleBar()
|
||||
setUpBubbleStashController()
|
||||
whenever(bubbleStashController.bubbleBarTranslationY)
|
||||
.thenReturn(BAR_TRANSLATION_Y_FOR_HOTSEAT)
|
||||
|
||||
val barAnimator = PhysicsAnimator.getInstance(bubbleBarView)
|
||||
|
||||
val animator =
|
||||
BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler)
|
||||
|
||||
InstrumentationRegistry.getInstrumentation().runOnMainSync {
|
||||
animator.animateBubbleBarForCollapsed(bubble, isExpanding = false)
|
||||
}
|
||||
|
||||
InstrumentationRegistry.getInstrumentation().runOnMainSync {}
|
||||
// verify we started animating
|
||||
assertThat(bubbleBarView.isAnimatingNewBubble).isTrue()
|
||||
|
||||
// advance the animation handler by the duration of the initial lift
|
||||
InstrumentationRegistry.getInstrumentation().runOnMainSync {
|
||||
animatorTestRule.advanceTimeBy(250)
|
||||
}
|
||||
|
||||
// the lift animation is complete; the spring back animation should start now
|
||||
InstrumentationRegistry.getInstrumentation().runOnMainSync {}
|
||||
barAnimator.assertIsRunning()
|
||||
PhysicsAnimatorTestUtils.blockUntilFirstAnimationFrameWhereTrue(barAnimator) { true }
|
||||
|
||||
// verify there is a pending hide animation
|
||||
assertThat(animatorScheduler.delayedBlock).isNotNull()
|
||||
assertThat(bubbleBarView.isAnimatingNewBubble).isTrue()
|
||||
|
||||
InstrumentationRegistry.getInstrumentation().runOnMainSync {
|
||||
animator.expandedWhileAnimating()
|
||||
}
|
||||
|
||||
// let the animation finish
|
||||
PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
|
||||
|
||||
// verify that the hide animation was canceled
|
||||
assertThat(animatorScheduler.delayedBlock).isNull()
|
||||
|
||||
assertThat(bubbleBarView.isAnimatingNewBubble).isFalse()
|
||||
assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_HOTSEAT)
|
||||
assertThat(bubbleBarView.isExpanded).isTrue()
|
||||
verify(bubbleStashController).showBubbleBarImmediate()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun animateBubbleBarForCollapsed_expandingWhileFullyIn() {
|
||||
setUpBubbleBar()
|
||||
setUpBubbleStashController()
|
||||
whenever(bubbleStashController.bubbleBarTranslationY)
|
||||
.thenReturn(BAR_TRANSLATION_Y_FOR_HOTSEAT)
|
||||
|
||||
val barAnimator = PhysicsAnimator.getInstance(bubbleBarView)
|
||||
|
||||
val animator =
|
||||
BubbleBarViewAnimator(bubbleBarView, bubbleStashController, animatorScheduler)
|
||||
|
||||
InstrumentationRegistry.getInstrumentation().runOnMainSync {
|
||||
animator.animateBubbleBarForCollapsed(bubble, isExpanding = false)
|
||||
}
|
||||
|
||||
InstrumentationRegistry.getInstrumentation().runOnMainSync {}
|
||||
// verify we started animating
|
||||
assertThat(bubbleBarView.isAnimatingNewBubble).isTrue()
|
||||
|
||||
// advance the animation handler by the duration of the initial lift
|
||||
InstrumentationRegistry.getInstrumentation().runOnMainSync {
|
||||
animatorTestRule.advanceTimeBy(250)
|
||||
}
|
||||
|
||||
// the lift animation is complete; the spring back animation should start now
|
||||
InstrumentationRegistry.getInstrumentation().runOnMainSync {}
|
||||
barAnimator.assertIsRunning()
|
||||
PhysicsAnimatorTestUtils.blockUntilAnimationsEnd(DynamicAnimation.TRANSLATION_Y)
|
||||
|
||||
// verify there is a pending hide animation
|
||||
assertThat(animatorScheduler.delayedBlock).isNotNull()
|
||||
assertThat(bubbleBarView.isAnimatingNewBubble).isTrue()
|
||||
|
||||
InstrumentationRegistry.getInstrumentation().runOnMainSync {
|
||||
animator.expandedWhileAnimating()
|
||||
}
|
||||
|
||||
// verify that the hide animation was canceled
|
||||
assertThat(animatorScheduler.delayedBlock).isNull()
|
||||
|
||||
assertThat(bubbleBarView.isAnimatingNewBubble).isFalse()
|
||||
assertThat(bubbleBarView.translationY).isEqualTo(BAR_TRANSLATION_Y_FOR_HOTSEAT)
|
||||
assertThat(bubbleBarView.isExpanded).isTrue()
|
||||
verify(bubbleStashController).showBubbleBarImmediate()
|
||||
}
|
||||
|
||||
private fun setUpBubbleBar() {
|
||||
bubbleBarView = BubbleBarView(context)
|
||||
InstrumentationRegistry.getInstrumentation().runOnMainSync {
|
||||
@@ -459,6 +779,14 @@ class BubbleBarViewAnimatorTest {
|
||||
.thenReturn(BAR_TRANSLATION_Y_FOR_TASKBAR)
|
||||
}
|
||||
|
||||
private fun verifyBubbleBarIsExpandedWithTranslation(ty: Float) {
|
||||
assertThat(bubbleBarView.visibility).isEqualTo(VISIBLE)
|
||||
assertThat(bubbleBarView.scaleX).isEqualTo(1)
|
||||
assertThat(bubbleBarView.scaleY).isEqualTo(1)
|
||||
assertThat(bubbleBarView.translationY).isEqualTo(ty)
|
||||
assertThat(bubbleBarView.isExpanded).isTrue()
|
||||
}
|
||||
|
||||
private fun <T> PhysicsAnimator<T>.assertIsRunning() {
|
||||
InstrumentationRegistry.getInstrumentation().runOnMainSync {
|
||||
assertThat(isRunning()).isTrue()
|
||||
|
||||
+2
@@ -24,6 +24,7 @@ import com.android.launcher3.taskbar.rules.TaskbarModeRule.Mode.TRANSIENT
|
||||
import com.android.launcher3.taskbar.rules.TaskbarModeRule.TaskbarMode
|
||||
import com.android.launcher3.util.DisplayController
|
||||
import com.android.launcher3.util.LauncherMultivalentJUnit
|
||||
import com.android.launcher3.util.LauncherMultivalentJUnit.EmulatedDevices
|
||||
import com.android.launcher3.util.NavigationMode
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import org.junit.Rule
|
||||
@@ -31,6 +32,7 @@ import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(LauncherMultivalentJUnit::class)
|
||||
@EmulatedDevices(["pixelFoldable2023", "pixelTablet2023"])
|
||||
class TaskbarModeRuleTest {
|
||||
|
||||
private val context = TaskbarWindowSandboxContext.create(getInstrumentation().targetContext)
|
||||
|
||||
+2
@@ -19,6 +19,7 @@ package com.android.launcher3.taskbar.rules
|
||||
import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
|
||||
import com.android.launcher3.util.DisplayController
|
||||
import com.android.launcher3.util.LauncherMultivalentJUnit
|
||||
import com.android.launcher3.util.LauncherMultivalentJUnit.EmulatedDevices
|
||||
import com.android.launcher3.util.window.WindowManagerProxy
|
||||
import com.google.android.apps.nexuslauncher.deviceemulator.TestWindowManagerProxy
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
@@ -28,6 +29,7 @@ import org.junit.runner.RunWith
|
||||
import org.junit.runners.model.Statement
|
||||
|
||||
@RunWith(LauncherMultivalentJUnit::class)
|
||||
@EmulatedDevices(["pixelFoldable2023", "pixelTablet2023"])
|
||||
class TaskbarPinningPreferenceRuleTest {
|
||||
private val context = TaskbarWindowSandboxContext.create(getInstrumentation().targetContext)
|
||||
|
||||
|
||||
+2
@@ -19,6 +19,7 @@ package com.android.launcher3.taskbar.rules
|
||||
import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
|
||||
import com.android.launcher3.LauncherPrefs.Companion.TASKBAR_PINNING
|
||||
import com.android.launcher3.util.LauncherMultivalentJUnit
|
||||
import com.android.launcher3.util.LauncherMultivalentJUnit.EmulatedDevices
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import org.junit.Test
|
||||
import org.junit.runner.Description
|
||||
@@ -26,6 +27,7 @@ import org.junit.runner.RunWith
|
||||
import org.junit.runners.model.Statement
|
||||
|
||||
@RunWith(LauncherMultivalentJUnit::class)
|
||||
@EmulatedDevices(["pixelFoldable2023", "pixelTablet2023"])
|
||||
class TaskbarPreferenceRuleTest {
|
||||
|
||||
private val context = TaskbarWindowSandboxContext.create(getInstrumentation().targetContext)
|
||||
|
||||
+1
-1
@@ -34,7 +34,7 @@ import org.junit.runner.RunWith
|
||||
import org.junit.runners.model.Statement
|
||||
|
||||
@RunWith(LauncherMultivalentJUnit::class)
|
||||
@EmulatedDevices(["pixelFoldable2023"])
|
||||
@EmulatedDevices(["pixelFoldable2023", "pixelTablet2023"])
|
||||
class TaskbarUnitTestRuleTest {
|
||||
|
||||
private val context = TaskbarWindowSandboxContext.create(getInstrumentation().targetContext)
|
||||
|
||||
+1
@@ -25,6 +25,7 @@ import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(LauncherMultivalentJUnit::class)
|
||||
@LauncherMultivalentJUnit.EmulatedDevices(["pixelFoldable2023", "pixelTablet2023"])
|
||||
class TaskbarWindowSandboxContextTest {
|
||||
|
||||
private val context = TaskbarWindowSandboxContext.create(getInstrumentation().targetContext)
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* 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
|
||||
|
||||
import android.platform.test.rule.AllowedDevices
|
||||
import android.platform.test.rule.DeviceProduct
|
||||
import android.platform.test.rule.IgnoreLimit
|
||||
import androidx.test.uiautomator.By
|
||||
import androidx.test.uiautomator.Until
|
||||
import com.android.launcher3.BuildConfig
|
||||
import com.android.launcher3.ui.AbstractLauncherUiTest
|
||||
import com.android.launcher3.ui.PortraitLandscapeRunner.PortraitLandscape
|
||||
import com.android.launcher3.uioverrides.QuickstepLauncher
|
||||
import com.google.common.truth.Truth.assertWithMessage
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
|
||||
/** Test Desktop windowing in Overview. */
|
||||
@AllowedDevices(allowed = [DeviceProduct.CF_TABLET, DeviceProduct.TANGORPRO])
|
||||
@IgnoreLimit(ignoreLimit = BuildConfig.IS_STUDIO_BUILD)
|
||||
class TaplTestsOverviewDesktop : AbstractLauncherUiTest<QuickstepLauncher?>() {
|
||||
@Before
|
||||
fun setup() {
|
||||
val overview = mLauncher.goHome().switchToOverview()
|
||||
if (overview.hasTasks()) {
|
||||
overview.dismissAllTasks()
|
||||
}
|
||||
startTestAppsWithCheck()
|
||||
mLauncher.goHome()
|
||||
}
|
||||
|
||||
@Test
|
||||
@PortraitLandscape
|
||||
fun enterDesktopViaOverviewMenu() {
|
||||
// Move last launched TEST_ACTIVITY_2 into Desktop
|
||||
mLauncher.workspace
|
||||
.switchToOverview()
|
||||
.getTestActivityTask(TEST_ACTIVITY_2)
|
||||
.tapMenu()
|
||||
.tapDesktopMenuItem()
|
||||
assertTestAppLaunched(TEST_ACTIVITY_2)
|
||||
|
||||
// Scroll back to TEST_ACTIVITY_1, then move it into Desktop
|
||||
mLauncher
|
||||
.goHome()
|
||||
.switchToOverview()
|
||||
.apply { flingForward() }
|
||||
.getTestActivityTask(TEST_ACTIVITY_1)
|
||||
.tapMenu()
|
||||
.tapDesktopMenuItem()
|
||||
TEST_ACTIVITIES.forEach { assertTestAppLaunched(it) }
|
||||
|
||||
// Launch static DesktopTaskView
|
||||
val desktop =
|
||||
mLauncher.goHome().switchToOverview().getTestActivityTask(TEST_ACTIVITIES).open()
|
||||
TEST_ACTIVITIES.forEach { assertTestAppLaunched(it) }
|
||||
|
||||
// Launch live-tile DesktopTaskView
|
||||
desktop.switchToOverview().getTestActivityTask(TEST_ACTIVITIES).open()
|
||||
TEST_ACTIVITIES.forEach { assertTestAppLaunched(it) }
|
||||
}
|
||||
|
||||
private fun startTestAppsWithCheck() {
|
||||
TEST_ACTIVITIES.forEach {
|
||||
startTestActivity(it)
|
||||
executeOnLauncher { launcher ->
|
||||
assertWithMessage(
|
||||
"Launcher activity is the top activity; expecting TestActivity$it"
|
||||
)
|
||||
.that(isInLaunchedApp(launcher))
|
||||
.isTrue()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun assertTestAppLaunched(index: Int) {
|
||||
assertWithMessage("TestActivity$index not opened in Desktop")
|
||||
.that(
|
||||
mDevice.wait(
|
||||
Until.hasObject(By.pkg(getAppPackageName()).text("TestActivity$index")),
|
||||
DEFAULT_UI_TIMEOUT
|
||||
)
|
||||
)
|
||||
.isTrue()
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TEST_ACTIVITY_1 = 2
|
||||
const val TEST_ACTIVITY_2 = 3
|
||||
val TEST_ACTIVITIES = listOf(TEST_ACTIVITY_1, TEST_ACTIVITY_2)
|
||||
}
|
||||
}
|
||||
@@ -306,6 +306,7 @@
|
||||
<dimen name="blur_size_medium_outline">2dp</dimen>
|
||||
<dimen name="blur_size_click_shadow">4dp</dimen>
|
||||
<dimen name="click_shadow_high_shift">2dp</dimen>
|
||||
<dimen name="app_title_icon_shadow_inset">1dp</dimen>
|
||||
|
||||
<!-- Pending widget -->
|
||||
<dimen name="pending_widget_min_padding">8dp</dimen>
|
||||
|
||||
@@ -873,7 +873,7 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver,
|
||||
if (drawable == null) {
|
||||
setText(text);
|
||||
Log.w(TAG, "setTextWithStartIcon: start icon Drawable not found from resources"
|
||||
+ ", will just set text instead. text=" + text);
|
||||
+ ", will just set text instead.");
|
||||
return;
|
||||
}
|
||||
drawable.setTint(getCurrentTextColor());
|
||||
|
||||
@@ -20,7 +20,6 @@ import static com.android.launcher3.testing.shared.TestProtocol.SCROLL_FINISHED_
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Log;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.accessibility.AccessibilityNodeInfo;
|
||||
@@ -29,7 +28,6 @@ import androidx.annotation.Nullable;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.android.launcher3.compat.AccessibilityManagerCompat;
|
||||
import com.android.launcher3.testing.shared.TestProtocol;
|
||||
import com.android.launcher3.views.RecyclerViewFastScroller;
|
||||
|
||||
|
||||
@@ -189,10 +187,6 @@ public abstract class FastScrollRecyclerView extends RecyclerView {
|
||||
* Scrolls this recycler view to the top.
|
||||
*/
|
||||
public void scrollToTop() {
|
||||
if (TestProtocol.sDebugTracing) {
|
||||
Log.d(TestProtocol.PRIVATE_SPACE_SCROLL_FAILURE, "FastScrollRecyclerView#scrollToTop",
|
||||
new Exception());
|
||||
}
|
||||
if (mScrollbar != null) {
|
||||
mScrollbar.reattachThumbToScroll();
|
||||
}
|
||||
|
||||
@@ -19,11 +19,19 @@ package com.android.launcher3.views;
|
||||
import static com.android.launcher3.icons.GraphicsUtils.setColorAlphaBound;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Build;
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableString;
|
||||
import android.text.style.ImageSpan;
|
||||
import android.util.AttributeSet;
|
||||
import android.widget.TextView;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
||||
import com.android.launcher3.BubbleTextView;
|
||||
import com.android.launcher3.R;
|
||||
@@ -45,22 +53,65 @@ public class DoubleShadowBubbleTextView extends BubbleTextView {
|
||||
|
||||
public DoubleShadowBubbleTextView(Context context, AttributeSet attrs, int defStyle) {
|
||||
super(context, attrs, defStyle);
|
||||
mShadowInfo = new ShadowInfo(context, attrs, defStyle);
|
||||
setShadowLayer(mShadowInfo.ambientShadowBlur, 0, 0, mShadowInfo.ambientShadowColor);
|
||||
mShadowInfo = ShadowInfo.Companion.fromContext(context, attrs, defStyle);
|
||||
setShadowLayer(
|
||||
mShadowInfo.getAmbientShadowBlur(),
|
||||
0,
|
||||
0,
|
||||
mShadowInfo.getAmbientShadowColor()
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTextWithStartIcon(CharSequence text, @DrawableRes int drawableId) {
|
||||
Drawable drawable = getContext().getDrawable(drawableId);
|
||||
if (drawable == null) {
|
||||
setText(text);
|
||||
Log.w(TAG, "setTextWithStartIcon: start icon Drawable not found from resources"
|
||||
+ ", will just set text instead.");
|
||||
return;
|
||||
}
|
||||
drawable.setTint(getCurrentTextColor());
|
||||
int textSize = Math.round(getTextSize());
|
||||
ImageSpan imageSpan;
|
||||
if (!skipDoubleShadow() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
drawable = getDoubleShadowDrawable(drawable, textSize);
|
||||
}
|
||||
drawable.setBounds(0, 0, textSize, textSize);
|
||||
imageSpan = new ImageSpan(drawable, ImageSpan.ALIGN_CENTER);
|
||||
// First space will be replaced with Drawable, second space is for space before text.
|
||||
SpannableString spannable = new SpannableString(" " + text);
|
||||
spannable.setSpan(imageSpan, 0, 1, Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
|
||||
setText(spannable);
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.S)
|
||||
private DoubleShadowIconDrawable getDoubleShadowDrawable(
|
||||
@NonNull Drawable drawable, int textSize
|
||||
) {
|
||||
// add some padding via inset to avoid shadow clipping
|
||||
int iconInsetSize = getContext().getResources()
|
||||
.getDimensionPixelSize(R.dimen.app_title_icon_shadow_inset);
|
||||
return new DoubleShadowIconDrawable(
|
||||
mShadowInfo,
|
||||
drawable,
|
||||
textSize,
|
||||
iconInsetSize
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDraw(Canvas canvas) {
|
||||
// If text is transparent or shadow alpha is 0, don't draw any shadow
|
||||
if (mShadowInfo.skipDoubleShadow(this)) {
|
||||
if (skipDoubleShadow()) {
|
||||
super.onDraw(canvas);
|
||||
return;
|
||||
}
|
||||
int alpha = Color.alpha(getCurrentTextColor());
|
||||
|
||||
// We enhance the shadow by drawing the shadow twice
|
||||
getPaint().setShadowLayer(mShadowInfo.ambientShadowBlur, 0, 0,
|
||||
getTextShadowColor(mShadowInfo.ambientShadowColor, alpha));
|
||||
getPaint().setShadowLayer(mShadowInfo.getAmbientShadowBlur(), 0, 0,
|
||||
getTextShadowColor(mShadowInfo.getAmbientShadowColor(), alpha));
|
||||
|
||||
drawWithoutDot(canvas);
|
||||
canvas.save();
|
||||
@@ -69,10 +120,10 @@ public class DoubleShadowBubbleTextView extends BubbleTextView {
|
||||
getScrollY() + getHeight());
|
||||
|
||||
getPaint().setShadowLayer(
|
||||
mShadowInfo.keyShadowBlur,
|
||||
mShadowInfo.keyShadowOffsetX,
|
||||
mShadowInfo.keyShadowOffsetY,
|
||||
getTextShadowColor(mShadowInfo.keyShadowColor, alpha));
|
||||
mShadowInfo.getKeyShadowBlur(),
|
||||
mShadowInfo.getKeyShadowOffsetX(),
|
||||
mShadowInfo.getKeyShadowOffsetY(),
|
||||
getTextShadowColor(mShadowInfo.getKeyShadowColor(), alpha));
|
||||
drawWithoutDot(canvas);
|
||||
canvas.restore();
|
||||
|
||||
@@ -80,55 +131,30 @@ public class DoubleShadowBubbleTextView extends BubbleTextView {
|
||||
drawRunningAppIndicatorIfNecessary(canvas);
|
||||
}
|
||||
|
||||
public static class ShadowInfo {
|
||||
public final float ambientShadowBlur;
|
||||
public final int ambientShadowColor;
|
||||
|
||||
public final float keyShadowBlur;
|
||||
public final float keyShadowOffsetX;
|
||||
public final float keyShadowOffsetY;
|
||||
public final int keyShadowColor;
|
||||
|
||||
public ShadowInfo(Context c, AttributeSet attrs, int defStyle) {
|
||||
|
||||
TypedArray a = c.obtainStyledAttributes(
|
||||
attrs, R.styleable.ShadowInfo, defStyle, 0);
|
||||
|
||||
ambientShadowBlur = a.getDimensionPixelSize(
|
||||
R.styleable.ShadowInfo_ambientShadowBlur, 0);
|
||||
ambientShadowColor = a.getColor(R.styleable.ShadowInfo_ambientShadowColor, 0);
|
||||
|
||||
keyShadowBlur = a.getDimensionPixelSize(R.styleable.ShadowInfo_keyShadowBlur, 0);
|
||||
keyShadowOffsetX = a.getDimensionPixelSize(R.styleable.ShadowInfo_keyShadowOffsetX, 0);
|
||||
keyShadowOffsetY = a.getDimensionPixelSize(R.styleable.ShadowInfo_keyShadowOffsetY, 0);
|
||||
keyShadowColor = a.getColor(R.styleable.ShadowInfo_keyShadowColor, 0);
|
||||
a.recycle();
|
||||
}
|
||||
|
||||
public boolean skipDoubleShadow(TextView textView) {
|
||||
int textAlpha = Color.alpha(textView.getCurrentTextColor());
|
||||
int keyShadowAlpha = Color.alpha(keyShadowColor);
|
||||
int ambientShadowAlpha = Color.alpha(ambientShadowColor);
|
||||
if (textAlpha == 0 || (keyShadowAlpha == 0 && ambientShadowAlpha == 0)) {
|
||||
textView.getPaint().clearShadowLayer();
|
||||
return true;
|
||||
} else if (ambientShadowAlpha > 0 && keyShadowAlpha == 0) {
|
||||
textView.getPaint().setShadowLayer(ambientShadowBlur, 0, 0,
|
||||
getTextShadowColor(ambientShadowColor, textAlpha));
|
||||
return true;
|
||||
} else if (keyShadowAlpha > 0 && ambientShadowAlpha == 0) {
|
||||
textView.getPaint().setShadowLayer(
|
||||
keyShadowBlur,
|
||||
keyShadowOffsetX,
|
||||
keyShadowOffsetY,
|
||||
getTextShadowColor(keyShadowColor, textAlpha));
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
private boolean skipDoubleShadow() {
|
||||
int textAlpha = Color.alpha(getCurrentTextColor());
|
||||
int keyShadowAlpha = Color.alpha(mShadowInfo.getKeyShadowColor());
|
||||
int ambientShadowAlpha = Color.alpha(mShadowInfo.getAmbientShadowColor());
|
||||
if (textAlpha == 0 || (keyShadowAlpha == 0 && ambientShadowAlpha == 0)) {
|
||||
getPaint().clearShadowLayer();
|
||||
return true;
|
||||
} else if (ambientShadowAlpha > 0 && keyShadowAlpha == 0) {
|
||||
getPaint().setShadowLayer(mShadowInfo.getAmbientShadowBlur(), 0, 0,
|
||||
getTextShadowColor(mShadowInfo.getAmbientShadowColor(), textAlpha));
|
||||
return true;
|
||||
} else if (keyShadowAlpha > 0 && ambientShadowAlpha == 0) {
|
||||
getPaint().setShadowLayer(
|
||||
mShadowInfo.getKeyShadowBlur(),
|
||||
mShadowInfo.getKeyShadowOffsetX(),
|
||||
mShadowInfo.getKeyShadowOffsetY(),
|
||||
getTextShadowColor(mShadowInfo.getKeyShadowColor(), textAlpha));
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Multiplies the alpha of shadowColor by textAlpha.
|
||||
private static int getTextShadowColor(int shadowColor, int textAlpha) {
|
||||
return setColorAlphaBound(shadowColor,
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
/*
|
||||
* 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.views
|
||||
|
||||
import android.content.res.ColorStateList
|
||||
import android.graphics.BlendMode
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.graphics.ColorFilter
|
||||
import android.graphics.PixelFormat
|
||||
import android.graphics.PorterDuff
|
||||
import android.graphics.PorterDuffColorFilter
|
||||
import android.graphics.RenderEffect
|
||||
import android.graphics.RenderNode
|
||||
import android.graphics.Shader
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.graphics.drawable.InsetDrawable
|
||||
import android.os.Build.VERSION_CODES
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.annotation.VisibleForTesting
|
||||
|
||||
/**
|
||||
* Launcher wrapper for Drawables to provide a double shadow effect. Currently for use with
|
||||
* [DoubleShadowBubbleTextView] to provide a similar shadow to inline icons.
|
||||
*/
|
||||
@RequiresApi(VERSION_CODES.S)
|
||||
class DoubleShadowIconDrawable(
|
||||
private val shadowInfo: ShadowInfo,
|
||||
iconDrawable: Drawable,
|
||||
private val iconSize: Int,
|
||||
iconInsetSize: Int
|
||||
) : Drawable() {
|
||||
private val mIconDrawable: InsetDrawable
|
||||
private val mDoubleShadowNode: RenderNode?
|
||||
|
||||
init {
|
||||
mIconDrawable = InsetDrawable(iconDrawable, iconInsetSize)
|
||||
mIconDrawable.setBounds(0, 0, iconSize, iconSize)
|
||||
mDoubleShadowNode = createShadowRenderNode()
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
fun createShadowRenderNode(): RenderNode {
|
||||
val renderNode = RenderNode("DoubleShadowNode")
|
||||
renderNode.setPosition(0, 0, iconSize, iconSize)
|
||||
// Create render effects
|
||||
val ambientShadow =
|
||||
createShadowRenderEffect(
|
||||
shadowInfo.ambientShadowBlur,
|
||||
0f,
|
||||
0f,
|
||||
Color.alpha(shadowInfo.ambientShadowColor).toFloat()
|
||||
)
|
||||
val keyShadow =
|
||||
createShadowRenderEffect(
|
||||
shadowInfo.keyShadowBlur,
|
||||
shadowInfo.keyShadowOffsetX,
|
||||
shadowInfo.keyShadowOffsetY,
|
||||
Color.alpha(shadowInfo.keyShadowColor).toFloat()
|
||||
)
|
||||
val blend = RenderEffect.createBlendModeEffect(ambientShadow, keyShadow, BlendMode.DST_ATOP)
|
||||
renderNode.setRenderEffect(blend)
|
||||
return renderNode
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
fun createShadowRenderEffect(
|
||||
radius: Float,
|
||||
offsetX: Float,
|
||||
offsetY: Float,
|
||||
alpha: Float
|
||||
): RenderEffect {
|
||||
return RenderEffect.createColorFilterEffect(
|
||||
PorterDuffColorFilter(Color.argb(alpha, 0f, 0f, 0f), PorterDuff.Mode.MULTIPLY),
|
||||
RenderEffect.createOffsetEffect(
|
||||
offsetX,
|
||||
offsetY,
|
||||
RenderEffect.createBlurEffect(radius, radius, Shader.TileMode.CLAMP)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun draw(canvas: Canvas) {
|
||||
if (canvas.isHardwareAccelerated && mDoubleShadowNode != null) {
|
||||
if (!mDoubleShadowNode.hasDisplayList()) {
|
||||
// Record render node if its display list is not recorded or discarded
|
||||
// (which happens when it's no longer drawn by anything).
|
||||
val recordingCanvas = mDoubleShadowNode.beginRecording()
|
||||
mIconDrawable.draw(recordingCanvas)
|
||||
mDoubleShadowNode.endRecording()
|
||||
}
|
||||
canvas.drawRenderNode(mDoubleShadowNode)
|
||||
}
|
||||
mIconDrawable.draw(canvas)
|
||||
}
|
||||
|
||||
override fun getIntrinsicHeight() = iconSize
|
||||
|
||||
override fun getIntrinsicWidth() = iconSize
|
||||
|
||||
override fun getOpacity() = PixelFormat.TRANSPARENT
|
||||
|
||||
override fun setAlpha(alpha: Int) {
|
||||
mIconDrawable.alpha = alpha
|
||||
}
|
||||
|
||||
override fun setColorFilter(colorFilter: ColorFilter?) {
|
||||
mIconDrawable.colorFilter = colorFilter
|
||||
}
|
||||
|
||||
override fun setTint(color: Int) {
|
||||
mIconDrawable.setTint(color)
|
||||
}
|
||||
|
||||
override fun setTintList(tint: ColorStateList?) {
|
||||
mIconDrawable.setTintList(tint)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* 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.views
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import com.android.launcher3.R
|
||||
|
||||
/**
|
||||
* Launcher data holder for classes such as [DoubleShadowBubbleTextView] to model shadows for
|
||||
* "double shadow" effect.
|
||||
*/
|
||||
data class ShadowInfo(
|
||||
val ambientShadowBlur: Float,
|
||||
val ambientShadowColor: Int,
|
||||
val keyShadowBlur: Float,
|
||||
val keyShadowOffsetX: Float,
|
||||
val keyShadowOffsetY: Float,
|
||||
val keyShadowColor: Int
|
||||
) {
|
||||
|
||||
companion object {
|
||||
/** Constructs instance of ShadowInfo from Context and given attribute set. */
|
||||
@JvmStatic
|
||||
fun fromContext(context: Context, attrs: AttributeSet?, defStyle: Int): ShadowInfo {
|
||||
val styledAttrs =
|
||||
context.obtainStyledAttributes(attrs, R.styleable.ShadowInfo, defStyle, 0)
|
||||
val shadowInfo =
|
||||
ShadowInfo(
|
||||
ambientShadowBlur =
|
||||
styledAttrs
|
||||
.getDimensionPixelSize(R.styleable.ShadowInfo_ambientShadowBlur, 0)
|
||||
.toFloat(),
|
||||
ambientShadowColor =
|
||||
styledAttrs.getColor(R.styleable.ShadowInfo_ambientShadowColor, 0),
|
||||
keyShadowBlur =
|
||||
styledAttrs
|
||||
.getDimensionPixelSize(R.styleable.ShadowInfo_keyShadowBlur, 0)
|
||||
.toFloat(),
|
||||
keyShadowOffsetX =
|
||||
styledAttrs
|
||||
.getDimensionPixelSize(R.styleable.ShadowInfo_keyShadowOffsetX, 0)
|
||||
.toFloat(),
|
||||
keyShadowOffsetY =
|
||||
styledAttrs
|
||||
.getDimensionPixelSize(R.styleable.ShadowInfo_keyShadowOffsetY, 0)
|
||||
.toFloat(),
|
||||
keyShadowColor = styledAttrs.getColor(R.styleable.ShadowInfo_keyShadowColor, 0)
|
||||
)
|
||||
styledAttrs.recycle()
|
||||
return shadowInfo
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -173,8 +173,6 @@ public final class TestProtocol {
|
||||
public static final String TEST_DRAG_APP_ICON_TO_MULTIPLE_WORKSPACES_FAILURE = "b/326908466";
|
||||
public static final String WIDGET_CONFIG_NULL_EXTRA_INTENT = "b/324419890";
|
||||
public static final String OVERVIEW_SELECT_TOOLTIP_MISALIGNED = "b/332485341";
|
||||
public static final String PRIVATE_SPACE_SCROLL_FAILURE = "b/339737008";
|
||||
|
||||
public static final String REQUEST_FLAG_ENABLE_GRID_ONLY_OVERVIEW = "enable-grid-only-overview";
|
||||
public static final String REQUEST_FLAG_ENABLE_APP_PAIRS = "enable-app-pairs";
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ import android.os.RemoteException;
|
||||
import android.os.UserHandle;
|
||||
import android.os.UserManager;
|
||||
import android.platform.test.flag.junit.SetFlagsRule;
|
||||
import android.platform.test.rule.LimitDevicesRule;
|
||||
import android.system.OsConstants;
|
||||
import android.util.Log;
|
||||
|
||||
@@ -222,6 +223,9 @@ public abstract class AbstractLauncherUiTest<LAUNCHER_TYPE extends Launcher> {
|
||||
@Rule
|
||||
public ExtendedLongPressTimeoutRule mLongPressTimeoutRule = new ExtendedLongPressTimeoutRule();
|
||||
|
||||
@Rule
|
||||
public LimitDevicesRule mlimitDevicesRule = new LimitDevicesRule();
|
||||
|
||||
public static void initialize(AbstractLauncherUiTest test) throws Exception {
|
||||
test.reinitializeLauncherData();
|
||||
test.mDevice.pressHome();
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* 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.ui
|
||||
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.Drawable
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.filters.SmallTest
|
||||
import com.android.launcher3.views.DoubleShadowIconDrawable
|
||||
import com.android.launcher3.views.ShadowInfo
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mockito.kotlin.mock
|
||||
import org.mockito.kotlin.spy
|
||||
import org.mockito.kotlin.verify
|
||||
|
||||
@SmallTest
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class DoubleShadowIconDrawableTest {
|
||||
|
||||
@Test
|
||||
fun `DoubleShadowIconDrawable is setup correctly from given ShadowInfo`() {
|
||||
// Given
|
||||
val shadowInfo: ShadowInfo = mock()
|
||||
val originalDrawable: Drawable = mock()
|
||||
val iconSize = 2
|
||||
val iconInsetSize = 1
|
||||
// When
|
||||
val drawableUnderTest =
|
||||
DoubleShadowIconDrawable(shadowInfo, originalDrawable, iconSize, iconInsetSize)
|
||||
// Then
|
||||
assertThat(drawableUnderTest.intrinsicHeight).isEqualTo(iconSize)
|
||||
assertThat(drawableUnderTest.intrinsicWidth).isEqualTo(iconSize)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `createShadowRenderNode creates RenderNode for shadow effects`() {
|
||||
// Given
|
||||
val shadowInfo =
|
||||
ShadowInfo(
|
||||
ambientShadowBlur = 1f,
|
||||
ambientShadowColor = 2,
|
||||
keyShadowBlur = 3f,
|
||||
keyShadowOffsetX = 4f,
|
||||
keyShadowOffsetY = 5f,
|
||||
keyShadowColor = 6
|
||||
)
|
||||
val originalDrawable: Drawable = mock()
|
||||
val iconSize = 2
|
||||
val iconInsetSize = 1
|
||||
// When
|
||||
val shadowDrawableUnderTest =
|
||||
spy(DoubleShadowIconDrawable(shadowInfo, originalDrawable, iconSize, iconInsetSize))
|
||||
shadowDrawableUnderTest.createShadowRenderNode()
|
||||
// Then
|
||||
verify(shadowDrawableUnderTest)
|
||||
.createShadowRenderEffect(
|
||||
shadowInfo.ambientShadowBlur,
|
||||
0f,
|
||||
0f,
|
||||
Color.alpha(shadowInfo.ambientShadowColor).toFloat()
|
||||
)
|
||||
verify(shadowDrawableUnderTest)
|
||||
.createShadowRenderEffect(
|
||||
shadowInfo.keyShadowBlur,
|
||||
shadowInfo.keyShadowOffsetX,
|
||||
shadowInfo.keyShadowOffsetY,
|
||||
Color.alpha(shadowInfo.keyShadowColor).toFloat()
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* 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.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.TypedArray
|
||||
import android.util.AttributeSet
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.filters.SmallTest
|
||||
import com.android.launcher3.R
|
||||
import com.android.launcher3.views.ShadowInfo
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mockito.kotlin.doReturn
|
||||
import org.mockito.kotlin.mock
|
||||
import org.mockito.kotlin.whenever
|
||||
|
||||
@SmallTest
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class ShadowInfoTest {
|
||||
|
||||
@Test
|
||||
fun `ShadowInfo is created correctly from context`() {
|
||||
// Given
|
||||
val mockContext: Context = mock()
|
||||
val mockAttrs: AttributeSet = mock()
|
||||
val styledAttrs: TypedArray = mock()
|
||||
val expectedShadowInfo =
|
||||
ShadowInfo(
|
||||
ambientShadowBlur = 1f,
|
||||
ambientShadowColor = 2,
|
||||
keyShadowBlur = 3f,
|
||||
keyShadowOffsetX = 4f,
|
||||
keyShadowOffsetY = 5f,
|
||||
keyShadowColor = 6
|
||||
)
|
||||
doReturn(styledAttrs)
|
||||
.whenever(mockContext)
|
||||
.obtainStyledAttributes(mockAttrs, R.styleable.ShadowInfo, 0, 0)
|
||||
doReturn(1)
|
||||
.whenever(styledAttrs)
|
||||
.getDimensionPixelSize(R.styleable.ShadowInfo_ambientShadowBlur, 0)
|
||||
doReturn(2).whenever(styledAttrs).getColor(R.styleable.ShadowInfo_ambientShadowColor, 0)
|
||||
doReturn(3)
|
||||
.whenever(styledAttrs)
|
||||
.getDimensionPixelSize(R.styleable.ShadowInfo_keyShadowBlur, 0)
|
||||
doReturn(4)
|
||||
.whenever(styledAttrs)
|
||||
.getDimensionPixelSize(R.styleable.ShadowInfo_keyShadowOffsetX, 0)
|
||||
doReturn(5)
|
||||
.whenever(styledAttrs)
|
||||
.getDimensionPixelSize(R.styleable.ShadowInfo_keyShadowOffsetY, 0)
|
||||
doReturn(6).whenever(styledAttrs).getColor(R.styleable.ShadowInfo_keyShadowColor, 0)
|
||||
// When
|
||||
val actualShadowInfo = ShadowInfo.fromContext(mockContext, mockAttrs, 0)
|
||||
// Then
|
||||
assertThat(actualShadowInfo.ambientShadowBlur).isEqualTo(1)
|
||||
assertThat(actualShadowInfo.ambientShadowColor).isEqualTo(2)
|
||||
assertThat(actualShadowInfo.keyShadowBlur).isEqualTo(3)
|
||||
assertThat(actualShadowInfo.keyShadowOffsetX).isEqualTo(4)
|
||||
assertThat(actualShadowInfo.keyShadowOffsetY).isEqualTo(5)
|
||||
assertThat(actualShadowInfo.keyShadowColor).isEqualTo(6)
|
||||
assertThat(actualShadowInfo).isEqualTo(expectedShadowInfo)
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
package com.android.launcher3.tapl;
|
||||
|
||||
import static com.android.launcher3.tapl.BaseOverview.TASK_RES_ID;
|
||||
import static com.android.launcher3.tapl.BaseOverview.TASK_SELECTOR;
|
||||
import static com.android.launcher3.tapl.OverviewTask.TASK_START_EVENT;
|
||||
import static com.android.launcher3.testing.shared.TestProtocol.OVERVIEW_STATE_ORDINAL;
|
||||
|
||||
@@ -117,10 +117,10 @@ public abstract class Background extends LauncherInstrumentation.VisibleContaine
|
||||
// non-tablet overview, snapshots can be on either side of the swiped
|
||||
// task, but we still check that they become visible after swiping and
|
||||
// pausing.
|
||||
mLauncher.waitForOverviewObject(TASK_RES_ID);
|
||||
mLauncher.waitForObjectBySelector(TASK_SELECTOR);
|
||||
if (mLauncher.isTablet()) {
|
||||
List<UiObject2> tasks = mLauncher.getDevice().findObjects(
|
||||
mLauncher.getOverviewObjectSelector(TASK_RES_ID));
|
||||
TASK_SELECTOR);
|
||||
final int centerX = mLauncher.getDevice().getDisplayWidth() / 2;
|
||||
mLauncher.assertTrue(
|
||||
"All tasks not to the left of the swiped task",
|
||||
|
||||
@@ -19,7 +19,9 @@ package com.android.launcher3.tapl;
|
||||
import static android.view.KeyEvent.KEYCODE_ESCAPE;
|
||||
|
||||
import static com.android.launcher3.tapl.LauncherInstrumentation.TASKBAR_RES_ID;
|
||||
import static com.android.launcher3.tapl.LauncherInstrumentation.log;
|
||||
import static com.android.launcher3.tapl.OverviewTask.TASK_START_EVENT;
|
||||
import static com.android.launcher3.tapl.TestHelpers.getOverviewPackageName;
|
||||
import static com.android.launcher3.testing.shared.TestProtocol.NORMAL_STATE_ORDINAL;
|
||||
|
||||
import android.graphics.Rect;
|
||||
@@ -35,9 +37,11 @@ import androidx.test.uiautomator.UiObject2;
|
||||
|
||||
import com.android.launcher3.testing.shared.TestProtocol;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@@ -46,7 +50,9 @@ import java.util.stream.Collectors;
|
||||
*/
|
||||
public class BaseOverview extends LauncherInstrumentation.VisibleContainer {
|
||||
private static final String TAG = "BaseOverview";
|
||||
protected static final String TASK_RES_ID = "task";
|
||||
protected static final BySelector TASK_SELECTOR = By.res(Pattern.compile(
|
||||
getOverviewPackageName()
|
||||
+ ":id/(task_view_single|task_view_grouped|task_view_desktop)"));
|
||||
private static final Pattern EVENT_ALT_ESC_UP = Pattern.compile(
|
||||
"Key event: KeyEvent.*?action=ACTION_UP.*?keyCode=KEYCODE_ESCAPE.*?metaState=0");
|
||||
private static final Pattern EVENT_ENTER_DOWN = Pattern.compile(
|
||||
@@ -56,10 +62,22 @@ public class BaseOverview extends LauncherInstrumentation.VisibleContainer {
|
||||
|
||||
private static final int FLINGS_FOR_DISMISS_LIMIT = 40;
|
||||
|
||||
private final @Nullable UiObject2 mLiveTileTask;
|
||||
|
||||
|
||||
BaseOverview(LauncherInstrumentation launcher) {
|
||||
this(launcher, /*launchedFromApp=*/false);
|
||||
}
|
||||
|
||||
BaseOverview(LauncherInstrumentation launcher, boolean launchedFromApp) {
|
||||
super(launcher);
|
||||
verifyActiveContainer();
|
||||
verifyActionsViewVisibility();
|
||||
if (launchedFromApp) {
|
||||
mLiveTileTask = getCurrentTaskUnchecked();
|
||||
} else {
|
||||
mLiveTileTask = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -79,7 +97,7 @@ public class BaseOverview extends LauncherInstrumentation.VisibleContainer {
|
||||
private void flingForwardImpl() {
|
||||
try (LauncherInstrumentation.Closable c =
|
||||
mLauncher.addContextLayer("want to fling forward in overview")) {
|
||||
LauncherInstrumentation.log("Overview.flingForward before fling");
|
||||
log("Overview.flingForward before fling");
|
||||
final UiObject2 overview = verifyActiveContainer();
|
||||
final int leftMargin =
|
||||
mLauncher.getTargetInsets().left + mLauncher.getEdgeSensitivityWidth();
|
||||
@@ -105,7 +123,7 @@ public class BaseOverview extends LauncherInstrumentation.VisibleContainer {
|
||||
private void flingBackwardImpl() {
|
||||
try (LauncherInstrumentation.Closable c =
|
||||
mLauncher.addContextLayer("want to fling backward in overview")) {
|
||||
LauncherInstrumentation.log("Overview.flingBackward before fling");
|
||||
log("Overview.flingBackward before fling");
|
||||
final UiObject2 overview = verifyActiveContainer();
|
||||
final int rightMargin =
|
||||
mLauncher.getTargetInsets().right + mLauncher.getEdgeSensitivityWidth();
|
||||
@@ -276,37 +294,56 @@ public class BaseOverview extends LauncherInstrumentation.VisibleContainer {
|
||||
*/
|
||||
@NonNull
|
||||
public OverviewTask getCurrentTask() {
|
||||
UiObject2 currentTask = getCurrentTaskUnchecked();
|
||||
mLauncher.assertNotNull("Unable to find a task", currentTask);
|
||||
return new OverviewTask(mLauncher, currentTask, this);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private UiObject2 getCurrentTaskUnchecked() {
|
||||
final List<UiObject2> taskViews = getTasks();
|
||||
mLauncher.assertNotEquals("Unable to find a task", 0, taskViews.size());
|
||||
if (taskViews.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// The widest, and most top-right task should be the current task
|
||||
UiObject2 currentTask = Collections.max(taskViews,
|
||||
return Collections.max(taskViews,
|
||||
Comparator.comparingInt((UiObject2 t) -> t.getVisibleBounds().width())
|
||||
.thenComparingInt((UiObject2 t) -> t.getVisibleCenter().x)
|
||||
.thenComparing(Comparator.comparing(
|
||||
(UiObject2 t) -> t.getVisibleCenter().y).reversed()));
|
||||
return new OverviewTask(mLauncher, currentTask, this);
|
||||
}
|
||||
|
||||
/** Returns an overview task matching TestActivity {@param activityNumber}. */
|
||||
/**
|
||||
* Returns an overview task that contains the specified test activity in its thumbnails.
|
||||
*
|
||||
* @param activityIndex index of TestActivity to match against
|
||||
*/
|
||||
@NonNull
|
||||
public OverviewTask getTestActivityTask(int activityNumber) {
|
||||
public OverviewTask getTestActivityTask(int activityIndex) {
|
||||
return getTestActivityTask(Collections.singleton(activityIndex));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an overview task that contains all the specified test activities in its thumbnails.
|
||||
*
|
||||
* @param activityNumbers collection of indices of TestActivity to match against
|
||||
*/
|
||||
@NonNull
|
||||
public OverviewTask getTestActivityTask(Collection<Integer> activityNumbers) {
|
||||
final List<UiObject2> taskViews = getTasks();
|
||||
mLauncher.assertNotEquals("Unable to find a task", 0, taskViews.size());
|
||||
|
||||
final String activityName = "TestActivity" + activityNumber;
|
||||
UiObject2 task = null;
|
||||
for (UiObject2 taskView : taskViews) {
|
||||
// TODO(b/239452415): Use equals instead of descEndsWith
|
||||
if (taskView.getParent().hasObject(By.descEndsWith(activityName))) {
|
||||
task = taskView;
|
||||
break;
|
||||
}
|
||||
}
|
||||
mLauncher.assertNotNull(
|
||||
"Unable to find a task with " + activityName + " from the task list", task);
|
||||
Optional<UiObject2> task = taskViews.stream().filter(
|
||||
taskView -> activityNumbers.stream().allMatch(activityNumber ->
|
||||
// TODO(b/239452415): Use equals instead of descEndsWith
|
||||
taskView.hasObject(By.descEndsWith("TestActivity" + activityNumber))
|
||||
)).findFirst();
|
||||
|
||||
return new OverviewTask(mLauncher, task, this);
|
||||
mLauncher.assertTrue("Unable to find a task with test activities " + activityNumbers
|
||||
+ " from the task list", task.isPresent());
|
||||
|
||||
return new OverviewTask(mLauncher, task.get(), this);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -328,8 +365,7 @@ public class BaseOverview extends LauncherInstrumentation.VisibleContainer {
|
||||
try (LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
|
||||
"want to get overview tasks")) {
|
||||
verifyActiveContainer();
|
||||
return mLauncher.getDevice().findObjects(
|
||||
mLauncher.getOverviewObjectSelector(TASK_RES_ID));
|
||||
return mLauncher.getDevice().findObjects(TASK_SELECTOR);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -506,4 +542,10 @@ public class BaseOverview extends LauncherInstrumentation.VisibleContainer {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected boolean isLiveTile(UiObject2 task) {
|
||||
// UiObject2.equals returns false even when mLiveTileTask and task have the same node, hence
|
||||
// compare only hashCode as a workaround.
|
||||
return mLiveTileTask != null && mLiveTileTask.hashCode() == task.hashCode();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ import android.view.InputDevice;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.ViewConfiguration;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.test.uiautomator.Condition;
|
||||
import androidx.test.uiautomator.UiDevice;
|
||||
|
||||
@@ -75,6 +76,20 @@ public final class LaunchedAppState extends Background {
|
||||
return false;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public BaseOverview switchToOverview() {
|
||||
try (LauncherInstrumentation.Closable ignored = mLauncher.eventsCheck();
|
||||
LauncherInstrumentation.Closable ignored1 = mLauncher.addContextLayer(
|
||||
"want to switch from background to overview")) {
|
||||
verifyActiveContainer();
|
||||
goToOverviewUnchecked();
|
||||
return mLauncher.is3PLauncher()
|
||||
? new BaseOverview(mLauncher, /*launchedFromApp=*/true)
|
||||
: new Overview(mLauncher, /*launchedFromApp=*/true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the taskbar.
|
||||
*
|
||||
|
||||
@@ -1585,7 +1585,7 @@ public final class LauncherInstrumentation {
|
||||
return objects;
|
||||
}
|
||||
|
||||
private UiObject2 waitForObjectBySelector(BySelector selector) {
|
||||
UiObject2 waitForObjectBySelector(BySelector selector) {
|
||||
Log.d(TEST_DRAG_APP_ICON_TO_MULTIPLE_WORKSPACES_FAILURE,
|
||||
"LauncherInstrumentation.waitForObjectBySelector");
|
||||
final UiObject2 object = mDevice.wait(Until.findObject(selector), WAIT_TIME_MS);
|
||||
|
||||
@@ -22,9 +22,12 @@ import com.android.launcher3.tapl.LauncherInstrumentation.ContainerType;
|
||||
* Overview pane.
|
||||
*/
|
||||
public class Overview extends BaseOverview {
|
||||
|
||||
Overview(LauncherInstrumentation launcher) {
|
||||
super(launcher);
|
||||
this(launcher, /*launchedFromApp=*/false);
|
||||
}
|
||||
|
||||
Overview(LauncherInstrumentation launcher, boolean launchedFromApp) {
|
||||
super(launcher, launchedFromApp);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -40,16 +40,23 @@ import java.util.stream.Collectors;
|
||||
public final class OverviewTask {
|
||||
private static final String SYSTEMUI_PACKAGE = "com.android.systemui";
|
||||
static final Pattern TASK_START_EVENT = Pattern.compile("startActivityFromRecentsAsync");
|
||||
static final Pattern TASK_START_EVENT_DESKTOP = Pattern.compile("launchDesktopFromRecents");
|
||||
static final Pattern TASK_START_EVENT_LIVE_TILE = Pattern.compile(
|
||||
"composeRecentsLaunchAnimator");
|
||||
static final Pattern SPLIT_SELECT_EVENT = Pattern.compile("enterSplitSelect");
|
||||
static final Pattern SPLIT_START_EVENT = Pattern.compile("launchSplitTasks");
|
||||
private final LauncherInstrumentation mLauncher;
|
||||
@NonNull
|
||||
private final UiObject2 mTask;
|
||||
private final TaskViewType mType;
|
||||
private final BaseOverview mOverview;
|
||||
|
||||
OverviewTask(LauncherInstrumentation launcher, UiObject2 task, BaseOverview overview) {
|
||||
OverviewTask(LauncherInstrumentation launcher, @NonNull UiObject2 task, BaseOverview overview) {
|
||||
mLauncher = launcher;
|
||||
mLauncher.assertNotNull("task must not be null", task);
|
||||
mTask = task;
|
||||
mOverview = overview;
|
||||
mType = getType();
|
||||
verifyActiveContainer();
|
||||
}
|
||||
|
||||
@@ -220,7 +227,22 @@ public final class OverviewTask {
|
||||
return new LaunchedAppState(mLauncher);
|
||||
}
|
||||
} else {
|
||||
mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, TASK_START_EVENT);
|
||||
final Pattern event;
|
||||
if (mOverview.isLiveTile(mTask)) {
|
||||
event = TASK_START_EVENT_LIVE_TILE;
|
||||
} else if (mType == TaskViewType.DESKTOP) {
|
||||
event = TASK_START_EVENT_DESKTOP;
|
||||
} else {
|
||||
event = TASK_START_EVENT;
|
||||
}
|
||||
mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, event);
|
||||
|
||||
if (mType == TaskViewType.DESKTOP) {
|
||||
try (LauncherInstrumentation.Closable ignored = mLauncher.addContextLayer(
|
||||
"launched desktop")) {
|
||||
mLauncher.waitForSystemUiObject("desktop_mode_caption");
|
||||
}
|
||||
}
|
||||
return new LaunchedAppState(mLauncher);
|
||||
}
|
||||
}
|
||||
@@ -273,6 +295,17 @@ public final class OverviewTask {
|
||||
return actual.contains(expected);
|
||||
}
|
||||
|
||||
private TaskViewType getType() {
|
||||
String resourceName = mTask.getResourceName();
|
||||
if (resourceName.endsWith("task_view_grouped")) {
|
||||
return TaskViewType.GROUPED;
|
||||
} else if (resourceName.endsWith("task_view_desktop")) {
|
||||
return TaskViewType.DESKTOP;
|
||||
} else {
|
||||
return TaskViewType.SINGLE;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enum used to specify which task is retrieved when it is a split task.
|
||||
*/
|
||||
@@ -292,4 +325,10 @@ public final class OverviewTask {
|
||||
this.iconAppRes = iconAppRes;
|
||||
}
|
||||
}
|
||||
|
||||
private enum TaskViewType {
|
||||
SINGLE,
|
||||
GROUPED,
|
||||
DESKTOP
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,20 +97,35 @@ public class OverviewTaskMenu {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Taps the Desktop item from the overview task menu and returns the LaunchedAppState
|
||||
* representing the Desktop.
|
||||
*/
|
||||
@NonNull
|
||||
public LaunchedAppState tapDesktopMenuItem() {
|
||||
try (LauncherInstrumentation.Closable ignored = mLauncher.eventsCheck();
|
||||
LauncherInstrumentation.Closable ignored1 = mLauncher.addContextLayer(
|
||||
"before tapping the desktop menu item")) {
|
||||
mLauncher.executeAndWaitForLauncherStop(
|
||||
() -> mLauncher.clickLauncherObject(
|
||||
mLauncher.findObjectInContainer(mMenu, By.text("Desktop"))),
|
||||
"tapped desktop menu item");
|
||||
|
||||
try (LauncherInstrumentation.Closable ignored2 = mLauncher.addContextLayer(
|
||||
"tapped desktop menu item")) {
|
||||
mLauncher.waitUntilSystemLauncherObjectGone("overview_panel");
|
||||
mLauncher.waitForSystemUiObject("desktop_mode_caption");
|
||||
return new LaunchedAppState(mLauncher);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns true if an item matching the given string is present in the menu. */
|
||||
public boolean hasMenuItem(String expectedMenuItemText) {
|
||||
UiObject2 menuItem = mLauncher.findObjectInContainer(mMenu, By.text(expectedMenuItemText));
|
||||
return menuItem != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the menu item specified by name if present.
|
||||
*/
|
||||
public OverviewTaskMenuItem getMenuItemByName(String menuItemName) {
|
||||
return new OverviewTaskMenuItem(mLauncher,
|
||||
mLauncher.waitForObjectInContainer(mMenu, By.text(menuItemName)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Taps outside task menu to dismiss it.
|
||||
*/
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2023 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.tapl;
|
||||
|
||||
import android.graphics.Rect;
|
||||
|
||||
import androidx.test.uiautomator.UiObject2;
|
||||
|
||||
/** Represents an item in the overview task menu. */
|
||||
public class OverviewTaskMenuItem {
|
||||
|
||||
private final LauncherInstrumentation mLauncher;
|
||||
private final UiObject2 mMenuItem;
|
||||
|
||||
OverviewTaskMenuItem(LauncherInstrumentation launcher, UiObject2 menuItem) {
|
||||
mLauncher = launcher;
|
||||
mMenuItem = menuItem;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns this menu item's visible bounds.
|
||||
*/
|
||||
public Rect getVisibleBounds() {
|
||||
return mMenuItem.getVisibleBounds();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user