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

Commit 45d20bed authored by Aaron Heuckroth's avatar Aaron Heuckroth
Browse files

Extract NotificationSwipeHelper and related state from NSSL.

Change-Id: Ic0a1178e4f3d6f2addd9bae1c31ec57dffee8eba
Test: Automated tests should pass. Manual testing should also show that notifications behave the same as before (swiping, tapping, dismissing, snapping/unsnapping menu, etc.)
parent 82b66a4f
Loading
Loading
Loading
Loading
+2 −1
Original line number Diff line number Diff line
@@ -61,13 +61,14 @@ public class SwipeHelper implements Gefingerpoken {
    public static final float SWIPED_FAR_ENOUGH_SIZE_FRACTION = 0.6f;
    static final float MAX_SCROLL_SIZE_FRACTION = 0.3f;

    protected final Handler mHandler;

    private float mMinSwipeProgress = 0f;
    private float mMaxSwipeProgress = 1f;

    private final FlingAnimationUtils mFlingAnimationUtils;
    private float mPagingTouchSlop;
    private final Callback mCallback;
    private final Handler mHandler;
    private final int mSwipeDirection;
    private final VelocityTracker mVelocityTracker;
    private final FalsingManager mFalsingManager;
+287 −535

File changed.

Preview size limit exceeded, changes collapsed.

+424 −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 Licen
 */


package com.android.systemui.statusbar.notification.stack;

import android.animation.Animator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Rect;
import android.os.Handler;
import android.service.notification.StatusBarNotification;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;

import com.android.internal.annotations.VisibleForTesting;
import com.android.systemui.SwipeHelper;
import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin;
import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper;
import com.android.systemui.statusbar.notification.ShadeViewRefactor;
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
import com.android.systemui.statusbar.notification.row.ExpandableView;

@ShadeViewRefactor(ShadeViewRefactor.RefactorComponent.INPUT)
class NotificationSwipeHelper extends SwipeHelper
        implements NotificationSwipeActionHelper {
    @VisibleForTesting
    protected static final long COVER_MENU_DELAY = 4000;
    private static final String TAG = "NotificationSwipeHelper";
    private final Runnable mFalsingCheck;
    private View mTranslatingParentView;
    private View mMenuExposedView;
    private final NotificationCallback mCallback;
    private final NotificationMenuRowPlugin.OnMenuEventListener mMenuListener;

    private static final long SWIPE_MENU_TIMING = 200;

    private NotificationMenuRowPlugin mCurrMenuRow;

    public NotificationSwipeHelper(int swipeDirection, NotificationCallback callback,
            Context context, NotificationMenuRowPlugin.OnMenuEventListener menuListener) {
        super(swipeDirection, callback, context);
        mMenuListener = menuListener;
        mCallback = callback;
        mFalsingCheck = new Runnable() {
            @Override
            public void run() {
                resetExposedMenuView(true /* animate */, true /* force */);
            }
        };
    }

    public View getTranslatingParentView() {
        return mTranslatingParentView;
    }

    public void clearTranslatingParentView() { setTranslatingParentView(null); }

    @VisibleForTesting
    protected void setTranslatingParentView(View view) { mTranslatingParentView = view; };

    public void setExposedMenuView(View view) {
        mMenuExposedView = view;
    }

    public void clearExposedMenuView() { setExposedMenuView(null); }

    public void clearCurrentMenuRow() { setCurrentMenuRow(null); }

    public View getExposedMenuView() {
        return mMenuExposedView;
    }

    public void setCurrentMenuRow(NotificationMenuRowPlugin menuRow) {
        mCurrMenuRow = menuRow;
    }

    public NotificationMenuRowPlugin getCurrentMenuRow() {  return mCurrMenuRow; }

    @VisibleForTesting
    protected Handler getHandler() { return mHandler; }

    @VisibleForTesting
    protected Runnable getFalsingCheck() { return mFalsingCheck; };

    @Override
    public void onDownUpdate(View currView, MotionEvent ev) {
        mTranslatingParentView = currView;
        NotificationMenuRowPlugin menuRow = getCurrentMenuRow();
        if (menuRow != null) {
            menuRow.onTouchStart();
        }
        clearCurrentMenuRow();
        getHandler().removeCallbacks(getFalsingCheck());

        // Slide back any notifications that might be showing a menu
        resetExposedMenuView(true /* animate */, false /* force */);

        if (currView instanceof ExpandableNotificationRow) {
            initializeRow((ExpandableNotificationRow) currView);
        }
    }

    @VisibleForTesting
    protected void initializeRow(ExpandableNotificationRow row) {
        if (row.getEntry().hasFinishedInitialization()) {
            mCurrMenuRow = row.createMenu();
            mCurrMenuRow.setMenuClickListener(mMenuListener);
            mCurrMenuRow.onTouchStart();
        }
    }

    private boolean swipedEnoughToShowMenu(NotificationMenuRowPlugin menuRow) {
        return !swipedFarEnough() && menuRow.isSwipedEnoughToShowMenu();
    }

    @Override
    public void onMoveUpdate(View view, MotionEvent ev, float translation, float delta) {
        getHandler().removeCallbacks(getFalsingCheck());
        NotificationMenuRowPlugin menuRow = getCurrentMenuRow();
        if (menuRow != null) {
            menuRow.onTouchMove(delta);
        }
    }

    @Override
    public boolean handleUpEvent(MotionEvent ev, View animView, float velocity,
            float translation) {
        NotificationMenuRowPlugin menuRow = getCurrentMenuRow();
        if (menuRow != null) {
            menuRow.onTouchEnd();
            handleMenuRowSwipe(ev, animView, velocity, menuRow);
            return true;
        }
        return false;
    }

    @VisibleForTesting
    protected void handleMenuRowSwipe(MotionEvent ev, View animView, float velocity,
            NotificationMenuRowPlugin menuRow) {
        if (!menuRow.shouldShowMenu()) {
            // If the menu should not be shown, then there is no need to check if the a swipe
            // should result in a snapping to the menu. As a result, just check if the swipe
            // was enough to dismiss the notification.
            if (isDismissGesture(ev)) {
                dismiss(animView, velocity);
            } else {
                snapClosed(animView, velocity);
                menuRow.onSnapClosed();
            }
            return;
        }

        if (menuRow.isSnappedAndOnSameSide()) {
            // Menu was snapped to previously and we're on the same side
            handleSwipeFromSnap(ev, animView, velocity, menuRow);
        } else {
            // Menu has not been snapped, or was snapped previously but is now on
            // the opposite side.
            handleSwipeFromNonSnap(ev, animView, velocity, menuRow);
        }
    }

    private void handleSwipeFromNonSnap(MotionEvent ev, View animView, float velocity,
            NotificationMenuRowPlugin menuRow) {
        boolean isDismissGesture = isDismissGesture(ev);
        final boolean gestureTowardsMenu = menuRow.isTowardsMenu(velocity);
        final boolean gestureFastEnough = getEscapeVelocity() <= Math.abs(velocity);

        final double timeForGesture = ev.getEventTime() - ev.getDownTime();
        final boolean showMenuForSlowOnGoing = !menuRow.canBeDismissed()
                && timeForGesture >= SWIPE_MENU_TIMING;

        if (!isFalseGesture(ev)
                && (swipedEnoughToShowMenu(menuRow)
                && (!gestureFastEnough || showMenuForSlowOnGoing))
                || (gestureTowardsMenu && !isDismissGesture)) {
            // Menu has not been snapped to previously and this is menu revealing gesture
            snapOpen(animView, menuRow.getMenuSnapTarget(), velocity);
            menuRow.onSnapOpen();
        } else if (isDismissGesture(ev) && !gestureTowardsMenu) {
            dismiss(animView, velocity);
            menuRow.onDismiss();
        } else {
            snapClosed(animView, velocity);
            menuRow.onSnapClosed();
        }
    }

    private void handleSwipeFromSnap(MotionEvent ev, View animView, float velocity,
            NotificationMenuRowPlugin menuRow) {
        boolean isDismissGesture = isDismissGesture(ev);

        final boolean withinSnapMenuThreshold =
                menuRow.isWithinSnapMenuThreshold();

        if (withinSnapMenuThreshold && !isDismissGesture) {
            // Haven't moved enough to unsnap from the menu
            menuRow.onSnapOpen();
            snapOpen(animView, menuRow.getMenuSnapTarget(), velocity);
        } else if (isDismissGesture && !menuRow.shouldSnapBack()) {
            // Only dismiss if we're not moving towards the menu
            dismiss(animView, velocity);
            menuRow.onDismiss();
        } else {
            snapClosed(animView, velocity);
            menuRow.onSnapClosed();
        }
    }

    @Override
    public void dismissChild(final View view, float velocity,
            boolean useAccelerateInterpolator) {
        superDismissChild(view, velocity, useAccelerateInterpolator);
        if (mCallback.isExpanded()) {
            // We don't want to quick-dismiss when it's a heads up as this might lead to closing
            // of the panel early.
            mCallback.handleChildViewDismissed(view);
        }
        mCallback.onDismiss();
        handleMenuCoveredOrDismissed();
    }

    @VisibleForTesting
    protected void superDismissChild(final View view, float velocity, boolean useAccelerateInterpolator) {
        super.dismissChild(view, velocity, useAccelerateInterpolator);
    }

    @VisibleForTesting
    protected void superSnapChild(final View animView, final float targetLeft, float velocity) {
        super.snapChild(animView, targetLeft, velocity);
    }

    @Override
    public void snapChild(final View animView, final float targetLeft, float velocity) {
        superSnapChild(animView, targetLeft, velocity);
        mCallback.onDragCancelled(animView);
        if (targetLeft == 0) {
            handleMenuCoveredOrDismissed();
        }
    }

    @Override
    public void snooze(StatusBarNotification sbn, SnoozeOption snoozeOption) {
        mCallback.onSnooze(sbn, snoozeOption);
    }

    @VisibleForTesting
    protected void handleMenuCoveredOrDismissed() {
        View exposedMenuView = getExposedMenuView();
        if (exposedMenuView != null && exposedMenuView == mTranslatingParentView) {
            clearExposedMenuView();
        }
    }

    @VisibleForTesting
    protected Animator superGetViewTranslationAnimator(View v, float target,
            ValueAnimator.AnimatorUpdateListener listener) {
        return super.getViewTranslationAnimator(v, target, listener);
    }

    @Override
    public Animator getViewTranslationAnimator(View v, float target,
            ValueAnimator.AnimatorUpdateListener listener) {
        if (v instanceof ExpandableNotificationRow) {
            return ((ExpandableNotificationRow) v).getTranslateViewAnimator(target, listener);
        } else {
            return superGetViewTranslationAnimator(v, target, listener);
        }
    }

    @Override
    public void setTranslation(View v, float translate) {
        if (v instanceof ExpandableNotificationRow) {
            ((ExpandableNotificationRow) v).setTranslation(translate);
        } else {
            Log.wtf(TAG, "setTranslation should only be called on an ExpandableNotificationRow.");
        }
    }

    @Override
    public float getTranslation(View v) {
        if (v instanceof ExpandableNotificationRow) {
            return ((ExpandableNotificationRow) v).getTranslation();
        }
        else {
            Log.wtf(TAG, "getTranslation should only be called on an ExpandableNotificationRow.");
            return 0f;
        }
    }

    @Override
    public boolean swipedFastEnough(float translation, float viewSize) {
        return swipedFastEnough();
    }

    @Override
    @VisibleForTesting
    protected boolean swipedFastEnough() {
        return super.swipedFastEnough();
    }

    @Override
    public boolean swipedFarEnough(float translation, float viewSize) {
        return swipedFarEnough();
    }

    @Override
    @VisibleForTesting
    protected boolean swipedFarEnough() {
        return super.swipedFarEnough();
    }

    @Override
    public void dismiss(View animView, float velocity) {
        dismissChild(animView, velocity,
                !swipedFastEnough() /* useAccelerateInterpolator */);
    }

    @Override
    public void snapOpen(View animView, int targetLeft, float velocity) {
        snapChild(animView, targetLeft, velocity);
    }

    @VisibleForTesting
    protected void snapClosed(View animView, float velocity) {
        snapChild(animView, 0, velocity);
    }

    @Override
    @VisibleForTesting
    protected float getEscapeVelocity() {
        return super.getEscapeVelocity();
    }

    @Override
    public float getMinDismissVelocity() {
        return getEscapeVelocity();
    }

    public void onMenuShown(View animView) {
        setExposedMenuView(getTranslatingParentView());
        mCallback.onDragCancelled(animView);
        Handler handler = getHandler();

        // If we're on the lockscreen we want to false this.
        if (mCallback.isAntiFalsingNeeded()) {
            handler.removeCallbacks(getFalsingCheck());
            handler.postDelayed(getFalsingCheck(), COVER_MENU_DELAY);
        }
    }

    @VisibleForTesting
    protected boolean shouldResetMenu(boolean force) {
        if (mMenuExposedView == null
                || (!force && mMenuExposedView == mTranslatingParentView)) {
            // If no menu is showing or it's showing for this view we do nothing.
            return false;
        }
        return true;
    }

    public void resetExposedMenuView(boolean animate, boolean force) {
        if (!shouldResetMenu(force)) {
            return;
        }
        final View prevMenuExposedView = getExposedMenuView();
        if (animate) {
            Animator anim = getViewTranslationAnimator(prevMenuExposedView,
                    0 /* leftTarget */, null /* updateListener */);
            if (anim != null) {
                anim.start();
            }
        } else if (prevMenuExposedView instanceof ExpandableNotificationRow) {
            ExpandableNotificationRow row = (ExpandableNotificationRow) prevMenuExposedView;
            if (!row.isRemoved()) {
                row.resetTranslation();
            }
        }
        clearExposedMenuView();
    }

    public static boolean isTouchInView(MotionEvent ev, View view) {
        if (view == null) {
            return false;
        }
        final int height = (view instanceof ExpandableView)
                ? ((ExpandableView) view).getActualHeight()
                : view.getHeight();
        final int rx = (int) ev.getRawX();
        final int ry = (int) ev.getRawY();
        int[] temp = new int[2];
        view.getLocationOnScreen(temp);
        final int x = temp[0];
        final int y = temp[1];
        Rect rect = new Rect(x, y, x + view.getWidth(), y + height);
        boolean ret = rect.contains(rx, ry);
        return ret;
    }

    public interface NotificationCallback extends SwipeHelper.Callback{
        boolean isExpanded();

        void handleChildViewDismissed(View view);

        void onSnooze(StatusBarNotification sbn, SnoozeOption snoozeOption);

        void onDismiss();
    }
}
 No newline at end of file
+511 −0

File added.

Preview size limit exceeded, changes collapsed.