diff --git a/aconfig/launcher_search.aconfig b/aconfig/launcher_search.aconfig
index b98eee6e32..4fa91a4c4b 100644
--- a/aconfig/launcher_search.aconfig
+++ b/aconfig/launcher_search.aconfig
@@ -43,6 +43,15 @@ flag {
bug: "289223923"
}
+flag {
+ name: "nudge_pill"
+ namespace: "launcher_search"
+ description: "This flag enables change the nav or 3 button to nudge related edu (pill or icon) for CtS."
+ bug: "409121556"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
flag {
name: "private_space_add_floating_mask_view"
diff --git a/quickstep/res/drawable/bg_nudge_view.xml b/quickstep/res/drawable/bg_nudge_view.xml
new file mode 100644
index 0000000000..9b75803436
--- /dev/null
+++ b/quickstep/res/drawable/bg_nudge_view.xml
@@ -0,0 +1,70 @@
+
+
+
+
+ -
+
+
+
+
+
+ -
+
+
+
+
+
+ -
+
+
+
+
+
+ -
+
+
+
+
+
+ -
+
+
+
+
+
+
\ No newline at end of file
diff --git a/quickstep/res/layout/taskbar.xml b/quickstep/res/layout/taskbar.xml
index 54f9ae87df..7fb2945413 100644
--- a/quickstep/res/layout/taskbar.xml
+++ b/quickstep/res/layout/taskbar.xml
@@ -83,6 +83,17 @@
android:layout_gravity="end"/>
+
+
@color/materialColorPrimary
+
+ #4285F4
+ #7430E2
+ #2A7EC8
+ #2DB8BD
\ No newline at end of file
diff --git a/quickstep/res/values/dimens.xml b/quickstep/res/values/dimens.xml
index 75bf331e0e..73fbe7d091 100644
--- a/quickstep/res/values/dimens.xml
+++ b/quickstep/res/values/dimens.xml
@@ -363,6 +363,9 @@
108dp
316dp
4dp
+ 24dp
+ 108dp
+ 200%
10dp
1dp
112dp
diff --git a/quickstep/src/com/android/launcher3/taskbar/NudgeView.java b/quickstep/src/com/android/launcher3/taskbar/NudgeView.java
new file mode 100644
index 0000000000..543475935e
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/NudgeView.java
@@ -0,0 +1,207 @@
+/*
+ * Copyright (C) 2025 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.taskbar;
+
+import static com.android.launcher3.anim.AnimatedFloat.VALUE;
+import static com.android.launcher3.anim.AnimatorListeners.forEndCallback;
+import static com.android.launcher3.taskbar.StashedHandleViewController.ALPHA_INDEX_NUDGED;
+import static com.android.launcher3.taskbar.StashedHandleViewController.ALPHA_INDEX_STASHED;
+
+import android.animation.AnimatorSet;
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+
+import androidx.annotation.Nullable;
+
+import com.android.launcher3.Flags;
+import com.android.launcher3.R;
+import com.android.launcher3.anim.AnimatedFloat;
+import com.android.launcher3.anim.SpringAnimationBuilder;
+import com.android.launcher3.util.MultiValueAlpha;
+
+public class NudgeView extends ImageView {
+
+ private static final float DAMPING_RATIO = 0.6f;
+ private static final float STIFFNESS = 380f;
+
+ private AnimatorSet mAnimatorSet;
+ private boolean mNudgeShown;
+ private int mNudgePillWidth;
+ private int mNudgePillHeight;
+ private int mStashedHandleWidth;
+ private int mStashedHandleHeight;
+ private ImageView mNudgeIcon;
+ private MultiValueAlpha mStashedHandleViewAlpha;
+
+ private final AnimatedFloat mAlphaForHandleView = new AnimatedFloat(
+ this::updateAlphaForHandleView);
+ private final AnimatedFloat mAlphaForNudgeIcon = new AnimatedFloat(
+ this::updateAlphaForNudgeIcon);
+ private final AnimatedFloat mWidthForNudgePill = new AnimatedFloat(
+ this::updateWidthForNudge);
+ private final AnimatedFloat mHeightForNudgePill = new AnimatedFloat(
+ this::updateHeightForNudge);
+
+ public NudgeView(Context context) {
+ this(context, null);
+ }
+
+ public NudgeView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public NudgeView(Context context, AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public NudgeView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ if (Flags.nudgePill()) {
+ mNudgePillWidth = context.getResources().getDimensionPixelSize(
+ R.dimen.taskbar_nudge_pill_width);
+ mNudgePillHeight = context.getResources().getDimensionPixelSize(
+ R.dimen.taskbar_nudge_pill_height);
+ mStashedHandleWidth = context.getResources().getDimensionPixelSize(
+ R.dimen.taskbar_nudge_pill_width);
+ mStashedHandleHeight = context.getResources().getDimensionPixelSize(
+ R.dimen.taskbar_stashed_handle_height);
+ }
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ mNudgeIcon = findViewById(R.id.nudge_icon);
+ }
+
+ void updateNudgeIcon(boolean showNudge, @Nullable Drawable icon,
+ @Nullable MultiValueAlpha stashedHandleViewAlpha) {
+ if (!Flags.nudgePill() || stashedHandleViewAlpha == null) {
+ return;
+ }
+ mStashedHandleViewAlpha = stashedHandleViewAlpha;
+ // Set update visibility to false when showing the nudge otherwise
+ // StashHandleView will be INVISIBLE at low alpha resulting in being unable to invoke CtS.
+ mStashedHandleViewAlpha.setUpdateVisibility(false);
+ mNudgeIcon.setImageDrawable(null);
+ if (icon != null) {
+ mNudgeIcon.setImageDrawable(icon);
+ }
+ mNudgeIcon.setVisibility(VISIBLE);
+ if (showNudge && !mNudgeShown) {
+ post(() -> animationBarToPill(true /* showNudge */));
+ } else if (!showNudge){
+ post(() -> animationBarToPill(false /* showNudge */));
+ }
+ }
+
+ private void animationBarToPill(boolean showNudge) {
+ if (!Flags.nudgePill()) {
+ return;
+ }
+ if (mAnimatorSet != null && mAnimatorSet.isRunning()) {
+ mAnimatorSet.cancel();
+ }
+ mNudgeShown = showNudge;
+ // Alpha
+ float targetIconAlpha = showNudge ? 1.0f : 0;
+ float targetHandleViewAlpha = showNudge ? 0 : 1.0f;
+ // Animate from navBar -> nudgePill if showNudge is true.
+ float startWidth = showNudge ? mStashedHandleWidth : mNudgePillWidth;
+ float startHeight = showNudge ? mStashedHandleHeight : mNudgePillHeight;
+ float endWidth = showNudge ? mNudgePillWidth : mStashedHandleWidth;
+ float endHeight = showNudge ? mNudgePillHeight : mStashedHandleHeight;
+
+ ValueAnimator alphaNudgeIcon = new SpringAnimationBuilder(mContext)
+ .setStartValue(mNudgeIcon.getAlpha())
+ .setEndValue(targetIconAlpha)
+ .setDampingRatio(DAMPING_RATIO)
+ .setStiffness(STIFFNESS)
+ .build(mAlphaForNudgeIcon, VALUE);
+ if (!showNudge) {
+ alphaNudgeIcon.addListener(forEndCallback(() -> {
+ mNudgeIcon.setVisibility(GONE);
+ mStashedHandleViewAlpha.setUpdateVisibility(true);
+ }));
+ }
+ ValueAnimator alphaHandleView = new SpringAnimationBuilder(mContext)
+ .setStartValue(mStashedHandleViewAlpha.get(ALPHA_INDEX_STASHED).getValue())
+ .setEndValue(targetHandleViewAlpha)
+ .setDampingRatio(DAMPING_RATIO)
+ .setStiffness(STIFFNESS)
+ .build(mAlphaForHandleView, VALUE);
+ ValueAnimator widthAnimation = new SpringAnimationBuilder(mContext)
+ .setStartValue(startWidth)
+ .setEndValue(endWidth)
+ .setDampingRatio(DAMPING_RATIO)
+ .setStiffness(STIFFNESS)
+ .build(mWidthForNudgePill, VALUE);
+ ValueAnimator heightAnimation = new SpringAnimationBuilder(mContext)
+ .setStartValue(startHeight)
+ .setEndValue(endHeight)
+ .setDampingRatio(DAMPING_RATIO)
+ .setStiffness(STIFFNESS)
+ .build(mHeightForNudgePill, VALUE);
+ mAnimatorSet = new AnimatorSet();
+ mAnimatorSet.playTogether(
+ alphaNudgeIcon, alphaHandleView,
+ widthAnimation, heightAnimation
+ );
+ mAnimatorSet.start();
+ }
+
+ private void updateAlphaForHandleView() {
+ if (!Flags.nudgePill()) {
+ return;
+ }
+ float alpha = mAlphaForHandleView.value;
+ mStashedHandleViewAlpha.get(ALPHA_INDEX_NUDGED).setValue(alpha);
+ }
+
+ private void updateAlphaForNudgeIcon() {
+ if (!Flags.nudgePill()) {
+ return;
+ }
+ float alpha = mAlphaForNudgeIcon.value;
+ mNudgeIcon.setAlpha(alpha);
+ }
+
+ private void updateWidthForNudge() {
+ if (!Flags.nudgePill()) {
+ return;
+ }
+ float width = mWidthForNudgePill.value;
+ ViewGroup.MarginLayoutParams nudgePillLayoutParams =
+ (ViewGroup.MarginLayoutParams) mNudgeIcon.getLayoutParams();
+ nudgePillLayoutParams.width = (int) width;
+ mNudgeIcon.setLayoutParams(nudgePillLayoutParams);
+ }
+
+ private void updateHeightForNudge() {
+ if (!Flags.nudgePill()) {
+ return;
+ }
+ float height = mHeightForNudgePill.value;
+ ViewGroup.MarginLayoutParams nudgePillLayoutParams =
+ (ViewGroup.MarginLayoutParams) mNudgeIcon.getLayoutParams();
+ nudgePillLayoutParams.height = (int) height;
+ mNudgeIcon.setLayoutParams(nudgePillLayoutParams);
+ }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/NudgeViewController.java b/quickstep/src/com/android/launcher3/taskbar/NudgeViewController.java
new file mode 100644
index 0000000000..89d4ec4476
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/NudgeViewController.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2025 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.taskbar;
+
+import static android.content.Context.RECEIVER_EXPORTED;
+
+import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
+
+import android.content.Intent;
+import android.content.res.Resources;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+
+import androidx.annotation.Nullable;
+
+import com.android.launcher3.Flags;
+import com.android.launcher3.R;
+import com.android.launcher3.util.SimpleBroadcastReceiver;
+
+import java.io.PrintWriter;
+
+/**
+ * NudgeViewController handles the broadcasts sent to launcher which will then update the view.
+ */
+public class NudgeViewController implements TaskbarControllers.LoggableTaskbarController{
+
+ private static final String NAV_UPDATE_ACTION = "android.app.action.UPDATE_NAVBAR";
+ private static final String GAME = "game";
+ private static final String TRANSLATE = "translate";
+ private static final String SHOW_NUDGE = "showNudge";
+ private static final String NUDGE_ICON = "nudgeIcon";
+ private final TaskbarActivityContext mActivity;
+ @Nullable
+ private final NudgeView mNudgeView;
+ private final Drawable mTranslateIcon;
+ private final Drawable mGameIcon;
+
+ @Nullable
+ private SimpleBroadcastReceiver mNudgeReceiver;
+
+ public NudgeViewController(TaskbarActivityContext activity,
+ @Nullable NudgeView nudgeView) {
+ mActivity = activity;
+ mNudgeView = nudgeView;
+ final Resources resources = mActivity.getResources();
+ if (Flags.nudgePill() && mNudgeView != null) {
+ mNudgeReceiver = new SimpleBroadcastReceiver(
+ mActivity, UI_HELPER_EXECUTOR, this::shouldChangeNavBar);
+ mNudgeReceiver.register(RECEIVER_EXPORTED, NAV_UPDATE_ACTION);
+ }
+ mTranslateIcon = resources.getDrawable(R.drawable.ic_translate);
+ mGameIcon = resources.getDrawable(R.drawable.ic_game);
+ }
+
+ private void shouldChangeNavBar(Intent i) {
+ if (!mActivity.isPhoneGestureNavMode() || !Flags.nudgePill()) {
+ return;
+ }
+ Bundle bundle = i.getExtras();
+ boolean showNudge = bundle.getBoolean(SHOW_NUDGE, false);
+ String iconToUse = bundle.getString(NUDGE_ICON, "");
+ Drawable icon = null;
+ if (GAME.equals(iconToUse)) {
+ icon = mGameIcon;
+ } else if (TRANSLATE.equals(iconToUse)) {
+ icon = mTranslateIcon;
+ }
+ // Nudge icon will only show if there is a valid icon to show.
+ mNudgeView.updateNudgeIcon(icon != null && showNudge /* showNudge */, icon,
+ mActivity.getControllers().stashedHandleViewController.getStashedHandleAlpha());
+ }
+
+ public void onDestroy() {
+ if (mNudgeReceiver != null) {
+ mNudgeReceiver.unregisterReceiverSafely();
+ }
+ mNudgeReceiver = null;
+ }
+
+ @Override
+ public void dumpLogs(String prefix, PrintWriter pw) {
+ pw.println(prefix + "NudgeViewController:");
+ pw.println(prefix + "\tmNudgeView=" + mNudgeView);
+ pw.println(prefix + "\tmNudgeReceiver=" + mNudgeReceiver);
+ }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/StashedHandleViewController.java b/quickstep/src/com/android/launcher3/taskbar/StashedHandleViewController.java
index dd0ce27c91..0fa5fd5370 100644
--- a/quickstep/src/com/android/launcher3/taskbar/StashedHandleViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/StashedHandleViewController.java
@@ -39,7 +39,6 @@ import com.android.launcher3.anim.AnimatedFloat;
import com.android.launcher3.anim.RevealOutlineAnimation;
import com.android.launcher3.anim.RoundedRectRevealOutlineProvider;
import com.android.launcher3.util.Executors;
-import com.android.launcher3.util.MultiPropertyFactory;
import com.android.launcher3.util.MultiValueAlpha;
import com.android.quickstep.NavHandle;
import com.android.systemui.shared.system.QuickStepContract.SystemUiStateFlags;
@@ -57,7 +56,8 @@ public class StashedHandleViewController implements TaskbarControllers.LoggableT
public static final int ALPHA_INDEX_HOME_DISABLED = 1;
public static final int ALPHA_INDEX_ASSISTANT_INVOKED = 2;
public static final int ALPHA_INDEX_HIDDEN_WHILE_DREAMING = 3;
- private static final int NUM_ALPHA_CHANNELS = 4;
+ public static final int ALPHA_INDEX_NUDGED = 4;
+ private static final int NUM_ALPHA_CHANNELS = 5;
// Values for long press animations, picked to most closely match navbar spec.
private static final float SCALE_TOUCH_ANIMATION_SHRINK = 0.85f;
@@ -203,7 +203,7 @@ public class StashedHandleViewController implements TaskbarControllers.LoggableT
mRegionSamplingHelper = null;
}
- public MultiPropertyFactory getStashedHandleAlpha() {
+ public MultiValueAlpha getStashedHandleAlpha() {
return mTaskbarStashedHandleAlpha;
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
index ba1ce18893..0044a809d0 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
@@ -307,6 +307,7 @@ public class TaskbarActivityContext extends BaseTaskbarContext {
TaskbarScrimView taskbarScrimView = mDragLayer.findViewById(R.id.taskbar_scrim);
NearestTouchFrame navButtonsView = mDragLayer.findViewById(R.id.navbuttons_view);
StashedHandleView stashedHandleView = mDragLayer.findViewById(R.id.stashed_handle);
+ NudgeView nudgeView = mDragLayer.findViewById(R.id.nudge_icon);
BubbleBarView bubbleBarView = mDragLayer.findViewById(R.id.taskbar_bubbles);
FrameLayout bubbleBarContainer = mDragLayer.findViewById(R.id.taskbar_bubbles_container);
StashedHandleView bubbleHandleView = mDragLayer.findViewById(R.id.stashed_bubble_handle);
@@ -391,7 +392,8 @@ public class TaskbarActivityContext extends BaseTaskbarContext {
bubbleControllersOptional,
new TaskbarDesktopModeController(this,
DesktopVisibilityController.INSTANCE.get(this)),
- new NudgeController(this));
+ new NudgeController(this),
+ new NudgeViewController(this, nudgeView));
mLauncherPrefs = LauncherPrefs.get(this);
onViewCreated();
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarControllers.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarControllers.java
index bd6bf68f0d..503cb7e663 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarControllers.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarControllers.java
@@ -69,6 +69,7 @@ public class TaskbarControllers {
public final Optional bubbleControllers;
public final TaskbarDesktopModeController taskbarDesktopModeController;
public final NudgeController nudgeController;
+ public final NudgeViewController nudgeViewController;
@Nullable private LoggableTaskbarController[] mControllersToLog = null;
@Nullable private BackgroundRendererController[] mBackgroundRendererControllers = null;
@@ -118,7 +119,8 @@ public class TaskbarControllers {
TaskbarPinningController taskbarPinningController,
Optional bubbleControllers,
TaskbarDesktopModeController taskbarDesktopModeController,
- NudgeController nudgeController) {
+ NudgeController nudgeController,
+ NudgeViewController nudgeViewController) {
this.taskbarActivityContext = taskbarActivityContext;
this.taskbarDragController = taskbarDragController;
this.navButtonController = navButtonController;
@@ -147,6 +149,7 @@ public class TaskbarControllers {
this.bubbleControllers = bubbleControllers;
this.taskbarDesktopModeController = taskbarDesktopModeController;
this.nudgeController = nudgeController;
+ this.nudgeViewController = nudgeViewController;
}
/**
@@ -273,6 +276,7 @@ public class TaskbarControllers {
taskbarUnfoldAnimationController.onDestroy();
taskbarViewController.onDestroy();
stashedHandleViewController.onDestroy();
+ nudgeViewController.onDestroy();
taskbarAutohideSuspendController.onDestroy();
taskbarPopupController.onDestroy();
taskbarForceVisibleImmersiveController.onDestroy();
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarBaseTestCase.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarBaseTestCase.kt
index 52d288a723..bfd5d76923 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarBaseTestCase.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarBaseTestCase.kt
@@ -60,6 +60,7 @@ abstract class TaskbarBaseTestCase {
@Mock lateinit var optionalBubbleControllers: Optional
@Mock lateinit var taskbarDesktopModeController: TaskbarDesktopModeController
@Mock lateinit var nudgeController: NudgeController
+ @Mock lateinit var nudgeViewController: NudgeViewController
lateinit var taskbarControllers: TaskbarControllers
@@ -103,6 +104,7 @@ abstract class TaskbarBaseTestCase {
optionalBubbleControllers,
taskbarDesktopModeController,
nudgeController,
+ nudgeViewController
)
}
}
diff --git a/res/drawable/ic_game.xml b/res/drawable/ic_game.xml
new file mode 100644
index 0000000000..fcfa8f78de
--- /dev/null
+++ b/res/drawable/ic_game.xml
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/res/drawable/ic_translate.xml b/res/drawable/ic_translate.xml
new file mode 100644
index 0000000000..add47bee8d
--- /dev/null
+++ b/res/drawable/ic_translate.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+