Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 5b636ebf authored by Sunny Goyal's avatar Sunny Goyal
Browse files

Optimizing some layouts in taskview

> Recycling DigitalWellBeingToast so that the view is not inflated everytime
> Simplifying DigitalWellBeingToast to use a single text view
> Adding support for footers in taskView without creating additional layout

Bug: 122345781
Change-Id: Ia889819b93eb8644532ea95c6767554874d5e2d1
parent 3d6e96d5
Loading
Loading
Loading
Loading
+0 −7
Original line number Diff line number Diff line
<com.android.quickstep.hints.ProactiveHintsContainer
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_gravity="center_horizontal|bottom"
    android:gravity="center_horizontal">
</com.android.quickstep.hints.ProactiveHintsContainer>
 No newline at end of file
+65 −40
Original line number Diff line number Diff line
@@ -18,9 +18,11 @@ package com.android.quickstep.views;

import static android.provider.Settings.ACTION_APP_USAGE_SETTINGS;

import static com.android.launcher3.Utilities.prefixTextWithIcon;

import android.annotation.TargetApi;
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;
@@ -28,16 +30,16 @@ import android.icu.text.MeasureFormat;
import android.icu.text.MeasureFormat.FormatWidth;
import android.icu.util.Measure;
import android.icu.util.MeasureUnit;
import android.os.Build;
import android.os.UserHandle;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.widget.LinearLayout;
import android.widget.TextView;

import androidx.annotation.StringRes;

import com.android.launcher3.BaseActivity;
import com.android.launcher3.BaseDraggingActivity;
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
import com.android.launcher3.userevent.nano.LauncherLogProto;
@@ -46,45 +48,72 @@ import com.android.systemui.shared.recents.model.Task;
import java.time.Duration;
import java.util.Locale;

public final class DigitalWellBeingToast extends LinearLayout {
@TargetApi(Build.VERSION_CODES.Q)
public final class DigitalWellBeingToast {
    static final Intent OPEN_APP_USAGE_SETTINGS_TEMPLATE = new Intent(ACTION_APP_USAGE_SETTINGS);
    static final int MINUTE_MS = 60000;
    private final LauncherApps mLauncherApps;

    public interface InitializeCallback {
        void call(String contentDescription);
    }

    private static final String TAG = DigitalWellBeingToast.class.getSimpleName();

    private final BaseDraggingActivity mActivity;
    private final TaskView mTaskView;
    private final LauncherApps mLauncherApps;

    private Task mTask;
    private TextView mText;
    private boolean mHasLimit;
    private long mAppRemainingTimeMs;

    public DigitalWellBeingToast(Context context, AttributeSet attrs) {
        super(context, attrs);
        setLayoutDirection(Utilities.isRtl(getResources()) ?
                View.LAYOUT_DIRECTION_RTL : View.LAYOUT_DIRECTION_LTR);
        setOnClickListener((view) -> openAppUsageSettings());
        mLauncherApps = context.getSystemService(LauncherApps.class);
    public DigitalWellBeingToast(BaseDraggingActivity activity, TaskView taskView) {
        mActivity = activity;
        mTaskView = taskView;
        mLauncherApps = activity.getSystemService(LauncherApps.class);
    }

    public TextView getTextView() {
        return mText;
    private void setTaskFooter(View view) {
        View oldFooter = mTaskView.setFooter(TaskView.INDEX_DIGITAL_WELLBEING_TOAST, view);
        if (oldFooter != null) {
            oldFooter.setOnClickListener(null);
            mActivity.getViewCache().recycleView(R.layout.digital_wellbeing_toast, oldFooter);
        }
    }

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
    private void setNoLimit() {
        mHasLimit = false;
        mTaskView.setContentDescription(mTask.titleDescription);
        setTaskFooter(null);
        mAppRemainingTimeMs = 0;
    }

        mText = findViewById(R.id.digital_well_being_remaining_time);
    private void setLimit(long appUsageLimitTimeMs, long appRemainingTimeMs) {
        mAppRemainingTimeMs = appRemainingTimeMs;
        mHasLimit = true;
        TextView toast = mActivity.getViewCache().getView(R.layout.digital_wellbeing_toast,
                mActivity, mTaskView);
        toast.setText(prefixTextWithIcon(mActivity, R.drawable.ic_hourglass_top, getText()));
        toast.setOnClickListener(this::openAppUsageSettings);
        setTaskFooter(toast);

        mTaskView.setContentDescription(
                getContentDescriptionForTask(mTask, appUsageLimitTimeMs, appRemainingTimeMs));
        RecentsView rv = mTaskView.getRecentsView();
        if (rv != null) {
            rv.onDigitalWellbeingToastShown();
        }
    }

    public String getText() {
        return getText(mAppRemainingTimeMs);
    }

    public void initialize(Task task, InitializeCallback callback) {
    public boolean hasLimit() {
        return mHasLimit;
    }

    public void initialize(Task task) {
        mTask = task;

        if (task.key.userId != UserHandle.myUserId()) {
            setVisibility(GONE);
            callback.call(task.titleDescription);
            setNoLimit();
            return;
        }

@@ -98,16 +127,12 @@ public final class DigitalWellBeingToast extends LinearLayout {
            final long appRemainingTimeMs =
                    usageLimit != null ? usageLimit.getUsageRemaining() : -1;

            post(() -> {
            mTaskView.post(() -> {
                if (appUsageLimitTimeMs < 0 || appRemainingTimeMs < 0) {
                    setVisibility(GONE);
                    setNoLimit();
                } else {
                    setVisibility(VISIBLE);
                    mText.setText(getText(appRemainingTimeMs));
                    setLimit(appUsageLimitTimeMs, appRemainingTimeMs);
                }

                callback.call(getContentDescriptionForTask(
                        task, appUsageLimitTimeMs, appRemainingTimeMs));
            });
        });
    }
@@ -146,7 +171,7 @@ public final class DigitalWellBeingToast extends LinearLayout {

        // Use a specific string for usage less than one minute but non-zero.
        if (duration.compareTo(Duration.ZERO) > 0) {
            return getResources().getString(durationLessThanOneMinuteStringId);
            return mActivity.getString(durationLessThanOneMinuteStringId);
        }

        // Otherwise, return 0-minute string.
@@ -176,24 +201,24 @@ public final class DigitalWellBeingToast extends LinearLayout {
    }

    private String getText(long remainingTime) {
        return getResources().getString(
        return mActivity.getString(
                R.string.time_left_for_app,
                getRoundedUpToMinuteReadableDuration(remainingTime));
    }

    public void openAppUsageSettings() {
    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 BaseActivity activity = BaseActivity.fromContext(getContext());
            final BaseActivity activity = BaseActivity.fromContext(view.getContext());
            final ActivityOptions options = ActivityOptions.makeScaleUpAnimation(
                    this, 0, 0,
                    getWidth(), getHeight());
                    view, 0, 0,
                    view.getWidth(), view.getHeight());
            activity.startActivity(intent, options.toBundle());
            activity.getUserEventDispatcher().logActionOnControl(LauncherLogProto.Action.Touch.TAP,
                    LauncherLogProto.ControlType.APP_USAGE_SETTINGS, this);
                    LauncherLogProto.ControlType.APP_USAGE_SETTINGS, view);
        } catch (ActivityNotFoundException e) {
            Log.e(TAG, "Failed to open app usage settings for task "
                    + mTask.getTopComponent().getPackageName(), e);
@@ -203,7 +228,7 @@ public final class DigitalWellBeingToast extends LinearLayout {
    private String getContentDescriptionForTask(
            Task task, long appUsageLimitTimeMs, long appRemainingTimeMs) {
        return appUsageLimitTimeMs >= 0 && appRemainingTimeMs >= 0 ?
                getResources().getString(
                mActivity.getString(
                        R.string.task_contents_description_with_remaining_time,
                        task.titleDescription,
                        getText(appRemainingTimeMs)) :
+3 −0
Original line number Diff line number Diff line
@@ -351,6 +351,9 @@ public abstract class RecentsView<T extends BaseActivity> extends PagedView impl
                .getDimensionPixelSize(R.dimen.recents_empty_message_text_padding);
        setWillNotDraw(false);
        updateEmptyMessage();

        // Initialize quickstep specific cache params here, as this is constructed only once
        mActivity.getViewCache().setCacheSize(R.layout.digital_wellbeing_toast, 5);
    }

    public OverScroller getScroller() {
+152 −41
Original line number Diff line number Diff line
@@ -27,6 +27,8 @@ import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ObjectAnimator;
import android.animation.TimeInterpolator;
import android.animation.ValueAnimator;
import android.animation.ValueAnimator.AnimatorUpdateListener;
import android.app.ActivityOptions;
import android.content.Context;
import android.content.res.Resources;
@@ -39,6 +41,7 @@ import android.os.Handler;
import android.util.AttributeSet;
import android.util.FloatProperty;
import android.util.Log;
import android.view.Gravity;
import android.view.View;
import android.view.ViewOutlineProvider;
import android.view.accessibility.AccessibilityNodeInfo;
@@ -145,14 +148,12 @@ public class TaskView extends FrameLayout implements PageCallbacks, Reusable {
            };

    private final TaskOutlineProvider mOutlineProvider;
    private final FooterOutlineProvider mFooterOutlineProvider;

    private Task mTask;
    private TaskThumbnailView mSnapshotView;
    private TaskMenuView mMenuView;
    private IconView mIconView;
    private View mTaskFooterContainer;
    private DigitalWellBeingToast mDigitalWellBeingToast;
    private final DigitalWellBeingToast mDigitalWellBeingToast;
    private float mCurveScale;
    private float mFullscreenProgress;
    private final FullscreenDrawParams mCurrentFullscreenParams;
@@ -171,6 +172,14 @@ public class TaskView extends FrameLayout implements PageCallbacks, Reusable {
    private TaskThumbnailCache.ThumbnailLoadRequest mThumbnailLoadRequest;
    private TaskIconCache.IconLoadRequest mIconLoadRequest;

    // Order in which the footers appear. Lower order appear below higher order.
    public static final int INDEX_DIGITAL_WELLBEING_TOAST = 0;
    public static final int INDEX_PROACTIVE_SUGGEST = 1;
    private final FooterWrapper[] mFooters = new FooterWrapper[2];
    private float mFooterVerticalOffset = 0;
    private float mFooterAlpha = 1;
    private int mStackHeight;

    public TaskView(Context context) {
        this(context, null);
    }
@@ -208,8 +217,9 @@ public class TaskView extends FrameLayout implements PageCallbacks, Reusable {
        mCornerRadius = TaskCornerRadius.get(context);
        mWindowCornerRadius = QuickStepContract.getWindowCornerRadius(context.getResources());
        mCurrentFullscreenParams = new FullscreenDrawParams(mCornerRadius);
        mDigitalWellBeingToast = new DigitalWellBeingToast(mActivity, this);

        mOutlineProvider = new TaskOutlineProvider(getResources(), mCurrentFullscreenParams);
        mFooterOutlineProvider = new FooterOutlineProvider(mCurrentFullscreenParams);
        setOutlineProvider(mOutlineProvider);
    }

@@ -218,10 +228,6 @@ public class TaskView extends FrameLayout implements PageCallbacks, Reusable {
        super.onFinishInflate();
        mSnapshotView = findViewById(R.id.snapshot);
        mIconView = findViewById(R.id.icon);
        mDigitalWellBeingToast = findViewById(R.id.digital_well_being_toast);
        mTaskFooterContainer = findViewById(R.id.task_footer_container);
        mTaskFooterContainer.setOutlineProvider(mFooterOutlineProvider);
        mTaskFooterContainer.setClipToOutline(true);
    }

    public TaskMenuView getMenuView() {
@@ -357,15 +363,7 @@ public class TaskView extends FrameLayout implements PageCallbacks, Reusable {
                        if (ENABLE_QUICKSTEP_LIVE_TILE.get() && isRunningTask()) {
                            getRecentsView().updateLiveTileIcon(task.icon);
                        }
                        mDigitalWellBeingToast.initialize(
                                mTask,
                                contentDescription -> {
                                    setContentDescription(contentDescription);
                                    if (mDigitalWellBeingToast.getVisibility() == VISIBLE
                                            && getRecentsView() != null) {
                                        getRecentsView().onDigitalWellbeingToastShown();
                                    }
                                });
                        mDigitalWellBeingToast.initialize(mTask);
                    });
        } else {
            mSnapshotView.setThumbnail(null, null);
@@ -424,14 +422,12 @@ public class TaskView extends FrameLayout implements PageCallbacks, Reusable {
        mIconView.setScaleX(scale);
        mIconView.setScaleY(scale);

        int footerVerticalOffset = (int) (mTaskFooterContainer.getHeight() * (1.0f - scale));
        mTaskFooterContainer.setTranslationY(
                mCurrentFullscreenParams.mCurrentDrawnInsets.bottom +
                mCurrentFullscreenParams.mCurrentDrawnInsets.top +
                footerVerticalOffset);
        mFooterOutlineProvider.setFullscreenDrawParams(
                mCurrentFullscreenParams, footerVerticalOffset);
        mTaskFooterContainer.invalidateOutline();
        mFooterVerticalOffset = 1.0f - scale;
        for (FooterWrapper footer : mFooters) {
            if (footer != null) {
                footer.updateFooterOffset();
            }
        }
    }

    public void setIconScaleAnimStartProgress(float startProgress) {
@@ -505,8 +501,13 @@ public class TaskView extends FrameLayout implements PageCallbacks, Reusable {
        mSnapshotView.setDimAlpha(curveInterpolation * MAX_PAGE_SCRIM_ALPHA);
        setCurveScale(getCurveScaleForCurveInterpolation(curveInterpolation));

        float fade = Utilities.boundToRange(1.0f - 2 * scrollState.linearInterpolation, 0f, 1f);
        mTaskFooterContainer.setAlpha(fade);
        mFooterAlpha = Utilities.boundToRange(1.0f - 2 * scrollState.linearInterpolation, 0f, 1f);
        for (FooterWrapper footer : mFooters) {
            if (footer != null) {
                footer.mView.setAlpha(mFooterAlpha);
            }
        }

        if (mMenuView != null) {
            mMenuView.setPosition(getX() - getRecentsView().getScrollX(), getY());
            mMenuView.setScaleX(getScaleX());
@@ -514,6 +515,56 @@ public class TaskView extends FrameLayout implements PageCallbacks, Reusable {
        }
    }


    /**
     * Sets the footer at the specific index and returns the previously set footer.
     */
    public View setFooter(int index, View view) {
        View oldFooter = null;

        // If the footer are is already collapsed, do not animate entry
        boolean shouldAnimateEntry = mFooterVerticalOffset <= 0;

        if (mFooters[index] != null) {
            oldFooter = mFooters[index].mView;
            mFooters[index].release();
            removeView(oldFooter);

            // If we are replacing an existing footer, do not animate entry
            shouldAnimateEntry = false;
        }
        if (view != null) {
            int indexToAdd = getChildCount();
            for (int i = index - 1; i >= 0; i--) {
                if (mFooters[i] != null) {
                    indexToAdd = indexOfChild(mFooters[i].mView);
                    break;
                }
            }

            addView(view, indexToAdd);
            ((LayoutParams) view.getLayoutParams()).gravity =
                    Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL;
            view.setAlpha(mFooterAlpha);
            mFooters[index] = new FooterWrapper(view);
            if (shouldAnimateEntry) {
                mFooters[index].animateEntry();
            }
        } else {
            mFooters[index] = null;
        }

        mStackHeight = 0;
        for (FooterWrapper footer : mFooters) {
            if (footer != null) {
                footer.setVerticalShift(mStackHeight);
                mStackHeight += footer.mExpectedHeight;
            }
        }

        return oldFooter;
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
@@ -523,6 +574,18 @@ public class TaskView extends FrameLayout implements PageCallbacks, Reusable {
            SYSTEM_GESTURE_EXCLUSION_RECT.get(0).set(0, 0, getWidth(), getHeight());
            setSystemGestureExclusionRects(SYSTEM_GESTURE_EXCLUSION_RECT);
        }

        mStackHeight = 0;
        for (FooterWrapper footer : mFooters) {
            if (footer != null) {
                mStackHeight += footer.mView.getHeight();
            }
        }
        for (FooterWrapper footer : mFooters) {
            if (footer != null) {
                footer.updateFooterOffset();
            }
        }
    }

    public static float getCurveScaleForInterpolation(float linearInterpolation) {
@@ -581,26 +644,74 @@ public class TaskView extends FrameLayout implements PageCallbacks, Reusable {
        }
    }

    private static final class FooterOutlineProvider extends ViewOutlineProvider {
    private class FooterWrapper extends ViewOutlineProvider {

        final View mView;
        final ViewOutlineProvider mOldOutlineProvider;
        final ViewOutlineProvider mDelegate;

        final int mExpectedHeight;
        final int mOldPaddingBottom;

        int mAnimationOffset = 0;
        int mEntryAnimationOffset = 0;

        private FullscreenDrawParams mFullscreenDrawParams;
        private int mVerticalOffset;
        private final Rect mOutlineRect = new Rect();
        public FooterWrapper(View view) {
            mView = view;
            mOldOutlineProvider = view.getOutlineProvider();
            mDelegate = mOldOutlineProvider == null
                    ? ViewOutlineProvider.BACKGROUND : mOldOutlineProvider;

        FooterOutlineProvider(FullscreenDrawParams params) {
            mFullscreenDrawParams = params;
            int h = view.getLayoutParams().height;
            if (h > 0) {
                mExpectedHeight = h;
            } else {
                int m = MeasureSpec.makeMeasureSpec(MeasureSpec.EXACTLY - 1, MeasureSpec.AT_MOST);
                view.measure(m, m);
                mExpectedHeight = view.getMeasuredHeight();
            }
            mOldPaddingBottom = view.getPaddingBottom();

            if (mOldOutlineProvider != null) {
                view.setOutlineProvider(this);
                view.setClipToOutline(true);
            }
        }

        void setFullscreenDrawParams(FullscreenDrawParams params, int verticalOffset) {
            mFullscreenDrawParams = params;
            mVerticalOffset = verticalOffset;
        public void setVerticalShift(int shift) {
            mView.setPadding(mView.getPaddingLeft(), mView.getPaddingTop(),
                    mView.getPaddingRight(), mOldPaddingBottom + shift);
        }

        @Override
        public void getOutline(View view, Outline outline) {
            mOutlineRect.set(0, 0, view.getWidth(), view.getHeight());
            mOutlineRect.offset(0, -mVerticalOffset);
            outline.setRoundRect(mOutlineRect, mFullscreenDrawParams.mCurrentDrawnCornerRadius);
            mDelegate.getOutline(view, outline);
            outline.offset(0, -mAnimationOffset - mEntryAnimationOffset);
        }

        void updateFooterOffset() {
            mAnimationOffset = Math.round(mStackHeight * mFooterVerticalOffset);
            mView.setTranslationY(mAnimationOffset + mEntryAnimationOffset
                    + mCurrentFullscreenParams.mCurrentDrawnInsets.bottom
                    + mCurrentFullscreenParams.mCurrentDrawnInsets.top);
            mView.invalidateOutline();
        }

        void release() {
            mView.setOutlineProvider(mOldOutlineProvider);
            setVerticalShift(0);
        }

        void animateEntry() {
            ValueAnimator animator = ValueAnimator.ofFloat(0, 1);
            animator.addUpdateListener(anim -> {
               float factor = 1 - anim.getAnimatedFraction();
               int totalShift = mExpectedHeight + mView.getPaddingBottom() - mOldPaddingBottom;
                mEntryAnimationOffset = Math.round(factor * totalShift);
                updateFooterOffset();
            });
            animator.setDuration(100);
            animator.start();
        }
    }

@@ -624,7 +735,7 @@ public class TaskView extends FrameLayout implements PageCallbacks, Reusable {
            }
        }

        if (mDigitalWellBeingToast.getVisibility() == VISIBLE) {
        if (mDigitalWellBeingToast.hasLimit()) {
            info.addAction(
                    new AccessibilityNodeInfo.AccessibilityAction(
                            R.string.accessibility_app_usage_settings,
@@ -648,7 +759,7 @@ public class TaskView extends FrameLayout implements PageCallbacks, Reusable {
        }

        if (action == R.string.accessibility_app_usage_settings) {
            mDigitalWellBeingToast.openAppUsageSettings();
            mDigitalWellBeingToast.openAppUsageSettings(this);
            return true;
        }

+27 −0
Original line number Diff line number Diff line
<?xml version="1.0" encoding="utf-8"?>
<!--
     Copyright (C) 2019 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.
-->
<TextView
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="48dp"
    android:background="@drawable/bg_wellbeing_toast"
    android:fontFamily="sans-serif"
    android:forceHasOverlappingRendering="false"
    android:gravity="center"
    android:importantForAccessibility="noHideDescendants"
    android:textColor="@android:color/white"
    android:textSize="14sp"/>
 No newline at end of file
Loading