Merge "Make TasksRepository listen to changes in task visuals and update" into main
diff --git a/aconfig/launcher.aconfig b/aconfig/launcher.aconfig
index dbcf49c..40c3797 100644
--- a/aconfig/launcher.aconfig
+++ b/aconfig/launcher.aconfig
@@ -316,6 +316,7 @@
     description: "Menu in Taskbar with options to launch and manage multiple instances of the same app"
     bug: "355237285"
 }
+
 flag {
     name: "navigate_to_child_preference"
     namespace: "launcher"
@@ -349,3 +350,13 @@
       purpose: PURPOSE_BUGFIX
     }
 }
+
+flag {
+    name: "ignore_three_finger_trackpad_for_nav_handle_long_press"
+    namespace: "launcher"
+    description: "Ignore three finger trackpad event for nav handle long press"
+    bug: "342143522"
+    metadata {
+      purpose: PURPOSE_BUGFIX
+    }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java b/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
index f61840a..a979d58 100644
--- a/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
@@ -63,7 +63,6 @@
 import android.graphics.Rect;
 import android.graphics.Region;
 import android.graphics.Region.Op;
-import android.graphics.drawable.AnimatedVectorDrawable;
 import android.graphics.drawable.Drawable;
 import android.graphics.drawable.PaintDrawable;
 import android.graphics.drawable.RotateDrawable;
@@ -74,8 +73,6 @@
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.View.OnAttachStateChangeListener;
-import android.view.View.OnClickListener;
-import android.view.View.OnHoverListener;
 import android.view.ViewGroup;
 import android.view.ViewTreeObserver;
 import android.view.WindowManager;
@@ -106,7 +103,6 @@
 import com.android.systemui.shared.navigationbar.KeyButtonRipple;
 import com.android.systemui.shared.rotation.FloatingRotationButton;
 import com.android.systemui.shared.rotation.RotationButton;
-import com.android.systemui.shared.rotation.RotationButtonController;
 import com.android.systemui.shared.statusbar.phone.BarTransitions;
 import com.android.systemui.shared.system.QuickStepContract;
 import com.android.systemui.shared.system.QuickStepContract.SystemUiStateFlags;
@@ -304,8 +300,13 @@
                         .get(ALPHA_INDEX_SMALL_SCREEN),
                 flags -> (flags & FLAG_SMALL_SCREEN) == 0));
 
-        mPropertyHolders.add(new StatePropertyHolder(mControllers.taskbarDragLayerController
-                .getKeyguardBgTaskbar(), flags -> (flags & FLAG_KEYGUARD_VISIBLE) == 0));
+        if (!mContext.isPhoneMode()) {
+            mPropertyHolders.add(new StatePropertyHolder(mControllers.taskbarDragLayerController
+                    .getKeyguardBgTaskbar(), flags -> (flags & FLAG_KEYGUARD_VISIBLE) == 0));
+        }
+
+        // Start at 1 because relevant flags are unset at init.
+        mOnBackgroundNavButtonColorOverrideMultiplier.value = 1;
 
         // Force nav buttons (specifically back button) to be visible during setup wizard.
         boolean isInSetup = !mContext.isUserSetupComplete();
@@ -317,39 +318,41 @@
         // - IME is showing (add separate translation for IME)
         // - VoiceInteractionWindow (assistant) is showing
         // - Keyboard shortcuts helper is showing
-        int flagsToRemoveTranslation = FLAG_NOTIFICATION_SHADE_EXPANDED | FLAG_IME_VISIBLE
-                | FLAG_VOICE_INTERACTION_WINDOW_SHOWING | FLAG_KEYBOARD_SHORTCUT_HELPER_SHOWING;
-        mPropertyHolders.add(new StatePropertyHolder(mNavButtonInAppDisplayProgressForSysui,
-                flags -> (flags & flagsToRemoveTranslation) != 0, AnimatedFloat.VALUE,
-                1, 0));
-        // Center nav buttons in new height for IME.
-        float transForIme = (mContext.getDeviceProfile().taskbarHeight
-                - mControllers.taskbarInsetsController.getTaskbarHeightForIme()) / 2f;
-        // For gesture nav, nav buttons only show for IME anyway so keep them translated down.
-        float defaultButtonTransY = alwaysShowButtons ? 0 : transForIme;
-        mPropertyHolders.add(new StatePropertyHolder(mTaskbarNavButtonTranslationYForIme,
-                flags -> (flags & FLAG_IME_VISIBLE) != 0 && !isInKidsMode, AnimatedFloat.VALUE,
-                transForIme, defaultButtonTransY));
+        if (!mContext.isPhoneMode()) {
+            int flagsToRemoveTranslation = FLAG_NOTIFICATION_SHADE_EXPANDED | FLAG_IME_VISIBLE
+                    | FLAG_VOICE_INTERACTION_WINDOW_SHOWING | FLAG_KEYBOARD_SHORTCUT_HELPER_SHOWING;
+            mPropertyHolders.add(new StatePropertyHolder(mNavButtonInAppDisplayProgressForSysui,
+                    flags -> (flags & flagsToRemoveTranslation) != 0, AnimatedFloat.VALUE,
+                    1, 0));
+            // Center nav buttons in new height for IME.
+            float transForIme = (mContext.getDeviceProfile().taskbarHeight
+                    - mControllers.taskbarInsetsController.getTaskbarHeightForIme()) / 2f;
+            // For gesture nav, nav buttons only show for IME anyway so keep them translated down.
+            float defaultButtonTransY = alwaysShowButtons ? 0 : transForIme;
+            mPropertyHolders.add(new StatePropertyHolder(mTaskbarNavButtonTranslationYForIme,
+                    flags -> (flags & FLAG_IME_VISIBLE) != 0 && !isInKidsMode, AnimatedFloat.VALUE,
+                    transForIme, defaultButtonTransY));
 
-        // Start at 1 because relevant flags are unset at init.
-        mOnBackgroundNavButtonColorOverrideMultiplier.value = 1;
-        mPropertyHolders.add(new StatePropertyHolder(
-                mOnBackgroundNavButtonColorOverrideMultiplier,
-                flags -> (flags & FLAGS_ON_BACKGROUND_COLOR_OVERRIDE_DISABLED) == 0));
+            mPropertyHolders.add(new StatePropertyHolder(
+                    mOnBackgroundNavButtonColorOverrideMultiplier,
+                    flags -> (flags & FLAGS_ON_BACKGROUND_COLOR_OVERRIDE_DISABLED) == 0));
 
-        mPropertyHolders.add(new StatePropertyHolder(
-                mSlideInViewVisibleNavButtonColorOverride,
-                flags -> (flags & FLAG_SLIDE_IN_VIEW_VISIBLE) != 0));
+            mPropertyHolders.add(new StatePropertyHolder(
+                    mSlideInViewVisibleNavButtonColorOverride,
+                    flags -> (flags & FLAG_SLIDE_IN_VIEW_VISIBLE) != 0));
+        }
 
         if (alwaysShowButtons) {
             initButtons(mNavButtonContainer, mEndContextualContainer,
                     mControllers.navButtonController);
             updateButtonLayoutSpacing();
-            updateStateForFlag(FLAG_SMALL_SCREEN, mContext.isPhoneButtonNavMode());
+            updateStateForFlag(FLAG_SMALL_SCREEN, mContext.isPhoneMode());
 
-            mPropertyHolders.add(new StatePropertyHolder(
-                    mControllers.taskbarDragLayerController.getNavbarBackgroundAlpha(),
-                    flags -> (flags & FLAG_ONLY_BACK_FOR_BOUNCER_VISIBLE) != 0));
+            if (!mContext.isPhoneMode()) {
+                mPropertyHolders.add(new StatePropertyHolder(
+                        mControllers.taskbarDragLayerController.getNavbarBackgroundAlpha(),
+                        flags -> (flags & FLAG_ONLY_BACK_FOR_BOUNCER_VISIBLE) != 0));
+            }
         } else if (!mIsImeRenderingNavButtons) {
             View imeDownButton = addButton(R.drawable.ic_sysbar_back, BUTTON_BACK,
                     mStartContextualContainer, mControllers.navButtonController, R.id.back);
@@ -711,7 +714,7 @@
     private void applyState() {
         int count = mPropertyHolders.size();
         for (int i = 0; i < count; i++) {
-            mPropertyHolders.get(i).setState(mState);
+            mPropertyHolders.get(i).setState(mState, mContext.isGestureNav());
         }
     }
 
@@ -1177,83 +1180,6 @@
         }
     }
 
-    private class RotationButtonImpl implements RotationButton {
-
-        private final ImageView mButton;
-        private AnimatedVectorDrawable mImageDrawable;
-
-        RotationButtonImpl(ImageView button) {
-            mButton = button;
-        }
-
-        @Override
-        public void setRotationButtonController(RotationButtonController rotationButtonController) {
-            // TODO(b/187754252) UI polish, different icons based on light/dark context, etc
-            mImageDrawable = (AnimatedVectorDrawable) mButton.getContext()
-                    .getDrawable(rotationButtonController.getIconResId());
-            mButton.setImageDrawable(mImageDrawable);
-            mButton.setContentDescription(mButton.getResources()
-                    .getString(R.string.accessibility_rotate_button));
-            mImageDrawable.setCallback(mButton);
-        }
-
-        @Override
-        public View getCurrentView() {
-            return mButton;
-        }
-
-        @Override
-        public boolean show() {
-            mButton.setVisibility(View.VISIBLE);
-            mState |= FLAG_ROTATION_BUTTON_VISIBLE;
-            applyState();
-            return true;
-        }
-
-        @Override
-        public boolean hide() {
-            mButton.setVisibility(View.GONE);
-            mState &= ~FLAG_ROTATION_BUTTON_VISIBLE;
-            applyState();
-            return true;
-        }
-
-        @Override
-        public boolean isVisible() {
-            return mButton.getVisibility() == View.VISIBLE;
-        }
-
-        @Override
-        public void updateIcon(int lightIconColor, int darkIconColor) {
-            // TODO(b/187754252): UI Polish
-        }
-
-        @Override
-        public void setOnClickListener(OnClickListener onClickListener) {
-            mButton.setOnClickListener(onClickListener);
-        }
-
-        @Override
-        public void setOnHoverListener(OnHoverListener onHoverListener) {
-            mButton.setOnHoverListener(onHoverListener);
-        }
-
-        @Override
-        public AnimatedVectorDrawable getImageDrawable() {
-            return mImageDrawable;
-        }
-
-        @Override
-        public void setDarkIntensity(float darkIntensity) {
-            // TODO(b/187754252) UI polish
-        }
-
-        @Override
-        public boolean acceptRotationProposal() {
-            return mButton.isAttachedToWindow();
-        }
-    }
-
     private static class StatePropertyHolder {
 
         private final float mEnabledValue, mDisabledValue;
@@ -1284,13 +1210,16 @@
             mAnimator = ObjectAnimator.ofFloat(target, property, enabledValue, disabledValue);
         }
 
-        public void setState(int flags) {
+        public void setState(int flags, boolean skipAnimation) {
             boolean isEnabled = mEnableCondition.test(flags);
             if (mIsEnabled != isEnabled) {
                 mIsEnabled = isEnabled;
                 mAnimator.cancel();
                 mAnimator.setFloatValues(mIsEnabled ? mEnabledValue : mDisabledValue);
                 mAnimator.start();
+                if (skipAnimation) {
+                    mAnimator.end();
+                }
             }
         }
 
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
index aef21aa..27c1d9c 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
@@ -219,8 +219,8 @@
         if (ENABLE_TASKBAR_NAVBAR_UNIFICATION) {
             // This gets modified in NavbarButtonsViewController, but the initial value it reads
             // may be incorrect since it's state gets destroyed on taskbar recreate, so reset here
-            mTaskbarIconAlpha.get(ALPHA_INDEX_SMALL_SCREEN)
-                    .animateToValue(mActivity.isPhoneButtonNavMode() ? 0 : 1).start();
+            mTaskbarIconAlpha.get(ALPHA_INDEX_SMALL_SCREEN).setValue(
+                    mActivity.isPhoneMode() ? 0 : 1);
         }
         if (enableTaskbarPinning()) {
             mTaskbarView.addOnLayoutChangeListener(mTaskbarViewLayoutChangeListener);
diff --git a/quickstep/src/com/android/quickstep/OrientationTouchTransformer.java b/quickstep/src/com/android/quickstep/OrientationTouchTransformer.java
index 84f6b55..a03c0f8 100644
--- a/quickstep/src/com/android/quickstep/OrientationTouchTransformer.java
+++ b/quickstep/src/com/android/quickstep/OrientationTouchTransformer.java
@@ -37,6 +37,7 @@
 import com.android.launcher3.util.DisplayController.Info;
 import com.android.launcher3.util.NavigationMode;
 import com.android.launcher3.util.window.CachedDisplayInfo;
+import com.android.systemui.shared.Flags;
 
 import java.io.PrintWriter;
 import java.util.HashMap;
@@ -242,7 +243,8 @@
         int rotation = display.rotation;
         int touchHeight = mNavBarGesturalHeight;
         OrientationRectF orientationRectF = new OrientationRectF(0, 0, size.x, size.y, rotation);
-        if (mMode == NavigationMode.NO_BUTTON) {
+        if (mMode == NavigationMode.NO_BUTTON
+                || (mMode == NavigationMode.THREE_BUTTONS && Flags.threeButtonCornerSwipe())) {
             orientationRectF.top = orientationRectF.bottom - touchHeight;
             updateAssistantRegions(orientationRectF);
         } else {
diff --git a/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java b/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
index f902284..5131774 100644
--- a/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
+++ b/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
@@ -74,6 +74,7 @@
 import com.android.quickstep.util.GestureExclusionManager;
 import com.android.quickstep.util.GestureExclusionManager.ExclusionListener;
 import com.android.quickstep.util.NavBarPosition;
+import com.android.systemui.shared.Flags;
 import com.android.systemui.shared.system.ActivityManagerWrapper;
 import com.android.systemui.shared.system.QuickStepContract;
 import com.android.systemui.shared.system.QuickStepContract.SystemUiStateFlags;
@@ -548,6 +549,13 @@
     }
 
     /**
+     * @return whether the Assistant gesture can be used in 3 button navigation mode.
+     */
+    public boolean supportsAssistantGestureInButtonNav() {
+        return Flags.threeButtonCornerSwipe();
+    }
+
+    /**
      * @param ev An ACTION_DOWN motion event
      * @return whether the given motion event can trigger the assistant over the current task.
      */
diff --git a/quickstep/src/com/android/quickstep/RotationTouchHelper.java b/quickstep/src/com/android/quickstep/RotationTouchHelper.java
index 6f1ab7d..80c07196 100644
--- a/quickstep/src/com/android/quickstep/RotationTouchHelper.java
+++ b/quickstep/src/com/android/quickstep/RotationTouchHelper.java
@@ -41,6 +41,7 @@
 import com.android.launcher3.util.NavigationMode;
 import com.android.launcher3.util.SafeCloseable;
 import com.android.quickstep.util.RecentsOrientedState;
+import com.android.systemui.shared.Flags;
 import com.android.systemui.shared.system.QuickStepContract;
 import com.android.systemui.shared.system.TaskStackChangeListener;
 import com.android.systemui.shared.system.TaskStackChangeListeners;
@@ -157,7 +158,7 @@
         // Register for navigation mode changes
         mDisplayController.addChangeListener(this);
         DisplayController.Info info = mDisplayController.getInfo();
-        onDisplayInfoChangedInternal(info, CHANGE_ALL, info.getNavigationMode().hasGestures);
+        onDisplayInfoChangedInternal(info, CHANGE_ALL, hasGestures(info.getNavigationMode()));
         runOnDestroy(() -> mDisplayController.removeChangeListener(this));
 
         mOrientationListener = new OrientationEventListener(mContext) {
@@ -229,7 +230,7 @@
      * Updates the regions for detecting the swipe up/quickswitch and assistant gestures.
      */
     public void updateGestureTouchRegions() {
-        if (!mMode.hasGestures) {
+        if (!hasGestures(mMode)) {
             return;
         }
 
@@ -268,7 +269,7 @@
                 | CHANGE_SUPPORTED_BOUNDS)) != 0) {
             mDisplayRotation = info.rotation;
 
-            if (mMode.hasGestures) {
+            if (hasGestures(mMode)) {
                 updateGestureTouchRegions();
                 mOrientationTouchTransformer.createOrAddTouchRegion(info);
                 mCurrentAppRotation = mDisplayRotation;
@@ -295,9 +296,9 @@
             mOrientationTouchTransformer.setNavigationMode(newMode, mDisplayController.getInfo(),
                     mContext.getResources());
 
-            if (forceRegister || (!mMode.hasGestures && newMode.hasGestures)) {
+            if (forceRegister || (!hasGestures(mMode) && hasGestures(newMode))) {
                 setupOrientationSwipeHandler();
-            } else if (mMode.hasGestures && !newMode.hasGestures) {
+            } else if (hasGestures(mMode) && !hasGestures(newMode)) {
                 destroyOrientationSwipeHandlerCallback();
             }
 
@@ -399,7 +400,7 @@
     }
 
     public int getCurrentActiveRotation() {
-        if (!mMode.hasGestures) {
+        if (!hasGestures(mMode)) {
             // touch rotation should always match that of display for 3 button
             return mDisplayRotation;
         }
@@ -416,4 +417,8 @@
     public OrientationTouchTransformer getOrientationTouchTransformer() {
         return mOrientationTouchTransformer;
     }
+
+    private boolean hasGestures(NavigationMode mode) {
+        return mode.hasGestures || (mode == THREE_BUTTONS && Flags.threeButtonCornerSwipe());
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/TouchInteractionService.java b/quickstep/src/com/android/quickstep/TouchInteractionService.java
index 2b5aa71..b321b3e 100644
--- a/quickstep/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/src/com/android/quickstep/TouchInteractionService.java
@@ -89,6 +89,7 @@
 import com.android.launcher3.BaseDraggingActivity;
 import com.android.launcher3.ConstantItem;
 import com.android.launcher3.EncryptionType;
+import com.android.launcher3.Flags;
 import com.android.launcher3.LauncherPrefs;
 import com.android.launcher3.anim.AnimatedFloat;
 import com.android.launcher3.config.FeatureFlags;
@@ -674,8 +675,9 @@
     private void initInputMonitor(String reason) {
         disposeEventHandlers("Initializing input monitor due to: " + reason);
 
-        if (mDeviceState.isButtonNavMode() && (!ENABLE_TRACKPAD_GESTURE.get()
-                || mTrackpadsConnected.isEmpty())) {
+        if (mDeviceState.isButtonNavMode()
+                && !mDeviceState.supportsAssistantGestureInButtonNav()
+                && (!ENABLE_TRACKPAD_GESTURE.get() || mTrackpadsConnected.isEmpty())) {
             return;
         }
 
@@ -857,7 +859,9 @@
                             .append("); cancelling gesture."),
                     NAVIGATION_MODE_SWITCHED);
             event.setAction(ACTION_CANCEL);
-        } else if (mDeviceState.isButtonNavMode() && !isTrackpadMotionEvent(event)) {
+        } else if (mDeviceState.isButtonNavMode()
+                && !mDeviceState.supportsAssistantGestureInButtonNav()
+                && !isTrackpadMotionEvent(event)) {
             ActiveGestureLog.INSTANCE.addLog(new CompoundString("TIS.onInputEvent: ")
                     .append("Cannot process input event: ")
                     .append("using 3-button nav and event is not a trackpad event"));
@@ -909,7 +913,22 @@
             if (isInSwipeUpTouchRegion && tac != null) {
                 tac.closeKeyboardQuickSwitchView();
             }
-            if ((!isOneHandedModeActive && isInSwipeUpTouchRegion)
+            if (mDeviceState.isButtonNavMode()
+                    && mDeviceState.supportsAssistantGestureInButtonNav()) {
+                reasonString.append("in three button mode which supports Assistant gesture");
+                // Consume gesture event for Assistant (all other gestures should do nothing).
+                if (mDeviceState.canTriggerAssistantAction(event)) {
+                    reasonString.append(" and event can trigger assistant action")
+                            .append(", consuming gesture for assistant action");
+                    mGestureState =
+                            createGestureState(mGestureState, getTrackpadGestureType(event));
+                    mUncheckedConsumer = tryCreateAssistantInputConsumer(mGestureState, event);
+                } else {
+                    reasonString.append(" but event cannot trigger Assistant")
+                            .append(", consuming gesture as no-op");
+                    mUncheckedConsumer = InputConsumer.NO_OP;
+                }
+            } else if ((!isOneHandedModeActive && isInSwipeUpTouchRegion)
                     || isHoverActionWithoutConsumer || isOnBubbles) {
                 reasonString.append(!isOneHandedModeActive && isInSwipeUpTouchRegion
                                 ? "one handed mode is not active and event is in swipe up region"
@@ -931,8 +950,7 @@
                                 : "event is a trackpad multi-finger swipe")
                         .append(" and event can trigger assistant action")
                         .append(", consuming gesture for assistant action");
-                mGestureState = createGestureState(mGestureState,
-                        getTrackpadGestureType(event));
+                mGestureState = createGestureState(mGestureState, getTrackpadGestureType(event));
                 // Do not change mConsumer as if there is an ongoing QuickSwitch gesture, we
                 // should not interrupt it. QuickSwitch assumes that interruption can only
                 // happen if the next gesture is also quick switch.
@@ -1193,7 +1211,8 @@
             NavHandle navHandle = tac != null ? tac.getNavHandle()
                     : SystemUiProxy.INSTANCE.get(this);
             if (canStartSystemGesture && !previousGestureState.isRecentsAnimationRunning()
-                    && navHandle.canNavHandleBeLongPressed()) {
+                    && navHandle.canNavHandleBeLongPressed()
+                    && !ignoreThreeFingerTrackpadForNavHandleLongPress(mGestureState)) {
                 reasonString.append(NEWLINE_PREFIX)
                         .append(reasonPrefix)
                         .append(SUBSTRING_PREFIX)
@@ -1289,6 +1308,11 @@
         return new CompoundString(NEWLINE_PREFIX).append(substring);
     }
 
+    private boolean ignoreThreeFingerTrackpadForNavHandleLongPress(GestureState gestureState) {
+        return Flags.ignoreThreeFingerTrackpadForNavHandleLongPress()
+                && gestureState.isThreeFingerTrackpadGesture();
+    }
+
     private void logInputConsumerSelectionReason(
             InputConsumer consumer, CompoundString reasonString) {
         ActiveGestureLog.INSTANCE.addLog(new CompoundString("setInputConsumer: ")
diff --git a/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.java b/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.java
deleted file mode 100644
index 8b87718..0000000
--- a/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.java
+++ /dev/null
@@ -1,444 +0,0 @@
-/*
- * Copyright (C) 2018 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.views;
-
-import static android.provider.Settings.ACTION_APP_USAGE_SETTINGS;
-
-import static com.android.launcher3.Utilities.prefixTextWithIcon;
-import static com.android.launcher3.util.Executors.ORDERED_BG_EXECUTOR;
-
-import android.app.ActivityOptions;
-import android.content.ActivityNotFoundException;
-import android.content.Context;
-import android.content.Intent;
-import android.content.pm.LauncherApps;
-import android.content.pm.LauncherApps.AppUsageLimit;
-import android.graphics.Outline;
-import android.graphics.Paint;
-import android.icu.text.MeasureFormat;
-import android.icu.text.MeasureFormat.FormatWidth;
-import android.icu.util.Measure;
-import android.icu.util.MeasureUnit;
-import android.os.UserHandle;
-import android.util.Log;
-import android.util.Pair;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.ViewOutlineProvider;
-import android.view.accessibility.AccessibilityNodeInfo;
-import android.widget.FrameLayout;
-import android.widget.TextView;
-
-import androidx.annotation.IntDef;
-import androidx.annotation.Nullable;
-import androidx.annotation.StringRes;
-
-import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.R;
-import com.android.launcher3.Utilities;
-import com.android.launcher3.util.SplitConfigurationOptions.SplitBounds;
-import com.android.quickstep.TaskUtils;
-import com.android.quickstep.orientation.RecentsPagedOrientationHandler;
-import com.android.systemui.shared.recents.model.Task;
-
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.time.Duration;
-import java.util.Locale;
-
-public final class DigitalWellBeingToast {
-
-    private static final float THRESHOLD_LEFT_ICON_ONLY = 0.4f;
-    private static final float THRESHOLD_RIGHT_ICON_ONLY = 0.6f;
-
-    /** Will span entire width of taskView with full text */
-    private static final int SPLIT_BANNER_FULLSCREEN = 0;
-    /** Used for grid task view, only showing icon and time */
-    private static final int SPLIT_GRID_BANNER_LARGE = 1;
-    /** Used for grid task view, only showing icon */
-    private static final int SPLIT_GRID_BANNER_SMALL = 2;
-
-    @IntDef(value = {
-            SPLIT_BANNER_FULLSCREEN,
-            SPLIT_GRID_BANNER_LARGE,
-            SPLIT_GRID_BANNER_SMALL,
-    })
-    @Retention(RetentionPolicy.SOURCE)
-    @interface SplitBannerConfig {
-    }
-
-    static final Intent OPEN_APP_USAGE_SETTINGS_TEMPLATE = new Intent(ACTION_APP_USAGE_SETTINGS);
-    static final int MINUTE_MS = 60000;
-
-    private static final String TAG = "DigitalWellBeingToast";
-
-    private final RecentsViewContainer mContainer;
-    private final TaskView mTaskView;
-    private final LauncherApps mLauncherApps;
-
-    private final int mBannerHeight;
-
-    private Task mTask;
-    private boolean mHasLimit;
-
-    private long mAppRemainingTimeMs;
-    @Nullable
-    private View mBanner;
-    private ViewOutlineProvider mOldBannerOutlineProvider;
-    private float mBannerOffsetPercentage;
-    @Nullable
-    private SplitBounds mSplitBounds;
-    private float mSplitOffsetTranslationY;
-    private float mSplitOffsetTranslationX;
-
-    private boolean mIsDestroyed = false;
-
-    public DigitalWellBeingToast(RecentsViewContainer container, TaskView taskView) {
-        mContainer = container;
-        mTaskView = taskView;
-        mLauncherApps = container.asContext().getSystemService(LauncherApps.class);
-        mBannerHeight = container.asContext().getResources().getDimensionPixelSize(
-                R.dimen.digital_wellbeing_toast_height);
-    }
-
-    private void setNoLimit() {
-        mHasLimit = false;
-        mTaskView.setContentDescription(mTask.titleDescription);
-        replaceBanner(null);
-        mAppRemainingTimeMs = -1;
-    }
-
-    private void setLimit(long appUsageLimitTimeMs, long appRemainingTimeMs) {
-        mAppRemainingTimeMs = appRemainingTimeMs;
-        mHasLimit = true;
-        TextView toast = mContainer.getViewCache().getView(R.layout.digital_wellbeing_toast,
-                mContainer.asContext(), mTaskView);
-        toast.setText(prefixTextWithIcon(mContainer.asContext(), R.drawable.ic_hourglass_top,
-                getText()));
-        toast.setOnClickListener(this::openAppUsageSettings);
-        replaceBanner(toast);
-
-        mTaskView.setContentDescription(
-                getContentDescriptionForTask(mTask, appUsageLimitTimeMs, appRemainingTimeMs));
-    }
-
-    public String getText() {
-        return getText(mAppRemainingTimeMs, false /* forContentDesc */);
-    }
-
-    public boolean hasLimit() {
-        return mHasLimit;
-    }
-
-    public void initialize(Task task) {
-        if (mIsDestroyed) {
-            throw new IllegalStateException("Cannot re-initialize a destroyed toast");
-        }
-        mTask = task;
-        ORDERED_BG_EXECUTOR.execute(() -> {
-            AppUsageLimit usageLimit = null;
-            try {
-                usageLimit = mLauncherApps.getAppUsageLimit(
-                        mTask.getTopComponent().getPackageName(),
-                        UserHandle.of(mTask.key.userId));
-            } catch (Exception e) {
-                Log.e(TAG, "Error initializing digital well being toast", e);
-            }
-            final long appUsageLimitTimeMs =
-                    usageLimit != null ? usageLimit.getTotalUsageLimit() : -1;
-            final long appRemainingTimeMs =
-                    usageLimit != null ? usageLimit.getUsageRemaining() : -1;
-
-            mTaskView.post(() -> {
-                if (mIsDestroyed) {
-                    return;
-                }
-                if (appUsageLimitTimeMs < 0 || appRemainingTimeMs < 0) {
-                    setNoLimit();
-                } else {
-                    setLimit(appUsageLimitTimeMs, appRemainingTimeMs);
-                }
-            });
-        });
-    }
-
-    /**
-     * Mark the DWB toast as destroyed and remove banner from TaskView.
-     */
-    public void destroy() {
-        mIsDestroyed = true;
-        mTaskView.post(() -> replaceBanner(null));
-    }
-
-    public void setSplitBounds(@Nullable SplitBounds splitBounds) {
-        mSplitBounds = splitBounds;
-    }
-
-    private @SplitBannerConfig int getSplitBannerConfig() {
-        if (mSplitBounds == null
-                || !mContainer.getDeviceProfile().isTablet
-                || mTaskView.isLargeTile()) {
-            return SPLIT_BANNER_FULLSCREEN;
-        }
-
-        // For portrait grid only height of task changes, not width. So we keep the text the same
-        if (!mContainer.getDeviceProfile().isLeftRightSplit) {
-            return SPLIT_GRID_BANNER_LARGE;
-        }
-
-        // For landscape grid, for 30% width we only show icon, otherwise show icon and time
-        if (mTask.key.id == mSplitBounds.leftTopTaskId) {
-            return mSplitBounds.leftTaskPercent < THRESHOLD_LEFT_ICON_ONLY
-                    ? SPLIT_GRID_BANNER_SMALL : SPLIT_GRID_BANNER_LARGE;
-        } else {
-            return mSplitBounds.leftTaskPercent > THRESHOLD_RIGHT_ICON_ONLY
-                    ? SPLIT_GRID_BANNER_SMALL : SPLIT_GRID_BANNER_LARGE;
-        }
-    }
-
-    private String getReadableDuration(
-            Duration duration,
-            @StringRes int durationLessThanOneMinuteStringId) {
-        int hours = Math.toIntExact(duration.toHours());
-        int minutes = Math.toIntExact(duration.minusHours(hours).toMinutes());
-
-        // Apply FormatWidth.WIDE if both the hour part and the minute part are non-zero.
-        if (hours > 0 && minutes > 0) {
-            return MeasureFormat.getInstance(Locale.getDefault(), FormatWidth.NARROW)
-                    .formatMeasures(
-                            new Measure(hours, MeasureUnit.HOUR),
-                            new Measure(minutes, MeasureUnit.MINUTE));
-        }
-
-        // Apply FormatWidth.WIDE if only the hour part is non-zero (unless forced).
-        if (hours > 0) {
-            return MeasureFormat.getInstance(Locale.getDefault(), FormatWidth.WIDE).formatMeasures(
-                    new Measure(hours, MeasureUnit.HOUR));
-        }
-
-        // Apply FormatWidth.WIDE if only the minute part is non-zero (unless forced).
-        if (minutes > 0) {
-            return MeasureFormat.getInstance(Locale.getDefault(), FormatWidth.WIDE).formatMeasures(
-                    new Measure(minutes, MeasureUnit.MINUTE));
-        }
-
-        // Use a specific string for usage less than one minute but non-zero.
-        if (duration.compareTo(Duration.ZERO) > 0) {
-            return mContainer.asContext().getString(durationLessThanOneMinuteStringId);
-        }
-
-        // Otherwise, return 0-minute string.
-        return MeasureFormat.getInstance(Locale.getDefault(), FormatWidth.WIDE).formatMeasures(
-                new Measure(0, MeasureUnit.MINUTE));
-    }
-
-    /**
-     * Returns text to show for the banner depending on {@link #getSplitBannerConfig()}
-     * If {@param forContentDesc} is {@code true}, this will always return the full
-     * string corresponding to {@link #SPLIT_BANNER_FULLSCREEN}
-     */
-    private String getText(long remainingTime, boolean forContentDesc) {
-        final Duration duration = Duration.ofMillis(
-                remainingTime > MINUTE_MS ?
-                        (remainingTime + MINUTE_MS - 1) / MINUTE_MS * MINUTE_MS :
-                        remainingTime);
-        String readableDuration = getReadableDuration(duration,
-                R.string.shorter_duration_less_than_one_minute
-                /* forceFormatWidth */);
-        @SplitBannerConfig int splitBannerConfig = getSplitBannerConfig();
-        if (forContentDesc || splitBannerConfig == SPLIT_BANNER_FULLSCREEN) {
-            return mContainer.asContext().getString(
-                    R.string.time_left_for_app,
-                    readableDuration);
-        }
-
-        if (splitBannerConfig == SPLIT_GRID_BANNER_SMALL) {
-            // show no text
-            return "";
-        } else { // SPLIT_GRID_BANNER_LARGE
-            // only show time
-            return readableDuration;
-        }
-    }
-
-    public void openAppUsageSettings(View view) {
-        final Intent intent = new Intent(OPEN_APP_USAGE_SETTINGS_TEMPLATE)
-                .putExtra(Intent.EXTRA_PACKAGE_NAME,
-                        mTask.getTopComponent().getPackageName()).addFlags(
-                        Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
-        try {
-            final RecentsViewContainer container =
-                    RecentsViewContainer.containerFromContext(view.getContext());
-            final ActivityOptions options = ActivityOptions.makeScaleUpAnimation(
-                    view, 0, 0,
-                    view.getWidth(), view.getHeight());
-            container.asContext().startActivity(intent, options.toBundle());
-
-            // TODO: add WW logging on the app usage settings click.
-        } catch (ActivityNotFoundException e) {
-            Log.e(TAG, "Failed to open app usage settings for task "
-                    + mTask.getTopComponent().getPackageName(), e);
-        }
-    }
-
-    private String getContentDescriptionForTask(
-            Task task, long appUsageLimitTimeMs, long appRemainingTimeMs) {
-        return appUsageLimitTimeMs >= 0 && appRemainingTimeMs >= 0 ?
-                mContainer.asContext().getString(
-                        R.string.task_contents_description_with_remaining_time,
-                        task.titleDescription,
-                        getText(appRemainingTimeMs, true /* forContentDesc */)) :
-                task.titleDescription;
-    }
-
-    private void replaceBanner(@Nullable View view) {
-        resetOldBanner();
-        setBanner(view);
-    }
-
-    private void resetOldBanner() {
-        if (mBanner != null) {
-            mBanner.setOutlineProvider(mOldBannerOutlineProvider);
-            mTaskView.removeView(mBanner);
-            mBanner.setOnClickListener(null);
-            mContainer.getViewCache().recycleView(R.layout.digital_wellbeing_toast, mBanner);
-        }
-    }
-
-    private void setBanner(@Nullable View view) {
-        mBanner = view;
-        if (mBanner != null && mTaskView.getRecentsView() != null) {
-            setupAndAddBanner();
-            setBannerOutline();
-        }
-    }
-
-    private void setupAndAddBanner() {
-        FrameLayout.LayoutParams layoutParams =
-                (FrameLayout.LayoutParams) mBanner.getLayoutParams();
-        DeviceProfile deviceProfile = mContainer.getDeviceProfile();
-        layoutParams.bottomMargin = ((ViewGroup.MarginLayoutParams)
-                mTaskView.getFirstSnapshotView().getLayoutParams()).bottomMargin;
-        RecentsPagedOrientationHandler orientationHandler = mTaskView.getPagedOrientationHandler();
-        Pair<Float, Float> translations = orientationHandler
-                .getDwbLayoutTranslations(mTaskView.getMeasuredWidth(),
-                        mTaskView.getMeasuredHeight(), mSplitBounds, deviceProfile,
-                        mTaskView.getSnapshotViews(), mTask.key.id, mBanner);
-        mSplitOffsetTranslationX = translations.first;
-        mSplitOffsetTranslationY = translations.second;
-        updateTranslationY();
-        updateTranslationX();
-        mTaskView.addView(mBanner);
-    }
-
-    private void setBannerOutline() {
-        // TODO(b\273367585) to investigate why mBanner.getOutlineProvider() can be null
-        mOldBannerOutlineProvider = mBanner.getOutlineProvider() != null
-                ? mBanner.getOutlineProvider()
-                : ViewOutlineProvider.BACKGROUND;
-
-        mBanner.setOutlineProvider(new ViewOutlineProvider() {
-            @Override
-            public void getOutline(View view, Outline outline) {
-                mOldBannerOutlineProvider.getOutline(view, outline);
-                float verticalTranslation = -view.getTranslationY() + mSplitOffsetTranslationY;
-                outline.offset(0, Math.round(verticalTranslation));
-            }
-        });
-        mBanner.setClipToOutline(true);
-    }
-
-    void updateBannerOffset(float offsetPercentage) {
-        if (mBannerOffsetPercentage != offsetPercentage) {
-            mBannerOffsetPercentage = offsetPercentage;
-            if (mBanner != null) {
-                updateTranslationY();
-                mBanner.invalidateOutline();
-            }
-        }
-    }
-
-    private void updateTranslationY() {
-        if (mBanner == null) {
-            return;
-        }
-
-        mBanner.setTranslationY(
-                (mBannerOffsetPercentage * mBannerHeight) + mSplitOffsetTranslationY);
-    }
-
-    private void updateTranslationX() {
-        if (mBanner == null) {
-            return;
-        }
-
-        mBanner.setTranslationX(mSplitOffsetTranslationX);
-    }
-
-    void setBannerColorTint(int color, float amount) {
-        if (mBanner == null) {
-            return;
-        }
-        if (amount == 0) {
-            mBanner.setLayerType(View.LAYER_TYPE_NONE, null);
-        }
-        Paint layerPaint = new Paint();
-        layerPaint.setColorFilter(Utilities.makeColorTintingColorFilter(color, amount));
-        mBanner.setLayerType(View.LAYER_TYPE_HARDWARE, layerPaint);
-        mBanner.setLayerPaint(layerPaint);
-    }
-
-    void setBannerVisibility(int visibility) {
-        if (mBanner == null) {
-            return;
-        }
-
-        mBanner.setVisibility(visibility);
-    }
-
-    private int getAccessibilityActionId() {
-        return (mSplitBounds != null
-                && mSplitBounds.rightBottomTaskId == mTask.key.id)
-                ? R.id.action_digital_wellbeing_bottom_right
-                : R.id.action_digital_wellbeing_top_left;
-    }
-
-    @Nullable
-    public AccessibilityNodeInfo.AccessibilityAction getDWBAccessibilityAction() {
-        if (!hasLimit()) {
-            return null;
-        }
-
-        Context context = mContainer.asContext();
-        String label =
-                (mTaskView.containsMultipleTasks())
-                        ? context.getString(
-                        R.string.split_app_usage_settings,
-                        TaskUtils.getTitle(context, mTask)
-                ) : context.getString(R.string.accessibility_app_usage_settings);
-        return new AccessibilityNodeInfo.AccessibilityAction(getAccessibilityActionId(), label);
-    }
-
-    public boolean handleAccessibilityAction(int action) {
-        if (getAccessibilityActionId() == action) {
-            openAppUsageSettings(mTaskView);
-            return true;
-        } else {
-            return false;
-        }
-    }
-}
diff --git a/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.kt b/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.kt
new file mode 100644
index 0000000..731b008
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.kt
@@ -0,0 +1,399 @@
+/*
+ * Copyright (C) 2018 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.views
+
+import android.app.ActivityOptions
+import android.content.ActivityNotFoundException
+import android.content.Intent
+import android.content.pm.LauncherApps
+import android.content.pm.LauncherApps.AppUsageLimit
+import android.graphics.Outline
+import android.graphics.Paint
+import android.icu.text.MeasureFormat
+import android.icu.util.Measure
+import android.icu.util.MeasureUnit
+import android.os.UserHandle
+import android.provider.Settings
+import android.util.Log
+import android.view.View
+import android.view.ViewGroup.MarginLayoutParams
+import android.view.ViewOutlineProvider
+import android.view.accessibility.AccessibilityNodeInfo
+import android.widget.FrameLayout
+import android.widget.TextView
+import androidx.annotation.StringRes
+import androidx.core.util.component1
+import androidx.core.util.component2
+import androidx.core.view.updateLayoutParams
+import com.android.launcher3.R
+import com.android.launcher3.Utilities
+import com.android.launcher3.util.Executors
+import com.android.launcher3.util.SplitConfigurationOptions
+import com.android.quickstep.TaskUtils
+import com.android.systemui.shared.recents.model.Task
+import java.time.Duration
+import java.util.Locale
+
+class DigitalWellBeingToast(
+    private val container: RecentsViewContainer,
+    private val taskView: TaskView
+) {
+    private val launcherApps: LauncherApps? =
+        container.asContext().getSystemService(LauncherApps::class.java)
+
+    private val bannerHeight =
+        container
+            .asContext()
+            .resources
+            .getDimensionPixelSize(R.dimen.digital_wellbeing_toast_height)
+
+    private lateinit var task: Task
+
+    private var appRemainingTimeMs: Long = 0
+    private var banner: View? = null
+    private var oldBannerOutlineProvider: ViewOutlineProvider? = null
+    private var splitOffsetTranslationY = 0f
+    private var splitOffsetTranslationX = 0f
+
+    private var isDestroyed = false
+
+    var hasLimit = false
+    var splitBounds: SplitConfigurationOptions.SplitBounds? = null
+    var bannerOffsetPercentage = 0f
+        set(value) {
+            if (field != value) {
+                field = value
+                banner?.let {
+                    updateTranslationY()
+                    it.invalidateOutline()
+                }
+            }
+        }
+
+    private fun setNoLimit() {
+        hasLimit = false
+        taskView.contentDescription = task.titleDescription
+        replaceBanner(null)
+        appRemainingTimeMs = -1
+    }
+
+    private fun setLimit(appUsageLimitTimeMs: Long, appRemainingTimeMs: Long) {
+        this.appRemainingTimeMs = appRemainingTimeMs
+        hasLimit = true
+        val toast =
+            container.viewCache
+                .getView<TextView>(
+                    R.layout.digital_wellbeing_toast,
+                    container.asContext(),
+                    taskView
+                )
+                .apply {
+                    text =
+                        Utilities.prefixTextWithIcon(
+                            container.asContext(),
+                            R.drawable.ic_hourglass_top,
+                            getBannerText()
+                        )
+                    setOnClickListener(::openAppUsageSettings)
+                }
+        replaceBanner(toast)
+
+        taskView.contentDescription =
+            getContentDescriptionForTask(task, appUsageLimitTimeMs, appRemainingTimeMs)
+    }
+
+    fun initialize(task: Task) {
+        check(!isDestroyed) { "Cannot re-initialize a destroyed toast" }
+        this.task = task
+        Executors.ORDERED_BG_EXECUTOR.execute {
+            var usageLimit: AppUsageLimit? = null
+            try {
+                usageLimit =
+                    launcherApps?.getAppUsageLimit(
+                        this.task.topComponent.packageName,
+                        UserHandle.of(this.task.key.userId)
+                    )
+            } catch (e: Exception) {
+                Log.e(TAG, "Error initializing digital well being toast", e)
+            }
+            val appUsageLimitTimeMs = usageLimit?.totalUsageLimit ?: -1
+            val appRemainingTimeMs = usageLimit?.usageRemaining ?: -1
+            taskView.post {
+                if (isDestroyed) {
+                    return@post
+                }
+                if (appUsageLimitTimeMs < 0 || appRemainingTimeMs < 0) {
+                    setNoLimit()
+                } else {
+                    setLimit(appUsageLimitTimeMs, appRemainingTimeMs)
+                }
+            }
+        }
+    }
+
+    /** Mark the DWB toast as destroyed and remove banner from TaskView. */
+    fun destroy() {
+        isDestroyed = true
+        taskView.post { replaceBanner(null) }
+    }
+
+    private fun getSplitBannerConfig(): SplitBannerConfig {
+        val splitBounds = splitBounds
+        return when {
+            splitBounds == null || !container.deviceProfile.isTablet || taskView.isLargeTile ->
+                SplitBannerConfig.SPLIT_BANNER_FULLSCREEN
+            // For portrait grid only height of task changes, not width. So we keep the text the
+            // same
+            !container.deviceProfile.isLeftRightSplit -> SplitBannerConfig.SPLIT_GRID_BANNER_LARGE
+            // For landscape grid, for 30% width we only show icon, otherwise show icon and time
+            task.key.id == splitBounds.leftTopTaskId ->
+                if (splitBounds.leftTaskPercent < THRESHOLD_LEFT_ICON_ONLY)
+                    SplitBannerConfig.SPLIT_GRID_BANNER_SMALL
+                else SplitBannerConfig.SPLIT_GRID_BANNER_LARGE
+            else ->
+                if (splitBounds.leftTaskPercent > THRESHOLD_RIGHT_ICON_ONLY)
+                    SplitBannerConfig.SPLIT_GRID_BANNER_SMALL
+                else SplitBannerConfig.SPLIT_GRID_BANNER_LARGE
+        }
+    }
+
+    private fun getReadableDuration(
+        duration: Duration,
+        @StringRes durationLessThanOneMinuteStringId: Int
+    ): String {
+        val hours = Math.toIntExact(duration.toHours())
+        val minutes = Math.toIntExact(duration.minusHours(hours.toLong()).toMinutes())
+        return when {
+            // Apply FormatWidth.WIDE if both the hour part and the minute part are non-zero.
+            hours > 0 && minutes > 0 ->
+                MeasureFormat.getInstance(Locale.getDefault(), MeasureFormat.FormatWidth.NARROW)
+                    .formatMeasures(
+                        Measure(hours, MeasureUnit.HOUR),
+                        Measure(minutes, MeasureUnit.MINUTE)
+                    )
+            // Apply FormatWidth.WIDE if only the hour part is non-zero (unless forced).
+            hours > 0 ->
+                MeasureFormat.getInstance(Locale.getDefault(), MeasureFormat.FormatWidth.WIDE)
+                    .formatMeasures(Measure(hours, MeasureUnit.HOUR))
+            // Apply FormatWidth.WIDE if only the minute part is non-zero (unless forced).
+            minutes > 0 ->
+                MeasureFormat.getInstance(Locale.getDefault(), MeasureFormat.FormatWidth.WIDE)
+                    .formatMeasures(Measure(minutes, MeasureUnit.MINUTE))
+            // Use a specific string for usage less than one minute but non-zero.
+            duration > Duration.ZERO ->
+                container.asContext().getString(durationLessThanOneMinuteStringId)
+            // Otherwise, return 0-minute string.
+            else ->
+                MeasureFormat.getInstance(Locale.getDefault(), MeasureFormat.FormatWidth.WIDE)
+                    .formatMeasures(Measure(0, MeasureUnit.MINUTE))
+        }
+    }
+
+    /**
+     * Returns text to show for the banner depending on [.getSplitBannerConfig] If {@param
+     * forContentDesc} is `true`, this will always return the full string corresponding to
+     * [.SPLIT_BANNER_FULLSCREEN]
+     */
+    @JvmOverloads
+    fun getBannerText(
+        remainingTime: Long = appRemainingTimeMs,
+        forContentDesc: Boolean = false
+    ): String {
+        val duration =
+            Duration.ofMillis(
+                if (remainingTime > MINUTE_MS)
+                    (remainingTime + MINUTE_MS - 1) / MINUTE_MS * MINUTE_MS
+                else remainingTime
+            )
+        val readableDuration =
+            getReadableDuration(
+                duration,
+                R.string.shorter_duration_less_than_one_minute /* forceFormatWidth */
+            )
+        val splitBannerConfig = getSplitBannerConfig()
+        return when {
+            forContentDesc || splitBannerConfig == SplitBannerConfig.SPLIT_BANNER_FULLSCREEN ->
+                container.asContext().getString(R.string.time_left_for_app, readableDuration)
+            // show no text
+            splitBannerConfig == SplitBannerConfig.SPLIT_GRID_BANNER_SMALL -> ""
+            // SPLIT_GRID_BANNER_LARGE only show time
+            else -> readableDuration
+        }
+    }
+
+    private fun openAppUsageSettings(view: View) {
+        val intent =
+            Intent(OPEN_APP_USAGE_SETTINGS_TEMPLATE)
+                .putExtra(Intent.EXTRA_PACKAGE_NAME, task.topComponent.packageName)
+                .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
+        try {
+            val options = ActivityOptions.makeScaleUpAnimation(view, 0, 0, view.width, view.height)
+            container.asContext().startActivity(intent, options.toBundle())
+
+            // TODO: add WW logging on the app usage settings click.
+        } catch (e: ActivityNotFoundException) {
+            Log.e(
+                TAG,
+                "Failed to open app usage settings for task " + task.topComponent.packageName,
+                e
+            )
+        }
+    }
+
+    private fun getContentDescriptionForTask(
+        task: Task,
+        appUsageLimitTimeMs: Long,
+        appRemainingTimeMs: Long
+    ): String =
+        if (appUsageLimitTimeMs >= 0 && appRemainingTimeMs >= 0)
+            container
+                .asContext()
+                .getString(
+                    R.string.task_contents_description_with_remaining_time,
+                    task.titleDescription,
+                    getBannerText(appRemainingTimeMs, true /* forContentDesc */)
+                )
+        else task.titleDescription
+
+    private fun replaceBanner(view: View?) {
+        resetOldBanner()
+        setBanner(view)
+    }
+
+    private fun resetOldBanner() {
+        val banner = banner ?: return
+        banner.outlineProvider = oldBannerOutlineProvider
+        taskView.removeView(banner)
+        banner.setOnClickListener(null)
+        container.viewCache.recycleView(R.layout.digital_wellbeing_toast, banner)
+    }
+
+    private fun setBanner(banner: View?) {
+        this.banner = banner
+        if (banner != null && taskView.recentsView != null) {
+            setupAndAddBanner()
+            setBannerOutline()
+        }
+    }
+
+    private fun setupAndAddBanner() {
+        val banner = banner ?: return
+        banner.updateLayoutParams<FrameLayout.LayoutParams> {
+            bottomMargin =
+                (taskView.firstSnapshotView.layoutParams as MarginLayoutParams).bottomMargin
+        }
+        val (translationX, translationY) =
+            taskView.pagedOrientationHandler.getDwbLayoutTranslations(
+                taskView.measuredWidth,
+                taskView.measuredHeight,
+                splitBounds,
+                container.deviceProfile,
+                taskView.snapshotViews,
+                task.key.id,
+                banner
+            )
+        splitOffsetTranslationX = translationX
+        splitOffsetTranslationY = translationY
+        updateTranslationY()
+        updateTranslationX()
+        taskView.addView(banner)
+    }
+
+    private fun setBannerOutline() {
+        val banner = banner ?: return
+        // TODO(b\273367585) to investigate why mBanner.getOutlineProvider() can be null
+        val oldBannerOutlineProvider =
+            if (banner.outlineProvider != null) banner.outlineProvider
+            else ViewOutlineProvider.BACKGROUND
+        this.oldBannerOutlineProvider = oldBannerOutlineProvider
+
+        banner.outlineProvider =
+            object : ViewOutlineProvider() {
+                override fun getOutline(view: View, outline: Outline) {
+                    oldBannerOutlineProvider.getOutline(view, outline)
+                    val verticalTranslation = -view.translationY + splitOffsetTranslationY
+                    outline.offset(0, Math.round(verticalTranslation))
+                }
+            }
+        banner.clipToOutline = true
+    }
+
+    private fun updateTranslationY() {
+        banner?.translationY = bannerOffsetPercentage * bannerHeight + splitOffsetTranslationY
+    }
+
+    private fun updateTranslationX() {
+        banner?.translationX = splitOffsetTranslationX
+    }
+
+    fun setBannerColorTint(color: Int, amount: Float) {
+        val banner = banner ?: return
+        if (amount == 0f) {
+            banner.setLayerType(View.LAYER_TYPE_NONE, null)
+        }
+        val layerPaint = Paint()
+        layerPaint.setColorFilter(Utilities.makeColorTintingColorFilter(color, amount))
+        banner.setLayerType(View.LAYER_TYPE_HARDWARE, layerPaint)
+        banner.setLayerPaint(layerPaint)
+    }
+
+    fun setBannerVisibility(visibility: Int) {
+        banner?.visibility = visibility
+    }
+
+    private fun getAccessibilityActionId(): Int =
+        if (splitBounds?.rightBottomTaskId == task.key.id)
+            R.id.action_digital_wellbeing_bottom_right
+        else R.id.action_digital_wellbeing_top_left
+
+    fun getDWBAccessibilityAction(): AccessibilityNodeInfo.AccessibilityAction? {
+        if (!hasLimit) return null
+        val context = container.asContext()
+        val label =
+            if ((taskView.containsMultipleTasks()))
+                context.getString(
+                    R.string.split_app_usage_settings,
+                    TaskUtils.getTitle(context, task)
+                )
+            else context.getString(R.string.accessibility_app_usage_settings)
+        return AccessibilityNodeInfo.AccessibilityAction(getAccessibilityActionId(), label)
+    }
+
+    fun handleAccessibilityAction(action: Int): Boolean {
+        if (getAccessibilityActionId() != action) return false
+        openAppUsageSettings(taskView)
+        return true
+    }
+
+    companion object {
+        private const val THRESHOLD_LEFT_ICON_ONLY = 0.4f
+        private const val THRESHOLD_RIGHT_ICON_ONLY = 0.6f
+
+        enum class SplitBannerConfig {
+            /** Will span entire width of taskView with full text */
+            SPLIT_BANNER_FULLSCREEN,
+            /** Used for grid task view, only showing icon and time */
+            SPLIT_GRID_BANNER_LARGE,
+            /** Used for grid task view, only showing icon */
+            SPLIT_GRID_BANNER_SMALL
+        }
+
+        val OPEN_APP_USAGE_SETTINGS_TEMPLATE: Intent = Intent(Settings.ACTION_APP_USAGE_SETTINGS)
+        const val MINUTE_MS: Int = 60000
+
+        private const val TAG = "DigitalWellBeingToast"
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/views/GroupedTaskView.kt b/quickstep/src/com/android/quickstep/views/GroupedTaskView.kt
index a046c42..ba4d786 100644
--- a/quickstep/src/com/android/quickstep/views/GroupedTaskView.kt
+++ b/quickstep/src/com/android/quickstep/views/GroupedTaskView.kt
@@ -130,7 +130,7 @@
         taskContainers.forEach { it.bind() }
 
         this.splitBoundsConfig = splitBoundsConfig
-        taskContainers.forEach { it.digitalWellBeingToast?.setSplitBounds(splitBoundsConfig) }
+        taskContainers.forEach { it.digitalWellBeingToast?.splitBounds = splitBoundsConfig }
         setOrientationState(orientedState)
     }
 
@@ -210,7 +210,7 @@
     fun updateSplitBoundsConfig(splitBounds: SplitConfigurationOptions.SplitBounds?) {
         splitBoundsConfig = splitBounds
         taskContainers.forEach {
-            it.digitalWellBeingToast?.setSplitBounds(splitBoundsConfig)
+            it.digitalWellBeingToast?.splitBounds = splitBoundsConfig
             it.digitalWellBeingToast?.initialize(it.task)
         }
         invalidate()
diff --git a/quickstep/src/com/android/quickstep/views/TaskView.kt b/quickstep/src/com/android/quickstep/views/TaskView.kt
index b18f0b1..815f8fa 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.kt
+++ b/quickstep/src/com/android/quickstep/views/TaskView.kt
@@ -1407,7 +1407,7 @@
     private fun onFocusTransitionProgressUpdated(focusTransitionProgress: Float) {
         taskContainers.forEach {
             it.iconView.setContentAlpha(focusTransitionProgress)
-            it.digitalWellBeingToast?.updateBannerOffset(1f - focusTransitionProgress)
+            it.digitalWellBeingToast?.bannerOffsetPercentage = 1f - focusTransitionProgress
         }
     }
 
@@ -1557,7 +1557,7 @@
     private fun onModalnessUpdated(modalness: Float) {
         taskContainers.forEach {
             it.iconView.setModalAlpha(1 - modalness)
-            it.digitalWellBeingToast?.updateBannerOffset(modalness)
+            it.digitalWellBeingToast?.bannerOffsetPercentage = modalness
         }
     }
 
diff --git a/quickstep/tests/src/com/android/quickstep/TaplDigitalWellBeingToastTest.java b/quickstep/tests/src/com/android/quickstep/TaplDigitalWellBeingToastTest.java
index 6e25b10..e981570 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplDigitalWellBeingToastTest.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplDigitalWellBeingToastTest.java
@@ -67,15 +67,15 @@
             mLauncher.goHome();
             final DigitalWellBeingToast toast = getToast();
 
-            waitForLauncherCondition("Toast is not visible", launcher -> toast.hasLimit());
-            assertEquals("Toast text: ", "5 minutes left today", toast.getText());
+            waitForLauncherCondition("Toast is not visible", launcher -> toast.getHasLimit());
+            assertEquals("Toast text: ", "5 minutes left today", toast.getBannerText());
 
             // Unset time limit for app.
             runWithShellPermission(
                     () -> usageStatsManager.unregisterAppUsageLimitObserver(observerId));
 
             mLauncher.goHome();
-            assertFalse("Toast is visible", getToast().hasLimit());
+            assertFalse("Toast is visible", getToast().getHasLimit());
         } finally {
             runWithShellPermission(
                     () -> usageStatsManager.unregisterAppUsageLimitObserver(observerId));