From 394f5d2b7fa3767635f06a40e9d5da2098bad8b2 Mon Sep 17 00:00:00 2001 From: Jeremy Sim Date: Tue, 6 Aug 2024 16:06:15 -0700 Subject: [PATCH 1/6] Fix problem with "Cancel" text not double-bolding This CL changes the way the "Cancel" string is bolded, so that it can automatically react to the "Use bold text" accessibility setting and double-bold itself appropriately. (Previously the bolding was hard coded in XML, this change allows TextView to handle its own bolding.) Fixes: 323111449 Test: Visually confirmed Flag: EXEMPT bugfix Change-Id: Ie50b5a1aab569cebd0101814b6ba896ad4151e7b --- quickstep/res/layout/split_instructions_view.xml | 1 + quickstep/res/values/strings.xml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/quickstep/res/layout/split_instructions_view.xml b/quickstep/res/layout/split_instructions_view.xml index 797ea45be4..a11974c858 100644 --- a/quickstep/res/layout/split_instructions_view.xml +++ b/quickstep/res/layout/split_instructions_view.xml @@ -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"/> \ No newline at end of file diff --git a/quickstep/res/values/strings.xml b/quickstep/res/values/strings.xml index 8bcbb33b85..037a0f6d72 100644 --- a/quickstep/res/values/strings.xml +++ b/quickstep/res/values/strings.xml @@ -237,7 +237,7 @@ Tap another app to use split screen Choose another app to use split screen - Cancel + Cancel Exit split screen selection Choose another app to use split screen From 99147f53e00913d0d1ae628b4d04ca38f3d5f01e Mon Sep 17 00:00:00 2001 From: Charlie Anderson Date: Thu, 1 Aug 2024 10:18:18 -0400 Subject: [PATCH 2/6] Use DoubleShadowIconDrawable to draw shadow behind archived cloud icon - Refactor Launcher3 ShadowInfo out of DoubleShadowBubbleTextView Bug: 350758155 Test: locally verified + screenshot test Flag: com.android.launcher3.use_new_icon_for_archived_apps Change-Id: I8e5e5936296fafbfbf3caf80a936ca1038c125e3 --- res/values/dimens.xml | 1 + src/com/android/launcher3/BubbleTextView.java | 2 +- .../views/DoubleShadowBubbleTextView.java | 140 +++++++++++------- .../views/DoubleShadowIconDrawable.kt | 132 +++++++++++++++++ src/com/android/launcher3/views/ShadowInfo.kt | 68 +++++++++ .../ui/DoubleShadowIconDrawableTest.kt | 86 +++++++++++ .../android/launcher3/ui/ShadowInfoTest.kt | 80 ++++++++++ 7 files changed, 451 insertions(+), 58 deletions(-) create mode 100644 src/com/android/launcher3/views/DoubleShadowIconDrawable.kt create mode 100644 src/com/android/launcher3/views/ShadowInfo.kt create mode 100644 tests/src/com/android/launcher3/ui/DoubleShadowIconDrawableTest.kt create mode 100644 tests/src/com/android/launcher3/ui/ShadowInfoTest.kt diff --git a/res/values/dimens.xml b/res/values/dimens.xml index af91b5a4ff..5e1d8a5ad3 100644 --- a/res/values/dimens.xml +++ b/res/values/dimens.xml @@ -306,6 +306,7 @@ 2dp 4dp 2dp + 1dp 8dp diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java index 2eb50340e3..1eccbff00a 100644 --- a/src/com/android/launcher3/BubbleTextView.java +++ b/src/com/android/launcher3/BubbleTextView.java @@ -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()); diff --git a/src/com/android/launcher3/views/DoubleShadowBubbleTextView.java b/src/com/android/launcher3/views/DoubleShadowBubbleTextView.java index bc66a33b0c..ef66ffe091 100644 --- a/src/com/android/launcher3/views/DoubleShadowBubbleTextView.java +++ b/src/com/android/launcher3/views/DoubleShadowBubbleTextView.java @@ -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, diff --git a/src/com/android/launcher3/views/DoubleShadowIconDrawable.kt b/src/com/android/launcher3/views/DoubleShadowIconDrawable.kt new file mode 100644 index 0000000000..7ac7c94ae4 --- /dev/null +++ b/src/com/android/launcher3/views/DoubleShadowIconDrawable.kt @@ -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) + } +} diff --git a/src/com/android/launcher3/views/ShadowInfo.kt b/src/com/android/launcher3/views/ShadowInfo.kt new file mode 100644 index 0000000000..4f626ec520 --- /dev/null +++ b/src/com/android/launcher3/views/ShadowInfo.kt @@ -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 + } + } +} diff --git a/tests/src/com/android/launcher3/ui/DoubleShadowIconDrawableTest.kt b/tests/src/com/android/launcher3/ui/DoubleShadowIconDrawableTest.kt new file mode 100644 index 0000000000..1cee71c219 --- /dev/null +++ b/tests/src/com/android/launcher3/ui/DoubleShadowIconDrawableTest.kt @@ -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() + ) + } +} diff --git a/tests/src/com/android/launcher3/ui/ShadowInfoTest.kt b/tests/src/com/android/launcher3/ui/ShadowInfoTest.kt new file mode 100644 index 0000000000..ef4dc1a8e7 --- /dev/null +++ b/tests/src/com/android/launcher3/ui/ShadowInfoTest.kt @@ -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) + } +} From 5c4ea5b0ca2432d7e5c92813158aa3fea7469a4b Mon Sep 17 00:00:00 2001 From: Anushree Ganjam Date: Wed, 7 Aug 2024 14:07:22 -0700 Subject: [PATCH 3/6] Remove debug logs Bug: 339737008 Test: Manual Flag: TEST_ONLY Change-Id: I8177fc721f330267bc20abf6e7d587289e45477b --- src/com/android/launcher3/FastScrollRecyclerView.java | 6 ------ .../com/android/launcher3/testing/shared/TestProtocol.java | 2 -- 2 files changed, 8 deletions(-) diff --git a/src/com/android/launcher3/FastScrollRecyclerView.java b/src/com/android/launcher3/FastScrollRecyclerView.java index 960d77a7d9..de1748b911 100644 --- a/src/com/android/launcher3/FastScrollRecyclerView.java +++ b/src/com/android/launcher3/FastScrollRecyclerView.java @@ -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(); } diff --git a/tests/multivalentTests/shared/com/android/launcher3/testing/shared/TestProtocol.java b/tests/multivalentTests/shared/com/android/launcher3/testing/shared/TestProtocol.java index dc3b3213d2..3f4a73aec6 100644 --- a/tests/multivalentTests/shared/com/android/launcher3/testing/shared/TestProtocol.java +++ b/tests/multivalentTests/shared/com/android/launcher3/testing/shared/TestProtocol.java @@ -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"; From 46111b109e03b2b15ff9d6e0d692ae1fd3fd5d64 Mon Sep 17 00:00:00 2001 From: Liran Binyamin Date: Tue, 6 Aug 2024 08:59:26 -0400 Subject: [PATCH 4/6] Support expanding the bar while animating Update the BubbleBarAnimator to handle auto expanding bubbles as well as expand signals that are received while animating the bubble bar. The expansion starts after the bar reaches its peak height. If the hide animation starts before the expansion signal is received, we ignore it. Next step is to delay showing the expanded view until the bubble bar starts expanding. Flag: com.android.wm.shell.enable_bubble_bar Fixes: 353644484 Fixes: 356415377 Bug: 339683389 Test: atest BubbleBarViewAnimatorTest Change-Id: I79a63f0b8728abc1ab3345f0116cbfcba2918643 --- .../bubbles/BubbleBarViewController.java | 15 +- .../BubbleStashedHandleViewController.java | 5 + .../animation/BubbleBarViewAnimator.kt | 164 ++++++-- .../bubbles/stashing/BubbleStashController.kt | 3 + .../PersistentBubbleStashController.kt | 2 + .../TransientBubbleStashController.kt | 2 + .../animation/BubbleBarViewAnimatorTest.kt | 354 +++++++++++++++++- 7 files changed, 502 insertions(+), 43 deletions(-) diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java index 2cdc0ced86..83123b507b 100644 --- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java @@ -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 { diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashedHandleViewController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashedHandleViewController.java index 52f5a29401..8158fe79b4 100644 --- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashedHandleViewController.java +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleStashedHandleViewController.java @@ -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. */ diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt index 0a0cfd007b..b745193e2b 100644 --- a/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimator.kt @@ -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 PhysicsAnimator?.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 + } + } } diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/BubbleStashController.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/BubbleStashController.kt index 0f437449bb..48eb7dea15 100644 --- a/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/BubbleStashController.kt +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/BubbleStashController.kt @@ -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 diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/PersistentBubbleStashController.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/PersistentBubbleStashController.kt index 62fe221c70..1b650195ef 100644 --- a/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/PersistentBubbleStashController.kt +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/PersistentBubbleStashController.kt @@ -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. diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/TransientBubbleStashController.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/TransientBubbleStashController.kt index 23e009b3a1..1a4b9823ef 100644 --- a/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/TransientBubbleStashController.kt +++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/stashing/TransientBubbleStashController.kt @@ -242,6 +242,8 @@ class TransientBubbleStashController( bubbleStashedHandleViewController?.setTranslationYForSwipe(translationY) } + override fun getHandleTranslationY(): Float? = bubbleStashedHandleViewController?.translationY + private fun getStashTranslation(): Float { return (bubbleBarViewController.bubbleBarCollapsedHeight - stashedHeight) / 2f } diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimatorTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimatorTest.kt index 619ce1c860..21eb3e0f7d 100644 --- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimatorTest.kt +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/bubbles/animation/BubbleBarViewAnimatorTest.kt @@ -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 PhysicsAnimator.assertIsRunning() { InstrumentationRegistry.getInstrumentation().runOnMainSync { assertThat(isRunning()).isTrue() From 1ceac5ddfe8cfb6e427b4d8c0a1fdb32557e61a0 Mon Sep 17 00:00:00 2001 From: Alex Chau Date: Tue, 30 Jul 2024 16:04:32 +0100 Subject: [PATCH 5/6] Implement e2e test for desktop windowing Code changes: - TaskView, GroupedTaskView and DesktopTaskView now uses different resId, so it can be differentiated by TAPL - Extracted result handling of icon loading, so we can override in DesktopTaskView - DesktopTaskView now load icons, so titleDescription can be loaded. When icons are loaded, contentDescription are applied to respective snapshotViews; icon unloading is ignored - Track launchDesktopFromRecents and composeRecentsLaunchAnimator in TAPL events so we can test if the TaskView launch path is correct Test changes: - Added TaplTestsOverviewDesktop that move 2 TestActivities into Desktop, and launch the DesktopTaskView as static and live tile - TaplTestsOverviewDesktop is limited to Tangor/cf_tablet only, and added LimitDeviceRule to AbstractLauncherUiTest to enable @AllowedDevices and @IgnoreLimit TAPL changes (2 APIs added/modified): - Changed TaskView matcher to use id/task_view_* to match all TaskView types - When Overview is launcehd from Background, mark the currentTask after the launch as liveTile. When an OverviewTask has the same accessibility node as the liveTile, it'll expect different event when launching. - [API change] BaseOverview.getTestActivityTask can now matches mutiple test activiites, useful for matching GroupedTaskView and DesktopTaskView; Fix a bug that getTestActivityTask wrongly use `getParent()` which is RecentsView to match activityName. - In OverviewTask.open, we'll expect different events based on TaskView types and whether it's a live tile. Launching DesktopTaskView will in additional verify Desktop header is present on screen. - [API change] In OverviewTaskMenu, support tapping Desktop menu and verify Desktop header is present on screen. - Removed unused OverviewTaskMenuItem Fix: 320313527 Test: TaplTestsOverviewDesktop Flag: com.android.window.flags.enable_desktop_windowing_mode Change-Id: I89261c787364901320f3acb18f01ddad5f62d17c --- quickstep/res/layout/task.xml | 2 +- quickstep/res/layout/task_desktop.xml | 2 +- quickstep/res/layout/task_grouped.xml | 2 +- .../quickstep/views/DesktopTaskView.kt | 17 ++- .../com/android/quickstep/views/TaskView.kt | 31 ++++-- .../quickstep/TaplTestsOverviewDesktop.kt | 105 ++++++++++++++++++ .../launcher3/ui/AbstractLauncherUiTest.java | 4 + .../android/launcher3/tapl/Background.java | 6 +- .../android/launcher3/tapl/BaseOverview.java | 86 ++++++++++---- .../launcher3/tapl/LaunchedAppState.java | 15 +++ .../tapl/LauncherInstrumentation.java | 2 +- .../com/android/launcher3/tapl/Overview.java | 7 +- .../android/launcher3/tapl/OverviewTask.java | 43 ++++++- .../launcher3/tapl/OverviewTaskMenu.java | 31 ++++-- .../launcher3/tapl/OverviewTaskMenuItem.java | 39 ------- 15 files changed, 302 insertions(+), 90 deletions(-) create mode 100644 quickstep/tests/src/com/android/quickstep/TaplTestsOverviewDesktop.kt delete mode 100644 tests/tapl/com/android/launcher3/tapl/OverviewTaskMenuItem.java diff --git a/quickstep/res/layout/task.xml b/quickstep/res/layout/task.xml index ac1a50a5d8..34193d34c6 100644 --- a/quickstep/res/layout/task.xml +++ b/quickstep/res/layout/task.xml @@ -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" diff --git a/quickstep/res/layout/task_desktop.xml b/quickstep/res/layout/task_desktop.xml index 64aa7e14ed..8c7090e41a 100644 --- a/quickstep/res/layout/task_desktop.xml +++ b/quickstep/res/layout/task_desktop.xml @@ -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" diff --git a/quickstep/res/layout/task_grouped.xml b/quickstep/res/layout/task_grouped.xml index da2b29f9c6..cb4b98fd4c 100644 --- a/quickstep/res/layout/task_grouped.xml +++ b/quickstep/res/layout/task_grouped.xml @@ -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" diff --git a/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt b/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt index 9ce2277b14..384945b34c 100644 --- a/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt +++ b/quickstep/src/com/android/quickstep/views/DesktopTaskView.kt @@ -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" } diff --git a/quickstep/src/com/android/quickstep/views/TaskView.kt b/quickstep/src/com/android/quickstep/views/TaskView.kt index e189d14527..99574a6645 100644 --- a/quickstep/src/com/android/quickstep/views/TaskView.kt +++ b/quickstep/src/com/android/quickstep/views/TaskView.kt @@ -868,18 +868,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) } } } @@ -898,6 +891,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) { @@ -1119,6 +1127,11 @@ constructor( isClickableAsLiveTile = true return runnableList } + TestLogging.recordEvent( + TestProtocol.SEQUENCE_MAIN, + "composeRecentsLaunchAnimator", + taskIds.contentToString() + ) val runnableList = RunnableList() with(AnimatorSet()) { TaskViewUtils.composeRecentsLaunchAnimator( diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsOverviewDesktop.kt b/quickstep/tests/src/com/android/quickstep/TaplTestsOverviewDesktop.kt new file mode 100644 index 0000000000..694a3822fe --- /dev/null +++ b/quickstep/tests/src/com/android/quickstep/TaplTestsOverviewDesktop.kt @@ -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() { + @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) + } +} diff --git a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java index 1e2744c3aa..749a75aaa1 100644 --- a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java +++ b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java @@ -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 { @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(); diff --git a/tests/tapl/com/android/launcher3/tapl/Background.java b/tests/tapl/com/android/launcher3/tapl/Background.java index 988aa94c28..b7ebfcd0d6 100644 --- a/tests/tapl/com/android/launcher3/tapl/Background.java +++ b/tests/tapl/com/android/launcher3/tapl/Background.java @@ -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 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", diff --git a/tests/tapl/com/android/launcher3/tapl/BaseOverview.java b/tests/tapl/com/android/launcher3/tapl/BaseOverview.java index 567a8bd1c0..e71b49fcad 100644 --- a/tests/tapl/com/android/launcher3/tapl/BaseOverview.java +++ b/tests/tapl/com/android/launcher3/tapl/BaseOverview.java @@ -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 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 activityNumbers) { final List 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 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(); + } } diff --git a/tests/tapl/com/android/launcher3/tapl/LaunchedAppState.java b/tests/tapl/com/android/launcher3/tapl/LaunchedAppState.java index 200f2fff17..b3ad930659 100644 --- a/tests/tapl/com/android/launcher3/tapl/LaunchedAppState.java +++ b/tests/tapl/com/android/launcher3/tapl/LaunchedAppState.java @@ -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. * diff --git a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java index 75c1b24e23..a87406286b 100644 --- a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java +++ b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java @@ -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); diff --git a/tests/tapl/com/android/launcher3/tapl/Overview.java b/tests/tapl/com/android/launcher3/tapl/Overview.java index 50c21368e0..deb27e4f87 100644 --- a/tests/tapl/com/android/launcher3/tapl/Overview.java +++ b/tests/tapl/com/android/launcher3/tapl/Overview.java @@ -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 diff --git a/tests/tapl/com/android/launcher3/tapl/OverviewTask.java b/tests/tapl/com/android/launcher3/tapl/OverviewTask.java index 6f420afe78..7a8ab492b5 100644 --- a/tests/tapl/com/android/launcher3/tapl/OverviewTask.java +++ b/tests/tapl/com/android/launcher3/tapl/OverviewTask.java @@ -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 + } } diff --git a/tests/tapl/com/android/launcher3/tapl/OverviewTaskMenu.java b/tests/tapl/com/android/launcher3/tapl/OverviewTaskMenu.java index 902ad5b568..90d32f3249 100644 --- a/tests/tapl/com/android/launcher3/tapl/OverviewTaskMenu.java +++ b/tests/tapl/com/android/launcher3/tapl/OverviewTaskMenu.java @@ -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. */ diff --git a/tests/tapl/com/android/launcher3/tapl/OverviewTaskMenuItem.java b/tests/tapl/com/android/launcher3/tapl/OverviewTaskMenuItem.java deleted file mode 100644 index e3035bf71b..0000000000 --- a/tests/tapl/com/android/launcher3/tapl/OverviewTaskMenuItem.java +++ /dev/null @@ -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(); - } -} From 1926d46784408204741c40013c1b019ffa43bcdf Mon Sep 17 00:00:00 2001 From: Jagrut Desai Date: Thu, 8 Aug 2024 13:30:48 -0700 Subject: [PATCH 6/6] Only run Taskbar related tests on foldables and tablets Test: presubmit Bug: 356409843 Flag: TEST_ONLY Change-Id: I18d8f6e6c2c4ec7ec876c84a27466c883b1c0f71 --- .../com/android/launcher3/taskbar/rules/TaskbarModeRuleTest.kt | 2 ++ .../launcher3/taskbar/rules/TaskbarPinningPreferenceRuleTest.kt | 2 ++ .../launcher3/taskbar/rules/TaskbarPreferenceRuleTest.kt | 2 ++ .../android/launcher3/taskbar/rules/TaskbarUnitTestRuleTest.kt | 2 +- .../launcher3/taskbar/rules/TaskbarWindowSandboxContextTest.kt | 1 + 5 files changed, 8 insertions(+), 1 deletion(-) diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarModeRuleTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarModeRuleTest.kt index f75e5423be..f7e457613a 100644 --- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarModeRuleTest.kt +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarModeRuleTest.kt @@ -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) diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarPinningPreferenceRuleTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarPinningPreferenceRuleTest.kt index a709133b34..a5154058fd 100644 --- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarPinningPreferenceRuleTest.kt +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarPinningPreferenceRuleTest.kt @@ -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) diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarPreferenceRuleTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarPreferenceRuleTest.kt index 22d2079821..46817d2bbd 100644 --- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarPreferenceRuleTest.kt +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarPreferenceRuleTest.kt @@ -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) diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarUnitTestRuleTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarUnitTestRuleTest.kt index 234e4991c6..5d4fdc5418 100644 --- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarUnitTestRuleTest.kt +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarUnitTestRuleTest.kt @@ -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) diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarWindowSandboxContextTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarWindowSandboxContextTest.kt index ad4b4de2ea..4834d48973 100644 --- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarWindowSandboxContextTest.kt +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/rules/TaskbarWindowSandboxContextTest.kt @@ -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)