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 @@ + + + + + + +