diff --git a/quickstep/res/values/override.xml b/quickstep/res/values/override.xml index 67be0dd113..860abc1cc3 100644 --- a/quickstep/res/values/override.xml +++ b/quickstep/res/values/override.xml @@ -27,6 +27,8 @@ + + com.android.launcher3.secondarydisplay.SecondaryDisplayPredictionsImpl com.android.launcher3.taskbar.TaskbarModelCallbacksFactory diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarNavButtonController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarNavButtonController.java index 0f8de34461..993f13eaa4 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarNavButtonController.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarNavButtonController.java @@ -51,6 +51,7 @@ import com.android.quickstep.OverviewCommandHelper; import com.android.quickstep.SystemUiProxy; import com.android.quickstep.TaskUtils; import com.android.quickstep.TouchInteractionService; +import com.android.quickstep.util.AssistUtilsBase; import com.android.quickstep.views.DesktopTaskView; import java.io.PrintWriter; @@ -158,7 +159,7 @@ public class TaskbarNavButtonController implements TaskbarControllers.LoggableTa switch (buttonType) { case BUTTON_HOME: logEvent(LAUNCHER_TASKBAR_HOME_BUTTON_LONGPRESS); - startAssistant(); + onLongPressHome(); return true; case BUTTON_A11Y: logEvent(LAUNCHER_TASKBAR_A11Y_BUTTON_LONGPRESS); @@ -307,13 +308,17 @@ public class TaskbarNavButtonController implements TaskbarControllers.LoggableTa } } - private void startAssistant() { + private void onLongPressHome() { if (mScreenPinned || !mAssistantLongPressEnabled) { return; } - Bundle args = new Bundle(); - args.putInt(INVOCATION_TYPE_KEY, INVOCATION_TYPE_HOME_BUTTON_LONG_PRESS); - mSystemUiProxy.startAssistant(args); + // Attempt to start Assist with AssistUtils, otherwise fall back to SysUi's implementation. + if (!AssistUtilsBase.newInstance(mService.getApplicationContext()).tryStartAssistOverride( + INVOCATION_TYPE_HOME_BUTTON_LONG_PRESS)) { + Bundle args = new Bundle(); + args.putInt(INVOCATION_TYPE_KEY, INVOCATION_TYPE_HOME_BUTTON_LONG_PRESS); + mSystemUiProxy.startAssistant(args); + } } private void showQuickSettings() { diff --git a/quickstep/src/com/android/quickstep/SystemUiProxy.java b/quickstep/src/com/android/quickstep/SystemUiProxy.java index e73b52585f..823e1378f3 100644 --- a/quickstep/src/com/android/quickstep/SystemUiProxy.java +++ b/quickstep/src/com/android/quickstep/SystemUiProxy.java @@ -62,6 +62,7 @@ import com.android.internal.view.AppearanceRegion; import com.android.launcher3.util.MainThreadInitializedObject; import com.android.launcher3.util.Preconditions; import com.android.launcher3.util.SplitConfigurationOptions; +import com.android.quickstep.util.AssistUtilsBase; import com.android.systemui.shared.recents.ISystemUiProxy; import com.android.systemui.shared.recents.model.ThumbnailData; import com.android.systemui.shared.system.RecentsAnimationControllerCompat; @@ -250,6 +251,8 @@ public class SystemUiProxy implements ISystemUiProxy { setBackToLauncherCallback(mBackToLauncherCallback, mBackToLauncherRunner); setUnfoldAnimationListener(mUnfoldAnimationListener); setDesktopTaskListener(mDesktopTaskListener); + setAssistantOverridesRequested( + AssistUtilsBase.newInstance(mContext).getSysUiAssistOverrideInvocationTypes()); } /** @@ -373,6 +376,17 @@ public class SystemUiProxy implements ISystemUiProxy { } } + @Override + public void setAssistantOverridesRequested(int[] invocationTypes) { + if (mSystemUiProxy != null) { + try { + mSystemUiProxy.setAssistantOverridesRequested(invocationTypes); + } catch (RemoteException e) { + Log.w(TAG, "Failed call setAssistantOverridesRequested", e); + } + } + } + @Override public void notifyAccessibilityButtonClicked(int displayId) { if (mSystemUiProxy != null) { diff --git a/quickstep/src/com/android/quickstep/TouchInteractionService.java b/quickstep/src/com/android/quickstep/TouchInteractionService.java index 22aca2571d..cd88894936 100644 --- a/quickstep/src/com/android/quickstep/TouchInteractionService.java +++ b/quickstep/src/com/android/quickstep/TouchInteractionService.java @@ -117,6 +117,7 @@ import com.android.quickstep.inputconsumers.TaskbarUnstashInputConsumer; import com.android.quickstep.inputconsumers.TrackpadStatusBarInputConsumer; import com.android.quickstep.util.ActiveGestureLog; import com.android.quickstep.util.ActiveGestureLog.CompoundString; +import com.android.quickstep.util.AssistUtilsBase; import com.android.systemui.shared.recents.IOverviewProxy; import com.android.systemui.shared.recents.ISystemUiProxy; import com.android.systemui.shared.system.ActivityManagerWrapper; @@ -280,6 +281,20 @@ public class TouchInteractionService extends Service { })); } + /** + * Sent when the assistant has been invoked with the given type (defined in AssistManager) + * and should be shown. This method is used if SystemUiProxy#setAssistantOverridesRequested + * was previously called including this invocation type. + */ + @Override + public void onAssistantOverrideInvoked(int invocationType) { + executeForTouchInteractionService(tis -> { + if (!AssistUtilsBase.newInstance(tis).tryStartAssistOverride(invocationType)) { + Log.w(TAG, "Failed to invoke Assist override"); + } + }); + } + @Override public void onNavigationBarSurface(SurfaceControl surface) { // TODO: implement diff --git a/quickstep/src/com/android/quickstep/util/AssistUtilsBase.java b/quickstep/src/com/android/quickstep/util/AssistUtilsBase.java new file mode 100644 index 0000000000..7b270204bf --- /dev/null +++ b/quickstep/src/com/android/quickstep/util/AssistUtilsBase.java @@ -0,0 +1,45 @@ +/* + * 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.quickstep.util; + +import android.content.Context; + +import com.android.launcher3.R; +import com.android.launcher3.util.ResourceBasedOverride; + +/** Utilities to work with Assistant functionality. */ +public class AssistUtilsBase implements ResourceBasedOverride { + + public AssistUtilsBase() {} + + /** Creates AssistUtils as specified by overrides */ + public static AssistUtilsBase newInstance(Context context) { + return Overrides.getObject(AssistUtilsBase.class, context, R.string.assist_utils_class); + } + + /** @return Array of AssistUtils.INVOCATION_TYPE_* that we want to handle instead of SysUI. */ + public int[] getSysUiAssistOverrideInvocationTypes() { + return new int[0]; + } + + /** + * @return {@code true} if the override was handled, i.e. an assist surface was shown or the + * request should be ignored. {@code false} means the caller should start assist another way. + */ + public boolean tryStartAssistOverride(int invocationType) { + return false; + } +} diff --git a/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarNavButtonControllerTest.java b/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarNavButtonControllerTest.java index 962261940c..b3d04c6ff8 100644 --- a/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarNavButtonControllerTest.java +++ b/quickstep/tests/src/com/android/launcher3/taskbar/TaskbarNavButtonControllerTest.java @@ -26,6 +26,7 @@ import static org.mockito.Mockito.when; import android.os.Handler; import android.view.View; +import androidx.test.platform.app.InstrumentationRegistry; import androidx.test.runner.AndroidJUnit4; import com.android.launcher3.logging.StatsLogManager; @@ -70,6 +71,9 @@ public class TaskbarNavButtonControllerTest { MockitoAnnotations.initMocks(this); when(mockService.getDisplayId()).thenReturn(DISPLAY_ID); when(mockService.getOverviewCommandHelper()).thenReturn(mockCommandHelper); + when(mockService.getApplicationContext()) + .thenReturn(InstrumentationRegistry.getInstrumentation().getTargetContext() + .getApplicationContext()); when(mockStatsLogManager.logger()).thenReturn(mockStatsLogger); when(mockTaskbarControllers.getTaskbarActivityContext()) .thenReturn(mockTaskbarActivityContext); diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsSplitscreen.java b/quickstep/tests/src/com/android/quickstep/TaplTestsSplitscreen.java index 1aa7ab6059..92b598b626 100644 --- a/quickstep/tests/src/com/android/quickstep/TaplTestsSplitscreen.java +++ b/quickstep/tests/src/com/android/quickstep/TaplTestsSplitscreen.java @@ -25,6 +25,8 @@ import static org.junit.Assume.assumeTrue; import android.content.Intent; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.LargeTest; import androidx.test.platform.app.InstrumentationRegistry; import com.android.launcher3.config.FeatureFlags; @@ -36,7 +38,10 @@ import com.android.quickstep.TaskbarModeSwitchRule.TaskbarModeSwitch; import org.junit.After; import org.junit.Before; import org.junit.Test; +import org.junit.runner.RunWith; +@LargeTest +@RunWith(AndroidJUnit4.class) public class TaplTestsSplitscreen extends AbstractQuickStepTest { private static final String CALCULATOR_APP_NAME = "Calculator"; private static final String CALCULATOR_APP_PACKAGE = diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java index 360e06059f..abf84ddcb5 100644 --- a/src/com/android/launcher3/BubbleTextView.java +++ b/src/com/android/launcher3/BubbleTextView.java @@ -16,6 +16,7 @@ package com.android.launcher3; +import static com.android.launcher3.config.FeatureFlags.ENABLE_CURSOR_HOVER_STATES; import static com.android.launcher3.config.FeatureFlags.ENABLE_DOWNLOAD_APP_UX_V2; import static com.android.launcher3.config.FeatureFlags.ENABLE_ICON_LABEL_AUTO_SCALING; import static com.android.launcher3.graphics.PreloadIconDrawable.newPendingIcon; @@ -152,7 +153,7 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver, private final CheckLongPressHelper mLongPressHelper; - private final boolean mLayoutHorizontal; + private boolean mLayoutHorizontal; private final boolean mIsRtl; private final int mIconSize; @@ -197,6 +198,7 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver, public BubbleTextView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); mActivity = ActivityContext.lookupContext(context); + FastBitmapDrawable.setFlagHoverEnabled(ENABLE_CURSOR_HOVER_STATES.get()); TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.BubbleTextView, defStyle, 0); @@ -665,6 +667,18 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver, } } + /** + * Sets whether the layout is horizontal. + */ + public void setLayoutHorizontal(boolean layoutHorizontal) { + if (mLayoutHorizontal == layoutHorizontal) { + return; + } + + mLayoutHorizontal = layoutHorizontal; + applyCompoundDrawables(getIconOrTransparentColor()); + } + /** * Sets whether to vertically center the content. */ @@ -991,10 +1005,14 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver, if (!mIsIconVisible) { resetIconScale(); } - Drawable icon = visible ? mIcon : new ColorDrawable(Color.TRANSPARENT); + Drawable icon = getIconOrTransparentColor(); applyCompoundDrawables(icon); } + private Drawable getIconOrTransparentColor() { + return mIsIconVisible ? mIcon : new ColorDrawable(Color.TRANSPARENT); + } + /** Sets the icon visual state to disabled or not. */ public void setIconDisabled(boolean isDisabled) { if (mIcon != null) { diff --git a/src/com/android/launcher3/CellLayout.java b/src/com/android/launcher3/CellLayout.java index 4674401b2b..08e5def70d 100644 --- a/src/com/android/launcher3/CellLayout.java +++ b/src/com/android/launcher3/CellLayout.java @@ -245,6 +245,7 @@ public class CellLayout extends ViewGroup { // the user where a dragged item will land when dropped. setWillNotDraw(false); setClipToPadding(false); + setClipChildren(false); mActivity = ActivityContext.lookupContext(context); DeviceProfile deviceProfile = mActivity.getDeviceProfile(); diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java index 94ab4425db..ffb8b8244b 100644 --- a/src/com/android/launcher3/Launcher.java +++ b/src/com/android/launcher3/Launcher.java @@ -198,6 +198,7 @@ import com.android.launcher3.touch.ItemLongClickListener; import com.android.launcher3.uioverrides.plugins.PluginManagerWrapper; import com.android.launcher3.util.ActivityResultInfo; import com.android.launcher3.util.ActivityTracker; +import com.android.launcher3.util.CannedAnimationCoordinator; import com.android.launcher3.util.ComponentKey; import com.android.launcher3.util.IntArray; import com.android.launcher3.util.IntSet; @@ -421,6 +422,9 @@ public class Launcher extends StatefulActivity private StartupLatencyLogger mStartupLatencyLogger; private CellPosMapper mCellPosMapper = CellPosMapper.DEFAULT; + private final CannedAnimationCoordinator mAnimationCoordinator = + new CannedAnimationCoordinator(this); + @Override @TargetApi(Build.VERSION_CODES.S) protected void onCreate(Bundle savedInstanceState) { @@ -3422,4 +3426,11 @@ public class Launcher extends StatefulActivity public void launchAppPair(WorkspaceItemInfo app1, WorkspaceItemInfo app2) { // Overridden } + + /** + * Returns the animation coordinator for playing one-off animations + */ + public CannedAnimationCoordinator getAnimationCoordinator() { + return mAnimationCoordinator; + } } diff --git a/src/com/android/launcher3/ShortcutAndWidgetContainer.java b/src/com/android/launcher3/ShortcutAndWidgetContainer.java index 07b71b32c1..f0fea61b33 100644 --- a/src/com/android/launcher3/ShortcutAndWidgetContainer.java +++ b/src/com/android/launcher3/ShortcutAndWidgetContainer.java @@ -66,6 +66,7 @@ public class ShortcutAndWidgetContainer extends ViewGroup implements FolderIcon. mActivity = ActivityContext.lookupContext(context); mWallpaperManager = WallpaperManager.getInstance(context); mContainerType = containerType; + setClipChildren(false); } public void setCellDimensions(int cellWidth, int cellHeight, int countX, int countY, diff --git a/src/com/android/launcher3/util/CannedAnimationCoordinator.kt b/src/com/android/launcher3/util/CannedAnimationCoordinator.kt new file mode 100644 index 0000000000..18f833978a --- /dev/null +++ b/src/com/android/launcher3/util/CannedAnimationCoordinator.kt @@ -0,0 +1,164 @@ +/* + * 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.util + +import android.animation.AnimatorSet +import android.animation.ValueAnimator +import android.util.Log +import android.view.ViewTreeObserver.OnGlobalLayoutListener +import androidx.core.view.OneShotPreDrawListener +import com.android.app.animation.Interpolators.LINEAR +import com.android.launcher3.anim.AnimatorListeners +import com.android.launcher3.anim.AnimatorPlaybackController +import com.android.launcher3.anim.PendingAnimation +import com.android.launcher3.statemanager.StatefulActivity +import java.util.function.Consumer + +private const val TAG = "CannedAnimCoordinator" + +/** + * Utility class to run a canned animation on Launcher. + * + * This class takes care to registering animations with stateManager and ensures that only one + * animation is playing at a time. + */ +class CannedAnimationCoordinator(private val activity: StatefulActivity<*>) { + + private val launcherLayoutListener = OnGlobalLayoutListener { scheduleRecreateAnimOnPreDraw() } + private var recreatePending = false + + private var animationProvider: Any? = null + + private var animationDuration: Long = 0L + private var animationFactory: Consumer? = null + private var animationController: AnimatorPlaybackController? = null + + private var currentAnim: AnimatorPlaybackController? = null + + /** + * Sets the current animation cancelling any previously set animation. + * + * Callers can control the animation using {@link #getPlaybackController}. The state is + * automatically cleared when the playback controller ends. The animation is automatically + * recreated when any layout change happens. Callers can also ask for recreation by calling + * {@link #recreateAnimation} + */ + fun setAnimation(provider: Any, factory: Consumer, duration: Long) { + if (provider != animationProvider) { + Log.e(TAG, "Trying to play two animations together, $provider and $animationProvider") + } + + // Cancel any previously running animation + endCurrentAnimation(false) + animationController?.dispatchOnCancel()?.dispatchOnEnd() + + animationProvider = provider + animationFactory = factory + animationDuration = duration + + // Setup a new controller and link it with launcher state animation + val anim = AnimatorSet() + anim.play( + ValueAnimator.ofFloat(0f, 1f).apply { + interpolator = LINEAR + this.duration = duration + addUpdateListener { anim -> currentAnim?.setPlayFraction(anim.animatedFraction) } + } + ) + val controller = AnimatorPlaybackController.wrap(anim, duration) + anim.addListener( + AnimatorListeners.forEndCallback { success -> + if (animationController != controller) { + return@forEndCallback + } + + endCurrentAnimation(success) + animationController = null + animationFactory = null + animationProvider = null + + activity.rootView.viewTreeObserver.apply { + if (isAlive) { + removeOnGlobalLayoutListener(launcherLayoutListener) + } + } + } + ) + + // Recreate animation whenever layout happens in case transforms change during layout + activity.rootView.viewTreeObserver.apply { + if (isAlive) { + addOnGlobalLayoutListener(launcherLayoutListener) + } + } + // Link this to the state manager so that it auto-cancels when state changes + recreatePending = false + animationController = + controller.apply { activity.stateManager.setCurrentUserControlledAnimation(this) } + recreateAnimation(provider) + } + + private fun endCurrentAnimation(success: Boolean) { + currentAnim?.apply { + // When cancelling an animation, apply final progress so that all transformations + // are restored + setPlayFraction(1f) + if (!success) dispatchOnCancel() + dispatchOnEnd() + } + currentAnim = null + } + + /** Returns the current animation controller to control the animation */ + fun getPlaybackController(provider: Any): AnimatorPlaybackController? { + return if (provider == animationProvider) animationController + else { + Log.d(TAG, "Wrong controller access from $provider, actual provider $animationProvider") + null + } + } + + private fun scheduleRecreateAnimOnPreDraw() { + if (!recreatePending) { + recreatePending = true + OneShotPreDrawListener.add(activity.rootView) { + if (recreatePending) { + recreatePending = false + animationProvider?.apply { recreateAnimation(this) } + } + } + } + } + + /** Notify the controller to recreate the animation. The animation progress is preserved */ + fun recreateAnimation(provider: Any) { + if (provider != animationProvider) { + Log.e(TAG, "Ignore recreate request from $provider, actual provider $animationProvider") + return + } + endCurrentAnimation(false /* success */) + + if (animationFactory == null || animationController == null) { + return + } + currentAnim = + PendingAnimation(animationDuration) + .apply { animationFactory?.accept(this) } + .createPlaybackController() + .apply { setPlayFraction(animationController!!.progressFraction) } + } +} diff --git a/src/com/android/launcher3/util/MultiScalePropertyFactory.java b/src/com/android/launcher3/util/MultiScalePropertyFactory.java index a7e6cc8679..cf8d6ccf67 100644 --- a/src/com/android/launcher3/util/MultiScalePropertyFactory.java +++ b/src/com/android/launcher3/util/MultiScalePropertyFactory.java @@ -40,8 +40,7 @@ public class MultiScalePropertyFactory { private static final boolean DEBUG = false; private static final String TAG = "MultiScaleProperty"; private final String mName; - private final ArrayMap mProperties = - new ArrayMap(); + private final ArrayMap mProperties = new ArrayMap<>(); // This is an optimization for cases when set is called repeatedly with the same setterIndex. private float mMinOfOthers = 0; @@ -55,7 +54,7 @@ public class MultiScalePropertyFactory { } /** Returns the [MultiFloatProperty] associated with [inx], creating it if not present. */ - public MultiScaleProperty get(Integer index) { + public FloatProperty get(Integer index) { return mProperties.computeIfAbsent(index, (k) -> new MultiScaleProperty(index, mName + "_" + index)); } diff --git a/tests/src/com/android/launcher3/icons/FastBitmapDrawableTest.java b/tests/src/com/android/launcher3/icons/FastBitmapDrawableTest.java new file mode 100644 index 0000000000..038c98b271 --- /dev/null +++ b/tests/src/com/android/launcher3/icons/FastBitmapDrawableTest.java @@ -0,0 +1,329 @@ +/* + * 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.icons; + +import static com.android.launcher3.icons.FastBitmapDrawable.CLICK_FEEDBACK_DURATION; +import static com.android.launcher3.icons.FastBitmapDrawable.HOVERED_SCALE; +import static com.android.launcher3.icons.FastBitmapDrawable.HOVER_FEEDBACK_DURATION; +import static com.android.launcher3.icons.FastBitmapDrawable.PRESSED_SCALE; +import static com.android.launcher3.icons.FastBitmapDrawable.SCALE; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.graphics.Bitmap; +import android.view.animation.AccelerateInterpolator; +import android.view.animation.DecelerateInterpolator; +import android.view.animation.PathInterpolator; + +import androidx.test.annotation.UiThreadTest; +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Spy; + +/** + * Tests for FastBitmapDrawable. + */ +@SmallTest +@UiThreadTest +@RunWith(AndroidJUnit4.class) +public class FastBitmapDrawableTest { + private static final float EPSILON = 0.00001f; + + @Spy + FastBitmapDrawable mFastBitmapDrawable = + spy(new FastBitmapDrawable(Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888))); + + @Before + public void setUp() { + FastBitmapDrawable.setFlagHoverEnabled(true); + when(mFastBitmapDrawable.isVisible()).thenReturn(true); + mFastBitmapDrawable.mIsPressed = false; + mFastBitmapDrawable.mIsHovered = false; + mFastBitmapDrawable.resetScale(); + } + + @Test + public void testOnStateChange_noState() { + int[] state = new int[]{}; + + boolean isHandled = mFastBitmapDrawable.onStateChange(state); + + // No scale changes without state change. + assertFalse("State change handled.", isHandled); + assertNull("Scale animation not null.", mFastBitmapDrawable.mScaleAnimation); + } + + @Test + public void testOnStateChange_statePressed() { + int[] state = new int[]{android.R.attr.state_pressed}; + + boolean isHandled = mFastBitmapDrawable.onStateChange(state); + + // Animate to state pressed. + assertTrue("State change not handled.", isHandled); + assertEquals("Duration not correct.", mFastBitmapDrawable.mScaleAnimation.getDuration(), + CLICK_FEEDBACK_DURATION); + mFastBitmapDrawable.mScaleAnimation.end(); + assertEquals("End value not correct.", + (float) SCALE.get(mFastBitmapDrawable), PRESSED_SCALE, EPSILON); + assertTrue("Wrong interpolator used.", + mFastBitmapDrawable.mScaleAnimation.getInterpolator() + instanceof AccelerateInterpolator); + } + + @Test + public void testOnStateChange_stateHovered() { + int[] state = new int[]{android.R.attr.state_hovered}; + + boolean isHandled = mFastBitmapDrawable.onStateChange(state); + + // Animate to state hovered. + assertTrue("State change not handled.", isHandled); + assertEquals("Duration not correct.", mFastBitmapDrawable.mScaleAnimation.getDuration(), + HOVER_FEEDBACK_DURATION); + mFastBitmapDrawable.mScaleAnimation.end(); + assertEquals("End value not correct.", + (float) SCALE.get(mFastBitmapDrawable), HOVERED_SCALE, EPSILON); + assertTrue("Wrong interpolator used.", + mFastBitmapDrawable.mScaleAnimation.getInterpolator() instanceof PathInterpolator); + } + + @Test + public void testOnStateChange_stateHoveredFlagDisabled() { + FastBitmapDrawable.setFlagHoverEnabled(false); + int[] state = new int[]{android.R.attr.state_hovered}; + + boolean isHandled = mFastBitmapDrawable.onStateChange(state); + + // No state change with flag disabled. + assertFalse("Hover state change handled with flag disabled.", isHandled); + assertNull("Animation should not run with hover flag disabled.", + mFastBitmapDrawable.mScaleAnimation); + } + + @Test + public void testOnStateChange_statePressedAndHovered() { + int[] state = new int[]{android.R.attr.state_pressed, android.R.attr.state_hovered}; + + boolean isHandled = mFastBitmapDrawable.onStateChange(state); + + // Animate to pressed state only. + assertTrue("State change not handled.", isHandled); + assertEquals("Duration not correct.", mFastBitmapDrawable.mScaleAnimation.getDuration(), + CLICK_FEEDBACK_DURATION); + mFastBitmapDrawable.mScaleAnimation.end(); + assertEquals("End value not correct.", + (float) SCALE.get(mFastBitmapDrawable), PRESSED_SCALE, EPSILON); + assertTrue("Wrong interpolator used.", + mFastBitmapDrawable.mScaleAnimation.getInterpolator() + instanceof AccelerateInterpolator); + } + + @Test + public void testOnStateChange_stateHoveredAndPressed() { + int[] state = new int[]{android.R.attr.state_hovered, android.R.attr.state_pressed}; + + boolean isHandled = mFastBitmapDrawable.onStateChange(state); + + // Animate to pressed state only. + assertTrue("State change not handled.", isHandled); + assertEquals("Duration not correct.", mFastBitmapDrawable.mScaleAnimation.getDuration(), + CLICK_FEEDBACK_DURATION); + mFastBitmapDrawable.mScaleAnimation.end(); + assertEquals("End value not correct.", + (float) SCALE.get(mFastBitmapDrawable), PRESSED_SCALE, EPSILON); + assertTrue("Wrong interpolator used.", + mFastBitmapDrawable.mScaleAnimation.getInterpolator() + instanceof AccelerateInterpolator); + } + + @Test + public void testOnStateChange_stateHoveredAndPressedToPressed() { + mFastBitmapDrawable.mIsPressed = true; + mFastBitmapDrawable.mIsHovered = true; + SCALE.setValue(mFastBitmapDrawable, PRESSED_SCALE); + int[] state = new int[]{android.R.attr.state_pressed}; + + boolean isHandled = mFastBitmapDrawable.onStateChange(state); + + // No scale change from pressed state to pressed state. + assertTrue("State not changed.", isHandled); + assertEquals("End value not correct.", + (float) SCALE.get(mFastBitmapDrawable), PRESSED_SCALE, EPSILON); + } + + @Test + public void testOnStateChange_stateHoveredAndPressedToHovered() { + mFastBitmapDrawable.mIsPressed = true; + mFastBitmapDrawable.mIsHovered = true; + SCALE.setValue(mFastBitmapDrawable, PRESSED_SCALE); + int[] state = new int[]{android.R.attr.state_hovered}; + + boolean isHandled = mFastBitmapDrawable.onStateChange(state); + + // No scale change from pressed state to hovered state. + assertTrue("State not changed.", isHandled); + assertEquals("End value not correct.", + (float) SCALE.get(mFastBitmapDrawable), HOVERED_SCALE, EPSILON); + } + + @Test + public void testOnStateChange_stateHoveredToPressed() { + mFastBitmapDrawable.mIsHovered = true; + SCALE.setValue(mFastBitmapDrawable, HOVERED_SCALE); + int[] state = new int[]{android.R.attr.state_pressed}; + + boolean isHandled = mFastBitmapDrawable.onStateChange(state); + + // No scale change from pressed state to hovered state. + assertTrue("State not changed.", isHandled); + assertEquals("End value not correct.", + (float) SCALE.get(mFastBitmapDrawable), PRESSED_SCALE, EPSILON); + } + + @Test + public void testOnStateChange_statePressedToHovered() { + mFastBitmapDrawable.mIsPressed = true; + SCALE.setValue(mFastBitmapDrawable, PRESSED_SCALE); + int[] state = new int[]{android.R.attr.state_hovered}; + + boolean isHandled = mFastBitmapDrawable.onStateChange(state); + + // No scale change from pressed state to hovered state. + assertTrue("State not changed.", isHandled); + assertEquals("End value not correct.", + (float) SCALE.get(mFastBitmapDrawable), HOVERED_SCALE, EPSILON); + } + + @Test + public void testOnStateChange_stateDefaultFromPressed() { + mFastBitmapDrawable.mIsPressed = true; + SCALE.setValue(mFastBitmapDrawable, PRESSED_SCALE); + int[] state = new int[]{}; + + boolean isHandled = mFastBitmapDrawable.onStateChange(state); + + // Animate to default state from pressed state. + assertTrue("State change not handled.", isHandled); + assertEquals("Duration not correct.", mFastBitmapDrawable.mScaleAnimation.getDuration(), + CLICK_FEEDBACK_DURATION); + mFastBitmapDrawable.mScaleAnimation.end(); + assertEquals("End value not correct.", (float) SCALE.get(mFastBitmapDrawable), 1f, EPSILON); + assertTrue("Wrong interpolator used.", + mFastBitmapDrawable.mScaleAnimation.getInterpolator() + instanceof DecelerateInterpolator); + } + + @Test + public void testOnStateChange_stateDefaultFromHovered() { + mFastBitmapDrawable.mIsHovered = true; + SCALE.setValue(mFastBitmapDrawable, HOVERED_SCALE); + int[] state = new int[]{}; + + boolean isHandled = mFastBitmapDrawable.onStateChange(state); + + // Animate to default state from hovered state. + assertTrue("State change not handled.", isHandled); + assertEquals("Duration not correct.", mFastBitmapDrawable.mScaleAnimation.getDuration(), + HOVER_FEEDBACK_DURATION); + mFastBitmapDrawable.mScaleAnimation.end(); + assertEquals("End value not correct.", (float) SCALE.get(mFastBitmapDrawable), 1f, EPSILON); + assertTrue("Wrong interpolator used.", + mFastBitmapDrawable.mScaleAnimation.getInterpolator() instanceof PathInterpolator); + } + + @Test + public void testOnStateChange_stateHoveredWhilePartiallyScaled() { + SCALE.setValue(mFastBitmapDrawable, 0.5f); + int[] state = new int[]{android.R.attr.state_hovered}; + + boolean isHandled = mFastBitmapDrawable.onStateChange(state); + + // Animate to hovered state from midway to pressed state. + assertTrue("State change not handled.", isHandled); + assertEquals("Duration not correct.", + mFastBitmapDrawable.mScaleAnimation.getDuration(), HOVER_FEEDBACK_DURATION); + mFastBitmapDrawable.mScaleAnimation.end(); + assertEquals("End value not correct.", + (float) SCALE.get(mFastBitmapDrawable), HOVERED_SCALE, EPSILON); + assertTrue("Wrong interpolator used.", + mFastBitmapDrawable.mScaleAnimation.getInterpolator() instanceof PathInterpolator); + } + + @Test + public void testOnStateChange_statePressedWhilePartiallyScaled() { + SCALE.setValue(mFastBitmapDrawable, 0.5f); + int[] state = new int[]{android.R.attr.state_pressed}; + + boolean isHandled = mFastBitmapDrawable.onStateChange(state); + + // Animate to pressed state from midway to hovered state. + assertTrue("State change not handled.", isHandled); + assertEquals("Duration not correct.", + mFastBitmapDrawable.mScaleAnimation.getDuration(), CLICK_FEEDBACK_DURATION); + mFastBitmapDrawable.mScaleAnimation.end(); + assertEquals("End value not correct.", + (float) SCALE.get(mFastBitmapDrawable), PRESSED_SCALE, EPSILON); + assertTrue("Wrong interpolator used.", + mFastBitmapDrawable.mScaleAnimation.getInterpolator() + instanceof AccelerateInterpolator); + } + + @Test + public void testOnStateChange_stateDefaultFromPressedNotVisible() { + when(mFastBitmapDrawable.isVisible()).thenReturn(false); + mFastBitmapDrawable.mIsPressed = true; + SCALE.setValue(mFastBitmapDrawable, PRESSED_SCALE); + clearInvocations(mFastBitmapDrawable); + int[] state = new int[]{}; + + boolean isHandled = mFastBitmapDrawable.onStateChange(state); + + // No animations when state was pressed but drawable no longer visible. Set values directly. + assertTrue("State change not handled.", isHandled); + assertNull("Scale animation not null.", mFastBitmapDrawable.mScaleAnimation); + assertEquals("End value not correct.", (float) SCALE.get(mFastBitmapDrawable), 1f, EPSILON); + verify(mFastBitmapDrawable).invalidateSelf(); + } + + @Test + public void testOnStateChange_stateDefaultFromHoveredNotVisible() { + when(mFastBitmapDrawable.isVisible()).thenReturn(false); + mFastBitmapDrawable.mIsHovered = true; + SCALE.setValue(mFastBitmapDrawable, HOVERED_SCALE); + clearInvocations(mFastBitmapDrawable); + int[] state = new int[]{}; + + boolean isHandled = mFastBitmapDrawable.onStateChange(state); + + // No animations when state was hovered but drawable no longer visible. Set values directly. + assertTrue("State change not handled.", isHandled); + assertNull("Scale animation not null.", mFastBitmapDrawable.mScaleAnimation); + assertEquals("End value not correct.", (float) SCALE.get(mFastBitmapDrawable), 1f, EPSILON); + verify(mFastBitmapDrawable).invalidateSelf(); + } +} diff --git a/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java b/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java index 168ed9f18c..e12cf2d068 100644 --- a/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java +++ b/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java @@ -395,6 +395,28 @@ public class TaplTestsLauncher3 extends AbstractLauncherUiTest { } } + @Test + public void testLaunchHomeScreenMenuItem() { + // Drag the test app icon to home screen and open short cut menu from the icon + final HomeAllApps allApps = mLauncher.getWorkspace().switchToAllApps(); + allApps.freeze(); + try { + allApps.getAppIcon(APP_NAME).dragToWorkspace(false, false); + final AppIconMenu menu = mLauncher.getWorkspace().getWorkspaceAppIcon( + APP_NAME).openDeepShortcutMenu(); + + executeOnLauncher( + launcher -> assertTrue("Launcher internal state didn't switch to Showing Menu", + isOptionsPopupVisible(launcher))); + + final AppIconMenuItem menuItem = menu.getMenuItem(1); + assertEquals("Wrong menu item", "Shortcut 2", menuItem.getText()); + menuItem.launch(getAppPackageName()); + } finally { + allApps.unfreeze(); + } + } + @PlatinumTest(focusArea = "launcher") @Test @PortraitLandscape diff --git a/tests/src/com/android/launcher3/util/viewcapture_analysis/FlashDetector.java b/tests/src/com/android/launcher3/util/viewcapture_analysis/FlashDetector.java index fc17b6747c..388a59a501 100644 --- a/tests/src/com/android/launcher3/util/viewcapture_analysis/FlashDetector.java +++ b/tests/src/com/android/launcher3/util/viewcapture_analysis/FlashDetector.java @@ -56,7 +56,10 @@ final class FlashDetector extends AnomalyDetector { RECENTS_DRAG_LAYER + "FallbackRecentsView:id/overview_panel|TaskView|IconView:id/icon", DRAG_LAYER + "SearchContainerView:id/apps_view", DRAG_LAYER + "LauncherDragView", - DRAG_LAYER + "FloatingTaskView|FloatingTaskThumbnailView:id/thumbnail" + DRAG_LAYER + "FloatingTaskView|FloatingTaskThumbnailView:id/thumbnail", + DRAG_LAYER + + "WidgetsFullSheet|SpringRelativeLayout:id/container|WidgetsRecyclerView:id" + + "/primary_widgets_list_view|WidgetsListHeader:id/widgets_list_header" )); // Per-AnalysisNode data that's specific to this detector. diff --git a/tests/tapl/com/android/launcher3/tapl/AllApps.java b/tests/tapl/com/android/launcher3/tapl/AllApps.java index 23d09d4928..fb08ea44eb 100644 --- a/tests/tapl/com/android/launcher3/tapl/AllApps.java +++ b/tests/tapl/com/android/launcher3/tapl/AllApps.java @@ -336,4 +336,11 @@ public abstract class AllApps extends LauncherInstrumentation.VisibleContainer { final Bundle testInfo = mLauncher.getTestInfo(TestProtocol.REQUEST_APP_LIST_FREEZE_FLAGS); return testInfo == null ? 0 : testInfo.getInt(TestProtocol.TEST_INFO_RESPONSE_FIELD); } + + /** + * Return the QSB UI object on the AllApps screen. + * @return the QSB UI object. + */ + @NonNull + public abstract Qsb getQsb(); } \ No newline at end of file diff --git a/tests/tapl/com/android/launcher3/tapl/AllAppsFromTaskbar.java b/tests/tapl/com/android/launcher3/tapl/AllAppsFromTaskbar.java index c4744a1e67..0e0291f82c 100644 --- a/tests/tapl/com/android/launcher3/tapl/AllAppsFromTaskbar.java +++ b/tests/tapl/com/android/launcher3/tapl/AllAppsFromTaskbar.java @@ -62,4 +62,10 @@ public class AllAppsFromTaskbar extends AllApps { return mLauncher.getTestInfo(TestProtocol.REQUEST_TASKBAR_APPS_LIST_SCROLL_Y) .getInt(TestProtocol.TEST_INFO_RESPONSE_FIELD); } + + @NonNull + @Override + public TaskbarAllAppsQsb getQsb() { + return new TaskbarAllAppsQsb(mLauncher, verifyActiveContainer()); + } } diff --git a/tests/tapl/com/android/launcher3/tapl/AllAppsQsb.java b/tests/tapl/com/android/launcher3/tapl/AllAppsQsb.java index 0931cd46b0..1692351181 100644 --- a/tests/tapl/com/android/launcher3/tapl/AllAppsQsb.java +++ b/tests/tapl/com/android/launcher3/tapl/AllAppsQsb.java @@ -22,16 +22,7 @@ import androidx.test.uiautomator.UiObject2; */ class AllAppsQsb extends Qsb { - private final UiObject2 mAllAppsContainer; - AllAppsQsb(LauncherInstrumentation launcher, UiObject2 allAppsContainer) { - super(launcher); - mAllAppsContainer = allAppsContainer; - waitForQsbObject(); - } - - @Override - protected UiObject2 waitForQsbObject() { - return mLauncher.waitForObjectInContainer(mAllAppsContainer, "search_container_all_apps"); + super(launcher, allAppsContainer, "search_container_all_apps"); } } diff --git a/tests/tapl/com/android/launcher3/tapl/HomeAllApps.java b/tests/tapl/com/android/launcher3/tapl/HomeAllApps.java index a03472a553..33c6334834 100644 --- a/tests/tapl/com/android/launcher3/tapl/HomeAllApps.java +++ b/tests/tapl/com/android/launcher3/tapl/HomeAllApps.java @@ -117,11 +117,8 @@ public class HomeAllApps extends AllApps { } } - /** - * Return the QSB UI object on the AllApps screen. - * @return the QSB UI object. - */ @NonNull + @Override public Qsb getQsb() { return new AllAppsQsb(mLauncher, verifyActiveContainer()); } diff --git a/tests/tapl/com/android/launcher3/tapl/HomeQsb.java b/tests/tapl/com/android/launcher3/tapl/HomeQsb.java index 20d09a1e16..5385c65165 100644 --- a/tests/tapl/com/android/launcher3/tapl/HomeQsb.java +++ b/tests/tapl/com/android/launcher3/tapl/HomeQsb.java @@ -22,16 +22,7 @@ import androidx.test.uiautomator.UiObject2; */ class HomeQsb extends Qsb { - private final UiObject2 mHotSeat; - HomeQsb(LauncherInstrumentation launcher, UiObject2 hotseat) { - super(launcher); - mHotSeat = hotseat; - waitForQsbObject(); - } - - @Override - protected UiObject2 waitForQsbObject() { - return mLauncher.waitForObjectInContainer(mHotSeat, "search_container_hotseat"); + super(launcher, hotseat, "search_container_hotseat"); } } diff --git a/tests/tapl/com/android/launcher3/tapl/Qsb.java b/tests/tapl/com/android/launcher3/tapl/Qsb.java index 6bc4f2109c..7f3f61d81f 100644 --- a/tests/tapl/com/android/launcher3/tapl/Qsb.java +++ b/tests/tapl/com/android/launcher3/tapl/Qsb.java @@ -30,13 +30,21 @@ public abstract class Qsb { private static final String ASSISTANT_APP_PACKAGE = "com.google.android.googlequicksearchbox"; private static final String ASSISTANT_ICON_RES_ID = "mic_icon"; protected final LauncherInstrumentation mLauncher; + private final UiObject2 mContainer; + private final String mQsbResName; - protected Qsb(LauncherInstrumentation launcher) { + protected Qsb(LauncherInstrumentation launcher, UiObject2 container, String qsbResName) { mLauncher = launcher; + mContainer = container; + mQsbResName = qsbResName; + waitForQsbObject(); } // Waits for the quick search box. - protected abstract UiObject2 waitForQsbObject(); + private UiObject2 waitForQsbObject() { + return mLauncher.waitForObjectInContainer(mContainer, mQsbResName); + } + /** * Launch assistant app by tapping mic icon on qsb. */ @@ -79,8 +87,12 @@ public abstract class Qsb { mLauncher.waitForIdle(); try (LauncherInstrumentation.Closable c2 = mLauncher.addContextLayer( "clicked qsb to open search result page")) { - return new SearchResultFromQsb(mLauncher); + return createSearchResult(); } } } + + protected SearchResultFromQsb createSearchResult() { + return new SearchResultFromQsb(mLauncher); + } } diff --git a/tests/tapl/com/android/launcher3/tapl/SearchResultFromQsb.java b/tests/tapl/com/android/launcher3/tapl/SearchResultFromQsb.java index 80176e993f..8c3402fd45 100644 --- a/tests/tapl/com/android/launcher3/tapl/SearchResultFromQsb.java +++ b/tests/tapl/com/android/launcher3/tapl/SearchResultFromQsb.java @@ -32,7 +32,7 @@ public class SearchResultFromQsb { // This particular ID change should happen with caution private static final String SEARCH_CONTAINER_RES_ID = "search_results_list_view"; - private final LauncherInstrumentation mLauncher; + protected final LauncherInstrumentation mLauncher; SearchResultFromQsb(LauncherInstrumentation launcher) { mLauncher = launcher; @@ -49,8 +49,12 @@ public class SearchResultFromQsb { } /** Find the app from search results with app name. */ - public Launchable findAppIcon(String appName) { + public AppIcon findAppIcon(String appName) { UiObject2 icon = mLauncher.waitForLauncherObject(By.clazz(TextView.class).text(appName)); + return createAppIcon(icon); + } + + protected AppIcon createAppIcon(UiObject2 icon) { return new AllAppsAppIcon(mLauncher, icon); } diff --git a/tests/tapl/com/android/launcher3/tapl/SearchResultFromTaskbarQsb.java b/tests/tapl/com/android/launcher3/tapl/SearchResultFromTaskbarQsb.java new file mode 100644 index 0000000000..c267c9e5b3 --- /dev/null +++ b/tests/tapl/com/android/launcher3/tapl/SearchResultFromTaskbarQsb.java @@ -0,0 +1,38 @@ +/* + * 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 androidx.test.uiautomator.UiObject2; + +/** + * Operations on search result page opened from Taskbar qsb. + */ +public class SearchResultFromTaskbarQsb extends SearchResultFromQsb { + + SearchResultFromTaskbarQsb(LauncherInstrumentation launcher) { + super(launcher); + } + + @Override + public TaskbarAppIcon findAppIcon(String appName) { + return (TaskbarAppIcon) super.findAppIcon(appName); + } + + @Override + protected TaskbarAppIcon createAppIcon(UiObject2 icon) { + return new TaskbarAppIcon(mLauncher, icon); + } +} diff --git a/tests/tapl/com/android/launcher3/tapl/TaskbarAllAppsQsb.java b/tests/tapl/com/android/launcher3/tapl/TaskbarAllAppsQsb.java new file mode 100644 index 0000000000..7cecd3e553 --- /dev/null +++ b/tests/tapl/com/android/launcher3/tapl/TaskbarAllAppsQsb.java @@ -0,0 +1,38 @@ +/* + * 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 androidx.test.uiautomator.UiObject2; + +/** + * Operations on Taskbar AllApp screen qsb. + */ +public class TaskbarAllAppsQsb extends Qsb { + + TaskbarAllAppsQsb(LauncherInstrumentation launcher, UiObject2 allAppsContainer) { + super(launcher, allAppsContainer, "search_container_all_apps"); + } + + @Override + public SearchResultFromTaskbarQsb showSearchResult() { + return (SearchResultFromTaskbarQsb) super.showSearchResult(); + } + + @Override + protected SearchResultFromTaskbarQsb createSearchResult() { + return new SearchResultFromTaskbarQsb(mLauncher); + } +}