Merge "Show Digital Wellbeing Banners for split tasks" into sc-v2-dev

This commit is contained in:
TreeHugger Robot 2021-12-16 11:13:10 +00:00 committed by Android (Google) Code Review
commit 8d25a0a363
7 changed files with 276 additions and 32 deletions

View File

@ -19,6 +19,7 @@ package com.android.quickstep.views;
import static android.provider.Settings.ACTION_APP_USAGE_SETTINGS;
import static android.view.Gravity.BOTTOM;
import static android.view.Gravity.CENTER_HORIZONTAL;
import static android.view.Gravity.START;
import static com.android.launcher3.Utilities.prefixTextWithIcon;
import static com.android.launcher3.util.Executors.THREAD_POOL_EXECUTOR;
@ -38,26 +39,51 @@ import android.icu.util.MeasureUnit;
import android.os.Build;
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.widget.FrameLayout;
import android.widget.TextView;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import com.android.launcher3.BaseActivity;
import com.android.launcher3.BaseDraggingActivity;
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
import com.android.launcher3.touch.PagedOrientationHandler;
import com.android.launcher3.util.SplitConfigurationOptions.StagedSplitBounds;
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;
@TargetApi(Build.VERSION_CODES.Q)
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 SPLIT_BANNER_CONFIG{}
static final Intent OPEN_APP_USAGE_SETTINGS_TEMPLATE = new Intent(ACTION_APP_USAGE_SETTINGS);
static final int MINUTE_MS = 60000;
@ -74,7 +100,16 @@ public final class DigitalWellBeingToast {
private View mBanner;
private ViewOutlineProvider mOldBannerOutlineProvider;
private float mBannerOffsetPercentage;
private float mVerticalOffset = 0f;
/**
* Clips rect provided by {@link #mOldBannerOutlineProvider} when in the model state to
* hide this banner as the taskView scales up and down
*/
private float mModalOffset = 0f;
@Nullable
private StagedSplitBounds mStagedSplitBounds;
private int mSplitBannerConfig = SPLIT_BANNER_FULLSCREEN;
private float mSplitOffsetTranslationY;
private float mSplitOffsetTranslationX;
public DigitalWellBeingToast(BaseDraggingActivity activity, TaskView taskView) {
mActivity = activity;
@ -103,7 +138,7 @@ public final class DigitalWellBeingToast {
}
public String getText() {
return getText(mAppRemainingTimeMs);
return getText(mAppRemainingTimeMs, false /* forContentDesc */);
}
public boolean hasLimit() {
@ -138,6 +173,31 @@ public final class DigitalWellBeingToast {
});
}
public void setSplitConfiguration(StagedSplitBounds stagedSplitBounds) {
mStagedSplitBounds = stagedSplitBounds;
if (mStagedSplitBounds == null ||
!mActivity.getDeviceProfile().overviewShowAsGrid ||
mTaskView.isFocusedTask()) {
mSplitBannerConfig = SPLIT_BANNER_FULLSCREEN;
return;
}
// For portrait grid only height of task changes, not width. So we keep the text the same
if (!mActivity.getDeviceProfile().isLandscape) {
mSplitBannerConfig = SPLIT_GRID_BANNER_LARGE;
return;
}
// For landscape grid, for 30% width we only show icon, otherwise show icon and time
if (mTask.key.id == mStagedSplitBounds.leftTopTaskId) {
mSplitBannerConfig = mStagedSplitBounds.leftTaskPercent < THRESHOLD_LEFT_ICON_ONLY ?
SPLIT_GRID_BANNER_SMALL : SPLIT_GRID_BANNER_LARGE;
} else {
mSplitBannerConfig = mStagedSplitBounds.leftTaskPercent > THRESHOLD_RIGHT_ICON_ONLY ?
SPLIT_GRID_BANNER_SMALL : SPLIT_GRID_BANNER_LARGE;
}
}
private String getReadableDuration(
Duration duration,
FormatWidth formatWidthHourAndMinute,
@ -181,30 +241,33 @@ public final class DigitalWellBeingToast {
.formatMeasures(new Measure(0, MeasureUnit.MINUTE));
}
private String getReadableDuration(
Duration duration,
FormatWidth formatWidthHourAndMinute,
@StringRes int durationLessThanOneMinuteStringId) {
return getReadableDuration(
duration,
formatWidthHourAndMinute,
durationLessThanOneMinuteStringId,
/* forceFormatWidth= */ false);
}
private String getRoundedUpToMinuteReadableDuration(long remainingTime) {
/**
* Returns text to show for the banner depending on {@link #mSplitBannerConfig}
* 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);
return getReadableDuration(
duration, FormatWidth.NARROW, R.string.shorter_duration_less_than_one_minute);
}
String readableDuration = getReadableDuration(duration,
FormatWidth.NARROW,
R.string.shorter_duration_less_than_one_minute,
false /* forceFormatWidth */);
if (forContentDesc || mSplitBannerConfig == SPLIT_BANNER_FULLSCREEN) {
return mActivity.getString(
R.string.time_left_for_app,
readableDuration);
}
private String getText(long remainingTime) {
return mActivity.getString(
R.string.time_left_for_app,
getRoundedUpToMinuteReadableDuration(remainingTime));
if (mSplitBannerConfig == SPLIT_GRID_BANNER_SMALL) {
// show no text
return "";
} else { // SPLIT_GRID_BANNER_LARGE
// only show time
return readableDuration;
}
}
public void openAppUsageSettings(View view) {
@ -232,7 +295,7 @@ public final class DigitalWellBeingToast {
mActivity.getString(
R.string.task_contents_description_with_remaining_time,
task.titleDescription,
getText(appRemainingTimeMs)) :
getText(appRemainingTimeMs, true /* forContentDesc */)) :
task.titleDescription;
}
@ -261,10 +324,18 @@ public final class DigitalWellBeingToast {
private void setupAndAddBanner() {
FrameLayout.LayoutParams layoutParams =
(FrameLayout.LayoutParams) mBanner.getLayoutParams();
layoutParams.gravity = BOTTOM | CENTER_HORIZONTAL;
DeviceProfile deviceProfile = mActivity.getDeviceProfile();
layoutParams.bottomMargin = ((ViewGroup.MarginLayoutParams)
mTaskView.getThumbnail().getLayoutParams()).bottomMargin;
mBanner.setTranslationY(mBannerOffsetPercentage * mBanner.getHeight());
PagedOrientationHandler orientationHandler = mTaskView.getPagedOrientationHandler();
Pair<Float, Float> translations = orientationHandler
.setDwbLayoutParamsAndGetTranslations(mTaskView.getMeasuredWidth(),
mTaskView.getMeasuredHeight(), mStagedSplitBounds, deviceProfile,
mTaskView.getThumbnails(), mTask.key.id, mBanner);
mSplitOffsetTranslationX = translations.first;
mSplitOffsetTranslationY = translations.second;
updateTranslationY();
updateTranslationX();
mTaskView.addView(mBanner);
}
@ -274,7 +345,9 @@ public final class DigitalWellBeingToast {
@Override
public void getOutline(View view, Outline outline) {
mOldBannerOutlineProvider.getOutline(view, outline);
outline.offset(0, Math.round(-view.getTranslationY() + mVerticalOffset));
float verticalTranslation = -view.getTranslationY() + mModalOffset
+ mSplitOffsetTranslationY;
outline.offset(0, Math.round(verticalTranslation));
}
});
mBanner.setClipToOutline(true);
@ -282,13 +355,33 @@ public final class DigitalWellBeingToast {
void updateBannerOffset(float offsetPercentage, float verticalOffset) {
if (mBanner != null && mBannerOffsetPercentage != offsetPercentage) {
mVerticalOffset = verticalOffset;
mModalOffset = verticalOffset;
mBannerOffsetPercentage = offsetPercentage;
mBanner.setTranslationY(offsetPercentage * mBanner.getHeight() + mVerticalOffset);
updateTranslationY();
mBanner.invalidateOutline();
}
}
private void updateTranslationY() {
if (mBanner == null) {
return;
}
mBanner.setTranslationY(
(mBannerOffsetPercentage * mBanner.getHeight()) +
mModalOffset +
mSplitOffsetTranslationY
);
}
private void updateTranslationX() {
if (mBanner == null) {
return;
}
mBanner.setTranslationX(mSplitOffsetTranslationX);
}
void setBannerColorTint(int color, float amount) {
if (mBanner == null) {
return;

View File

@ -50,17 +50,20 @@ public class GroupedTaskView extends TaskView {
private final float[] mIcon2CenterCoords = new float[2];
private TransformingTouchDelegate mIcon2TouchDelegate;
@Nullable private StagedSplitBounds mSplitBoundsConfig;
private final DigitalWellBeingToast mDigitalWellBeingToast2;
public GroupedTaskView(Context context) {
super(context);
this(context, null);
}
public GroupedTaskView(Context context, AttributeSet attrs) {
super(context, attrs);
this(context, attrs, 0);
}
public GroupedTaskView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mDigitalWellBeingToast2 = new DigitalWellBeingToast(mActivity, this);
}
@Override
@ -102,7 +105,9 @@ public class GroupedTaskView extends TaskView {
mIconLoadRequest2 = iconCache.updateIconInBackground(mSecondaryTask,
(task) -> {
setIcon(mIconView2, task.icon);
// TODO(199936292) Digital Wellbeing for individual tasks?
mDigitalWellBeingToast2.initialize(mSecondaryTask);
mDigitalWellBeingToast2.setSplitConfiguration(mSplitBoundsConfig);
mDigitalWellBeingToast.setSplitConfiguration(mSplitBoundsConfig);
});
}
} else {
@ -262,6 +267,19 @@ public class GroupedTaskView extends TaskView {
@Override
protected void setIconAndDimTransitionProgress(float progress, boolean invert) {
super.setIconAndDimTransitionProgress(progress, invert);
mIconView2.setAlpha(mIconView.getAlpha());
// Value set by super call
float scale = mIconView.getAlpha();
mIconView2.setAlpha(scale);
mDigitalWellBeingToast2.updateBannerOffset(1f - scale,
mCurrentFullscreenParams.mCurrentDrawnInsets.top
+ mCurrentFullscreenParams.mCurrentDrawnInsets.bottom);
}
@Override
public void setColorTint(float amount, int tintColor) {
super.setColorTint(amount, tintColor);
mIconView2.setIconColorTint(tintColor, amount);
mSnapshotView2.setDimAlpha(amount);
mDigitalWellBeingToast2.setBannerColorTint(tintColor, amount);
}
}

View File

@ -366,7 +366,7 @@ public class TaskView extends FrameLayout implements Reusable {
protected Task mTask;
protected TaskThumbnailView mSnapshotView;
protected IconView mIconView;
private final DigitalWellBeingToast mDigitalWellBeingToast;
protected final DigitalWellBeingToast mDigitalWellBeingToast;
private float mFullscreenProgress;
private float mGridProgress;
private float mNonGridScale = 1;

View File

@ -309,6 +309,46 @@ public class LandscapePagedViewHandler implements PagedOrientationHandler {
return new PointF(margin, 0);
}
@Override
public Pair<Float, Float> setDwbLayoutParamsAndGetTranslations(int taskViewWidth,
int taskViewHeight, StagedSplitBounds splitBounds, DeviceProfile deviceProfile,
View[] thumbnailViews, int desiredTaskId, View banner) {
float translationX = 0;
float translationY = 0;
FrameLayout.LayoutParams bannerParams = (FrameLayout.LayoutParams) banner.getLayoutParams();
banner.setPivotX(0);
banner.setPivotY(0);
banner.setRotation(getDegreesRotated());
translationX = banner.getHeight();
FrameLayout.LayoutParams snapshotParams =
(FrameLayout.LayoutParams) thumbnailViews[0]
.getLayoutParams();
bannerParams.gravity = TOP | START;
if (splitBounds == null) {
// Single, fullscreen case
bannerParams.width = taskViewHeight - snapshotParams.topMargin;
return new Pair<>(translationX, Integer.valueOf(snapshotParams.topMargin).floatValue());
}
// Set correct width
if (desiredTaskId == splitBounds.leftTopTaskId) {
bannerParams.width = thumbnailViews[0].getMeasuredHeight();
} else {
bannerParams.width = thumbnailViews[1].getMeasuredHeight();
}
// Set translations
if (desiredTaskId == splitBounds.rightBottomTaskId) {
translationY = (snapshotParams.topMargin + taskViewHeight)
* (splitBounds.leftTaskPercent) +
(taskViewHeight * splitBounds.dividerWidthPercent);
}
if (desiredTaskId == splitBounds.leftTopTaskId) {
translationY = snapshotParams.topMargin;
}
return new Pair<>(translationX, translationY);
}
/* ---------- The following are only used by TaskViewTouchHandler. ---------- */
@Override

View File

@ -191,6 +191,10 @@ public interface PagedOrientationHandler {
*/
PointF getAdditionalInsetForTaskMenu(float margin);
Pair<Float, Float> setDwbLayoutParamsAndGetTranslations(int taskViewWidth,
int taskViewHeight, StagedSplitBounds splitBounds, DeviceProfile deviceProfile,
View[] thumbnailViews, int desiredTaskId, View banner);
// The following are only used by TaskViewTouchHandler.
/** @return Either VERTICAL or HORIZONTAL. */
SingleAxisSwipeDetector.Direction getUpDownSwipeDirection();

View File

@ -16,9 +16,11 @@
package com.android.launcher3.touch;
import static android.view.Gravity.BOTTOM;
import static android.view.Gravity.CENTER_HORIZONTAL;
import static android.view.Gravity.START;
import static android.view.Gravity.TOP;
import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_X;
@ -321,6 +323,50 @@ public class PortraitPagedViewHandler implements PagedOrientationHandler {
return new PointF(0, 0);
}
@Override
public Pair<Float, Float> setDwbLayoutParamsAndGetTranslations(int taskViewWidth,
int taskViewHeight, StagedSplitBounds splitBounds, DeviceProfile deviceProfile,
View[] thumbnailViews, int desiredTaskId, View banner) {
float translationX = 0;
float translationY = 0;
FrameLayout.LayoutParams bannerParams = (FrameLayout.LayoutParams) banner.getLayoutParams();
banner.setPivotX(0);
banner.setPivotY(0);
banner.setRotation(getDegreesRotated());
if (splitBounds == null) {
// Single, fullscreen case
bannerParams.width = MATCH_PARENT;
bannerParams.gravity = BOTTOM | CENTER_HORIZONTAL;
return new Pair<>(translationX, translationY);
}
bannerParams.gravity = BOTTOM | ((deviceProfile.isLandscape) ? START : CENTER_HORIZONTAL);
// Set correct width
if (desiredTaskId == splitBounds.leftTopTaskId) {
bannerParams.width = thumbnailViews[0].getMeasuredWidth();
} else {
bannerParams.width = thumbnailViews[1].getMeasuredWidth();
}
// Set translations
if (deviceProfile.isLandscape) {
if (desiredTaskId == splitBounds.rightBottomTaskId) {
translationX = ((taskViewWidth * splitBounds.leftTaskPercent)
+ (taskViewWidth * splitBounds.dividerWidthPercent));
}
} else {
if (desiredTaskId == splitBounds.leftTopTaskId) {
FrameLayout.LayoutParams snapshotParams =
(FrameLayout.LayoutParams) thumbnailViews[0]
.getLayoutParams();
translationY = -((taskViewHeight - snapshotParams.topMargin)
* (1f - splitBounds.topTaskPercent));
}
}
return new Pair<>(translationX, translationY);
}
/* ---------- The following are only used by TaskViewTouchHandler. ---------- */
@Override

View File

@ -16,6 +16,7 @@
package com.android.launcher3.touch;
import static android.view.Gravity.BOTTOM;
import static android.view.Gravity.CENTER_VERTICAL;
import static android.view.Gravity.END;
import static android.view.Gravity.START;
@ -29,6 +30,7 @@ import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_TYPE_MA
import android.content.res.Resources;
import android.graphics.PointF;
import android.graphics.Rect;
import android.util.Pair;
import android.view.Surface;
import android.view.View;
import android.widget.FrameLayout;
@ -105,6 +107,47 @@ public class SeascapePagedViewHandler extends LandscapePagedViewHandler {
return new PointF(-margin, margin);
}
@Override
public Pair<Float, Float> setDwbLayoutParamsAndGetTranslations(int taskViewWidth,
int taskViewHeight, StagedSplitBounds splitBounds, DeviceProfile deviceProfile,
View[] thumbnailViews, int desiredTaskId, View banner) {
float translationX = 0;
float translationY = 0;
FrameLayout.LayoutParams bannerParams = (FrameLayout.LayoutParams) banner.getLayoutParams();
banner.setPivotX(0);
banner.setPivotY(0);
banner.setRotation(getDegreesRotated());
FrameLayout.LayoutParams snapshotParams =
(FrameLayout.LayoutParams) thumbnailViews[0]
.getLayoutParams();
bannerParams.gravity = BOTTOM | END;
translationX = taskViewWidth - banner.getHeight();
if (splitBounds == null) {
// Single, fullscreen case
bannerParams.width = taskViewHeight - snapshotParams.topMargin;
translationY = banner.getHeight();
return new Pair<>(translationX, translationY);
}
// Set correct width
if (desiredTaskId == splitBounds.leftTopTaskId) {
bannerParams.width = thumbnailViews[1].getMeasuredHeight();
} else {
bannerParams.width = thumbnailViews[0].getMeasuredHeight();
}
// Set translations
if (desiredTaskId == splitBounds.rightBottomTaskId) {
translationY = -(taskViewHeight - snapshotParams.topMargin)
* (1f - splitBounds.leftTaskPercent)
+ banner.getHeight();
}
if (desiredTaskId == splitBounds.leftTopTaskId) {
translationY = banner.getHeight();
}
return new Pair<>(translationX, translationY);
}
@Override
public int getDistanceToBottomOfRect(DeviceProfile dp, Rect rect) {
return dp.widthPx - rect.right;