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));