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

Commit 524cf7b6 authored by Rohan Shah's avatar Rohan Shah
Browse files

[Notif] Add Blocking helper to swipe

Implemented blocking helper UI as part of guts - the
ExpandableNotificationRow that's swiped away is effectively detached
from the notification logic but kept in as a view in the layout (it'll
still show up when looping through the normal view hierarchy). We
purposefully open up NotificationInfo with the intent of showing it as
the blocking helper.

It's animated away/closed when the user interacts with any other content
or swipes away, as with guts (Can be changed in the future if we want it
to stick around for longer).

KIs, that will be tracked in bugs once CL is in:
- Swiping a notification if it's partially hidden by the shelf does not
bring it up into focus
- If only one notification is showing, the shade auto-closes as if it
didn't have any notifications (which it technically doesn't)
- Animation on multiple blocking helper shows cuts too early and doesn't
allow the notification to transition out
- Long press on notification dismisses (it should probably ignore it)
- Metrics are still missing
- Accessibility needs a runthrough

Bug: 63095540,73783854
Test: Visually & Ran all tests
Change-Id: Iaa5e3d5537bbb72c946fdd7ee35b155e22ca2b05
parent 620da524
Loading
Loading
Loading
Loading
+3 −0
Original line number Diff line number Diff line
@@ -31,6 +31,7 @@ import com.android.systemui.classifier.FalsingManager;
import com.android.systemui.keyguard.DismissCallbackRegistry;
import com.android.systemui.qs.QSTileHost;
import com.android.systemui.statusbar.KeyguardIndicationController;
import com.android.systemui.statusbar.NotificationBlockingHelperManager;
import com.android.systemui.statusbar.NotificationEntryManager;
import com.android.systemui.statusbar.NotificationGutsManager;
import com.android.systemui.statusbar.NotificationListener;
@@ -130,6 +131,8 @@ public class SystemUIFactory {
        providers.put(NotificationGroupManager.class, NotificationGroupManager::new);
        providers.put(NotificationMediaManager.class, () -> new NotificationMediaManager(context));
        providers.put(NotificationGutsManager.class, () -> new NotificationGutsManager(context));
        providers.put(NotificationBlockingHelperManager.class,
                () -> new NotificationBlockingHelperManager(context));
        providers.put(NotificationRemoteInputManager.class,
                () -> new NotificationRemoteInputManager(context));
        providers.put(SmartReplyConstants.class,
+42 −7
Original line number Diff line number Diff line
@@ -16,6 +16,7 @@

package com.android.systemui.statusbar;

import static android.service.notification.NotificationListenerService.Ranking.USER_SENTIMENT_NEGATIVE;
import static com.android.systemui.statusbar.notification.ActivityLaunchAnimator.ExpandAnimationParameters;
import static com.android.systemui.statusbar.notification.NotificationInflater.InflationCallback;

@@ -34,7 +35,6 @@ import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Bundle;
import android.service.notification.NotificationListenerService;
import android.service.notification.StatusBarNotification;
import android.util.ArraySet;
import android.util.AttributeSet;
@@ -127,6 +127,8 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
    private boolean mHasUserChangedExpansion;
    /** If {@link #mHasUserChangedExpansion}, has the user expanded this row */
    private boolean mUserExpanded;
    /** Whether the blocking helper is showing on this notification (even if dismissed) */
    private boolean mIsBlockingHelperShowing;

    /**
     * Has this notification been expanded while it was pinned
@@ -400,8 +402,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
        updateIconVisibilities();
        updateShelfIconColor();

        showBlockingHelper(mEntry.userSentiment ==
                NotificationListenerService.Ranking.USER_SENTIMENT_NEGATIVE);
        showBlockingHelperButton(mEntry.userSentiment == USER_SENTIMENT_NEGATIVE);
        updateRippleAllowed();
    }

@@ -594,6 +595,13 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
        return mNotificationParent != null;
    }

    /**
     * @return whether this notification is the only child in the group summary
     */
    public boolean isOnlyChildInGroup() {
        return mGroupManager.isOnlyChildInGroup(getStatusBarNotification());
    }

    public ExpandableNotificationRow getNotificationParent() {
        return mNotificationParent;
    }
@@ -1150,11 +1158,31 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
        return mGroupParentWhenDismissed;
    }

    /**
     * Dismisses the notification with the option of showing the blocking helper in-place if we have
     * a negative user sentiment.
     *
     * @param fromAccessibility whether this dismiss is coming from an accessibility action
     * @return whether a blocking helper is shown in this row
     */
    public boolean performDismissWithBlockingHelper(boolean fromAccessibility) {
        NotificationBlockingHelperManager manager =
                Dependency.get(NotificationBlockingHelperManager.class);
        boolean isBlockingHelperShown = manager.perhapsShowBlockingHelper(this, mMenuRow);

        // Continue with dismiss since we don't want the blocking helper to be directly associated
        // with a certain notification.
        performDismiss(fromAccessibility);
        return isBlockingHelperShown;
    }

    public void performDismiss(boolean fromAccessibility) {
        if (mGroupManager.isOnlyChildInGroup(getStatusBarNotification())) {
        if (isOnlyChildInGroup()) {
            ExpandableNotificationRow groupSummary =
                    mGroupManager.getLogicalGroupSummary(getStatusBarNotification());
            if (groupSummary.isClearable()) {
                // If this is the only child in the group, dismiss the group, but don't try to show
                // the blocking helper affordance!
                groupSummary.performDismiss(fromAccessibility);
            }
        }
@@ -1166,6 +1194,14 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
        }
    }

    public void setBlockingHelperShowing(boolean isBlockingHelperShowing) {
        mIsBlockingHelperShowing = isBlockingHelperShowing;
    }

    public boolean isBlockingHelperShowing() {
        return mIsBlockingHelperShowing;
    }

    public void setOnDismissRunnable(Runnable onDismissRunnable) {
        mOnDismissRunnable = onDismissRunnable;
    }
@@ -1390,7 +1426,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
        requestLayout();
    }

    public void showBlockingHelper(boolean show) {
    public void showBlockingHelperButton(boolean show) {
        mHelperButton.setVisibility(show ? View.VISIBLE : View.GONE);
    }

@@ -1423,7 +1459,6 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
        mPrivateLayout = (NotificationContentView) findViewById(R.id.expanded);
        mLayouts = new NotificationContentView[] {mPrivateLayout, mPublicLayout};

        final NotificationGutsManager gutsMan = Dependency.get(NotificationGutsManager.class);
        mHelperButton = findViewById(R.id.helper);
        mHelperButton.setOnClickListener(view -> {
            doLongClickCallback();
@@ -2526,7 +2561,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
        }
        switch (action) {
            case AccessibilityNodeInfo.ACTION_DISMISS:
                performDismiss(true /* fromAccessibility */);
                performDismissWithBlockingHelper(true /* fromAccessibility */);
                return true;
            case AccessibilityNodeInfo.ACTION_COLLAPSE:
            case AccessibilityNodeInfo.ACTION_EXPAND:
+137 −0
Original line number Diff line number Diff line
/*
 * 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.systemui.statusbar;

import android.content.Context;
import android.support.annotation.VisibleForTesting;
import android.util.Log;

import com.android.systemui.Dependency;
import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin;

import static android.service.notification.NotificationListenerService.Ranking.USER_SENTIMENT_NEGATIVE;

/**
 * Manager for the notification blocking helper - tracks and helps create the blocking helper
 * affordance.
 */
public class NotificationBlockingHelperManager {
    /** Enables debug logging and always makes the blocking helper show up after a dismiss. */
    private static final boolean DEBUG = false;
    private static final String TAG = "BlockingHelper";

    private final Context mContext;
    /** Row that the blocking helper will be shown in (via {@link NotificationGuts}. */
    private ExpandableNotificationRow mBlockingHelperRow;

    /**
     * Whether the notification shade/stack is expanded - used to determine blocking helper
     * eligibility.
     */
    private boolean mIsShadeExpanded;

    public NotificationBlockingHelperManager(Context context) {
        mContext = context;
    }

    /**
     * Potentially shows the blocking helper, represented via the {@link NotificationInfo} menu
     * item, in the current row if user sentiment is negative.
     *
     * @param row row to render the blocking helper in
     * @param menuRow menu used to generate the {@link NotificationInfo} view that houses the
     *                blocking helper UI
     * @return whether we're showing a blocking helper in the given notification row
     */
    boolean perhapsShowBlockingHelper(
            ExpandableNotificationRow row, NotificationMenuRowPlugin menuRow) {
        int numChildren = row.getNumberOfNotificationChildren();

        // We only show the blocking helper if:
        // - The dismissed row is a valid group (>1 or 0 children) or the only child in the group
        // - The notification shade is fully expanded (guarantees we're not touching a HUN).
        // - User sentiment is negative
        if (DEBUG
                || row.getEntry().userSentiment == USER_SENTIMENT_NEGATIVE
                && mIsShadeExpanded
                && (!row.isChildInGroup() || row.isOnlyChildInGroup())) {
            // Dismiss any current blocking helper before continuing forward (only one can be shown
            // at a given time).
            dismissCurrentBlockingHelper();

            if (DEBUG) {
                Log.d(TAG, "Manager.perhapsShowBlockingHelper: Showing new blocking helper");
            }
            NotificationGutsManager manager = Dependency.get(NotificationGutsManager.class);

            // Enable blocking helper on the row before moving forward so everything in the guts is
            // correctly prepped.
            mBlockingHelperRow = row;
            mBlockingHelperRow.setBlockingHelperShowing(true);

            // We don't care about the touch origin (x, y) since we're opening guts without any
            // explicit user interaction.
            manager.openGuts(mBlockingHelperRow, 0, 0, menuRow.getLongpressMenuItem(mContext));
            return true;
        }
        return false;
    }

    /**
     * Dismiss the currently showing blocking helper, if any, through a notification update.
     *
     * @return whether the blocking helper was dismissed
     */
    boolean dismissCurrentBlockingHelper() {
        if (!isBlockingHelperRowNull()) {
            if (DEBUG) {
                Log.d(TAG, "Manager.dismissCurrentBlockingHelper: Dismissing current helper");
            }
            if (!mBlockingHelperRow.isBlockingHelperShowing()) {
                Log.e(TAG, "Manager.dismissCurrentBlockingHelper: "
                        + "Non-null row is not showing a blocking helper");
            }

            mBlockingHelperRow.setBlockingHelperShowing(false);
            if (mBlockingHelperRow.isAttachedToWindow()) {
                Dependency.get(NotificationEntryManager.class).updateNotifications();
            }
            mBlockingHelperRow = null;
            return true;
        }
        return false;
    }

    /**
     * Update the expansion status of the notification shade/stack.
     *
     * @param expandedHeight how much the shade is expanded ({code 0} indicating it's collapsed)
     */
    public void setNotificationShadeExpanded(float expandedHeight) {
        mIsShadeExpanded = expandedHeight > 0.0f;
    }

    @VisibleForTesting
    boolean isBlockingHelperRowNull() {
        return mBlockingHelperRow == null;
    }

    @VisibleForTesting
    void setBlockingHelperRowForTest(ExpandableNotificationRow blockingHelperRowForTest) {
        mBlockingHelperRow = blockingHelperRowForTest;
    }
}
+107 −42
Original line number Diff line number Diff line
@@ -30,6 +30,7 @@ import android.view.ViewAnimationUtils;
import android.view.accessibility.AccessibilityEvent;
import android.widget.FrameLayout;

import com.android.systemui.Dependency;
import com.android.systemui.Interpolators;
import com.android.systemui.R;
import com.android.systemui.statusbar.stack.StackStateAnimator;
@@ -189,8 +190,12 @@ public class NotificationGuts extends FrameLayout {
    }

    public void openControls(
            int x, int y, boolean needsFalsingProtection, @Nullable Runnable onAnimationEnd) {
        animateOpen(x, y, onAnimationEnd);
            boolean shouldDoCircularReveal,
            int x,
            int y,
            boolean needsFalsingProtection,
            @Nullable Runnable onAnimationEnd) {
        animateOpen(shouldDoCircularReveal, x, y, onAnimationEnd);
        setExposed(true /* exposed */, needsFalsingProtection);
    }

@@ -204,7 +209,20 @@ public class NotificationGuts extends FrameLayout {
        }
    }

    /**
     * Closes any exposed guts/views.
     *
     * @param x x coordinate to animate the close circular reveal with
     * @param y y coordinate to animate the close circular reveal with
     * @param save whether the state should be saved
     * @param force whether the guts should be force-closed regardless of state.
     */
    public void closeControls(int x, int y, boolean save, boolean force) {
        // First try to dismiss any blocking helper.
        boolean wasBlockingHelperDismissed =
                Dependency.get(NotificationBlockingHelperManager.class)
                        .dismissCurrentBlockingHelper();

        if (getWindowToken() == null) {
            if (mClosedListener != null) {
                mClosedListener.onGutsClosed(this);
@@ -212,8 +230,12 @@ public class NotificationGuts extends FrameLayout {
            return;
        }

        if (mGutsContent == null || !mGutsContent.handleCloseControls(save, force)) {
            animateClose(x, y);
        if (mGutsContent == null
                || !mGutsContent.handleCloseControls(save, force)
                || wasBlockingHelperDismissed) {
            // We only want to do a circular reveal if we're not showing the blocking helper.
            animateClose(x, y, !wasBlockingHelperDismissed /* shouldDoCircularReveal */);

            setExposed(false, mNeedsFalsingProtection);
            if (mClosedListener != null) {
                mClosedListener.onGutsClosed(this);
@@ -221,47 +243,58 @@ public class NotificationGuts extends FrameLayout {
        }
    }

    private void animateOpen(int x, int y, @Nullable Runnable onAnimationEnd) {
        final double horz = Math.max(getWidth() - x, x);
        final double vert = Math.max(getHeight() - y, y);
        final float r = (float) Math.hypot(horz, vert);

        final Animator a
                = ViewAnimationUtils.createCircularReveal(this, x, y, 0, r);
    /** Animates in the guts view via either a fade or a circular reveal. */
    private void animateOpen(
            boolean shouldDoCircularReveal, int x, int y, @Nullable Runnable onAnimationEnd) {
        if (shouldDoCircularReveal) {
            double horz = Math.max(getWidth() - x, x);
            double vert = Math.max(getHeight() - y, y);
            float r = (float) Math.hypot(horz, vert);
            // Circular reveal originating at (x, y)
            Animator a = ViewAnimationUtils.createCircularReveal(this, x, y, 0, r);
            a.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD);
            a.setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN);
        a.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                super.onAnimationEnd(animation);
                if (onAnimationEnd != null) {
                    onAnimationEnd.run();
                }
            }
        });
            a.addListener(new AnimateOpenListener(onAnimationEnd));
            a.start();
        } else {
            // Fade in content
            this.setAlpha(0f);
            this.animate()
                    .alpha(1f)
                    .setDuration(StackStateAnimator.ANIMATION_DURATION_BLOCKING_HELPER_FADE)
                    .setInterpolator(Interpolators.ALPHA_IN)
                    .setListener(new AnimateOpenListener(onAnimationEnd))
                    .start();
        }
    }

    private void animateClose(int x, int y) {

    /** Animates out the guts view via either a fade or a circular reveal. */
    private void animateClose(int x, int y, boolean shouldDoCircularReveal) {
        if (shouldDoCircularReveal) {
            // Circular reveal originating at (x, y)
            if (x == -1 || y == -1) {
                x = (getLeft() + getRight()) / 2;
                y = (getTop() + getHeight() / 2);
            }
        final double horz = Math.max(getWidth() - x, x);
        final double vert = Math.max(getHeight() - y, y);
        final float r = (float) Math.hypot(horz, vert);
        final Animator a = ViewAnimationUtils.createCircularReveal(this,
            double horz = Math.max(getWidth() - x, x);
            double vert = Math.max(getHeight() - y, y);
            float r = (float) Math.hypot(horz, vert);
            Animator a = ViewAnimationUtils.createCircularReveal(this,
                    x, y, r, 0);
            a.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD);
            a.setInterpolator(Interpolators.FAST_OUT_LINEAR_IN);
        a.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                super.onAnimationEnd(animation);
                setVisibility(View.GONE);
            }
        });
            a.addListener(new AnimateCloseListener(this /* view */));
            a.start();
        } else {
            // Fade in the blocking helper.
            this.animate()
                    .alpha(0f)
                    .setDuration(StackStateAnimator.ANIMATION_DURATION_BLOCKING_HELPER_FADE)
                    .setInterpolator(Interpolators.ALPHA_OUT)
                    .setListener(new AnimateCloseListener(this /* view */))
                    .start();
        }
    }

    public void setActualHeight(int actualHeight) {
@@ -336,4 +369,36 @@ public class NotificationGuts extends FrameLayout {
    public boolean isLeavebehind() {
        return mGutsContent != null && mGutsContent.isLeavebehind();
    }

    /** Listener for animations executed in {@link #animateOpen(boolean, int, int, Runnable)}. */
    private static class AnimateOpenListener extends AnimatorListenerAdapter {
        final Runnable mOnAnimationEnd;

        private AnimateOpenListener(Runnable onAnimationEnd) {
            mOnAnimationEnd = onAnimationEnd;
        }

        @Override
        public void onAnimationEnd(Animator animation) {
            super.onAnimationEnd(animation);
            if (mOnAnimationEnd != null) {
                mOnAnimationEnd.run();
            }
        }
    }

    /** Listener for animations executed in {@link #animateClose(int, int, boolean)}. */
    private static class AnimateCloseListener extends AnimatorListenerAdapter {
        final View mView;

        private AnimateCloseListener(View view) {
            mView = view;
        }

        @Override
        public void onAnimationEnd(Animator animation) {
            super.onAnimationEnd(animation);
            mView.setVisibility(View.GONE);
        }
    }
}
+152 −104

File changed.

Preview size limit exceeded, changes collapsed.

Loading