From 0ee66478c6d5882131ff839252f431306897b5a9 Mon Sep 17 00:00:00 2001 From: Sunny Goyal Date: Thu, 17 Aug 2023 16:19:22 -0700 Subject: [PATCH 1/8] Adding CannedAnimationCoordinator to enable running a predefined animation in Launcher > The coordinator takes care of recreating animations on layout changes and linking it with state animation > This also ensures that there is only one animation playing at a time Bug: 293464892 Test: Verified that calling with multiple animations cancels previos call Flag: None Change-Id: Iab161720c94843b5834734da78167344c17539c8 --- src/com/android/launcher3/Launcher.java | 11 ++ .../util/CannedAnimationCoordinator.kt | 164 ++++++++++++++++++ .../util/MultiScalePropertyFactory.java | 5 +- 3 files changed, 177 insertions(+), 3 deletions(-) create mode 100644 src/com/android/launcher3/util/CannedAnimationCoordinator.kt diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java index 4e7a8843a3..a0f756a698 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) { @@ -3398,4 +3402,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/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)); } From a66a121becd70908db00712726f447bcb0bbd8b9 Mon Sep 17 00:00:00 2001 From: helencheuk Date: Mon, 21 Aug 2023 16:08:40 +0100 Subject: [PATCH 2/8] Add test to long press icon on home screen Fix: 292083404 Test: TaplTestsLauncher3#testLaunchHomeScreenMenuItem Change-Id: I301ac719aeda744e8b8dca9a405208625eff89e7 --- .../launcher3/ui/TaplTestsLauncher3.java | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java b/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java index 1ade1b0c55..a0dbb7aee0 100644 --- a/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java +++ b/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java @@ -391,6 +391,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 From 65daa2da8d5f8c0292b1156b198372135417beb0 Mon Sep 17 00:00:00 2001 From: Vadim Tryshev Date: Tue, 22 Aug 2023 12:09:16 -0700 Subject: [PATCH 3/8] Properly marking TaplTestsSplitscreen with test annotations Without such annotations, the test may not run Bug: 187761685 Test: presubmit Flag: N/A Change-Id: I597ca8a8f176c1f5ee39e4fa7a7c3ac938b53b7f --- .../src/com/android/quickstep/TaplTestsSplitscreen.java | 5 +++++ 1 file changed, 5 insertions(+) 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 = From 42907f022e9282b66338d5626b27a65640064f4b Mon Sep 17 00:00:00 2001 From: Brian Isganitis Date: Fri, 11 Aug 2023 16:00:38 -0400 Subject: [PATCH 4/8] Introduce TAPL APIs for Taskbar QSB and search results. Includes support for interacting with app search results for Taskbar All Apps. Design doc: go/toasty-taskbar-tapl Test: Compiles Bug: 295366997 Flag: not needed Change-Id: I07cb8a96a0aba2668ffc647ca2a6b8c1137157ca --- .../com/android/launcher3/tapl/AllApps.java | 7 ++++ .../launcher3/tapl/AllAppsFromTaskbar.java | 6 +++ .../android/launcher3/tapl/AllAppsQsb.java | 11 +----- .../android/launcher3/tapl/HomeAllApps.java | 5 +-- .../com/android/launcher3/tapl/HomeQsb.java | 11 +----- .../tapl/com/android/launcher3/tapl/Qsb.java | 18 +++++++-- .../launcher3/tapl/SearchResultFromQsb.java | 8 +++- .../tapl/SearchResultFromTaskbarQsb.java | 38 +++++++++++++++++++ .../launcher3/tapl/TaskbarAllAppsQsb.java | 38 +++++++++++++++++++ 9 files changed, 113 insertions(+), 29 deletions(-) create mode 100644 tests/tapl/com/android/launcher3/tapl/SearchResultFromTaskbarQsb.java create mode 100644 tests/tapl/com/android/launcher3/tapl/TaskbarAllAppsQsb.java 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); + } +} From d0f729d13c9ac8deec647190bb53a7345d5f8fdb Mon Sep 17 00:00:00 2001 From: Pat Manning Date: Mon, 9 Jan 2023 12:04:25 +0000 Subject: [PATCH 5/8] Do not clip scaled Launcher icons on hover. Include change for setting the hover state flag programatically, as FastBitmapDrawable does not currently support DeviceConfig flags. Fix: 243191650 Test: FastBitmapDrawableTest. Screenshot tests in another cl. Flag: ENABLE_CURSOR_HOVER_STATES Change-Id: I0eb796ae62e571a3287132bfcb99c4fca1e2fbe4 --- src/com/android/launcher3/BubbleTextView.java | 2 + src/com/android/launcher3/CellLayout.java | 1 + .../launcher3/ShortcutAndWidgetContainer.java | 1 + .../icons/FastBitmapDrawableTest.java | 329 ++++++++++++++++++ 4 files changed, 333 insertions(+) create mode 100644 tests/src/com/android/launcher3/icons/FastBitmapDrawableTest.java diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java index 360e06059f..b3230983f5 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; @@ -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); 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/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/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(); + } +} From 250b3c3ea3654c3ed1b15950e918d9057f7b64be Mon Sep 17 00:00:00 2001 From: Vadim Tryshev Date: Wed, 23 Aug 2023 12:00:52 -0700 Subject: [PATCH 6/8] Ignoring WidgetsListHeader Bug: 297079300 Test: presubmit Flag: N/A Change-Id: I7e4fb9452bfc3df26c9121bd52557d53aa7d4bb6 --- .../launcher3/util/viewcapture_analysis/FlashDetector.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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. From b3b467c27fa7215821ceda0470414528c5b30316 Mon Sep 17 00:00:00 2001 From: Kevin Lim Date: Fri, 21 Jul 2023 02:15:34 +0000 Subject: [PATCH 7/8] [NexusLauncher] Expose setLayoutHorizontal. mLayoutHorizontal is exposed so that classes that extend BubbleTextView can override this programmatically based on LayoutType. mLayoutHorizontal prevents the title in horizontal cases affecting the icon bounds measure, and shifting things such as the icon halo. Bug: b/288471727 Test: See ag/24199217 Test: New tests in SearchResultIconTest Test: No regression in SearchResultSmallIconRowTest Change-Id: I424c1597d29ed26d7dea9e666ff2d274847e3217 --- src/com/android/launcher3/BubbleTextView.java | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java index 360e06059f..c4fd311462 100644 --- a/src/com/android/launcher3/BubbleTextView.java +++ b/src/com/android/launcher3/BubbleTextView.java @@ -152,7 +152,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; @@ -665,6 +665,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 +1003,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) { From f7554ff718197b003ee054e1b860582e39f3d0af Mon Sep 17 00:00:00 2001 From: Andy Wickham Date: Wed, 16 Aug 2023 17:27:51 -0700 Subject: [PATCH 8/8] Use new AssistUtils(Base) to override SysUI Assist invocations. AssistUtils#getSysUiAssistOverrideInvocationTypes(): Sent over SysUiProxy to request overriding these invocation types. AssistUtils#tryStartAssistOverride(): Called by OverviewProxy (TouchInteractionService) for previously requested overrides. Also used within Launcher for Taskbar and QSB to override other invocation logic. May return false to indicate that the override was not handled, so a fallback may be desired. Bug: 295874732 Test: Manual Change-Id: I488f3b7da1feb2663feab5d04dfa8d605c070efb --- quickstep/res/values/override.xml | 2 + .../taskbar/TaskbarNavButtonController.java | 15 ++++--- .../com/android/quickstep/SystemUiProxy.java | 14 ++++++ .../quickstep/TouchInteractionService.java | 15 +++++++ .../quickstep/util/AssistUtilsBase.java | 45 +++++++++++++++++++ .../TaskbarNavButtonControllerTest.java | 4 ++ 6 files changed, 90 insertions(+), 5 deletions(-) create mode 100644 quickstep/src/com/android/quickstep/util/AssistUtilsBase.java 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);