diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java index 7d39c4aaacbfe..dd60647021ffa 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java @@ -23,6 +23,7 @@ import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL; import android.annotation.IntDef; import android.content.Context; import android.content.res.Resources; +import android.os.SystemProperties; import android.view.ViewConfiguration; import android.view.WindowManagerPolicyConstants; @@ -115,6 +116,9 @@ public class QuickStepContract { public static final int SYSUI_STATE_FREEFORM_ACTIVE_IN_DESKTOP_MODE = 1 << 26; // Device dreaming state public static final int SYSUI_STATE_DEVICE_DREAMING = 1 << 27; + // Whether the back gesture is allowed (or ignored) by the Shade + public static final boolean ALLOW_BACK_GESTURE_IN_SHADE = SystemProperties.getBoolean( + "persist.wm.debug.shade_allow_back_gesture", false); @Retention(RetentionPolicy.SOURCE) @IntDef({SYSUI_STATE_SCREEN_PINNING, @@ -243,9 +247,14 @@ public class QuickStepContract { sysuiStateFlags &= ~SYSUI_STATE_NAV_BAR_HIDDEN; } // Disable when in immersive, or the notifications are interactive - int disableFlags = SYSUI_STATE_NAV_BAR_HIDDEN - | SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED - | SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING; + int disableFlags = SYSUI_STATE_NAV_BAR_HIDDEN | SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING; + + // EdgeBackGestureHandler ignores Back gesture when SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED. + // To allow Shade to respond to Back, we're bypassing this check (behind a flag). + if (!ALLOW_BACK_GESTURE_IN_SHADE) { + disableFlags |= SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED; + } + return (sysuiStateFlags & disableFlags) != 0; } diff --git a/packages/SystemUI/src/com/android/systemui/scrim/ScrimDrawable.java b/packages/SystemUI/src/com/android/systemui/scrim/ScrimDrawable.java index b36f0d7bacfc2..10e2afe0baa91 100644 --- a/packages/SystemUI/src/com/android/systemui/scrim/ScrimDrawable.java +++ b/packages/SystemUI/src/com/android/systemui/scrim/ScrimDrawable.java @@ -27,6 +27,7 @@ import android.graphics.Paint; import android.graphics.Path; import android.graphics.PixelFormat; import android.graphics.Rect; +import android.graphics.RectF; import android.graphics.Xfermode; import android.graphics.drawable.Drawable; import android.view.animation.DecelerateInterpolator; @@ -41,7 +42,11 @@ import com.android.systemui.statusbar.notification.stack.StackStateAnimator; public class ScrimDrawable extends Drawable { private static final String TAG = "ScrimDrawable"; + private boolean mShouldUseLargeScreenSize; private final Paint mPaint; + private final Path mPath = new Path(); + private final RectF mBoundsRectF = new RectF(); + private int mAlpha = 255; private int mMainColor; private ValueAnimator mColorAnimation; @@ -49,11 +54,13 @@ public class ScrimDrawable extends Drawable { private float mCornerRadius; private ConcaveInfo mConcaveInfo; private int mBottomEdgePosition; + private float mBottomEdgeRadius = -1; private boolean mCornerRadiusEnabled; public ScrimDrawable() { mPaint = new Paint(); mPaint.setStyle(Paint.Style.FILL); + mShouldUseLargeScreenSize = false; } /** @@ -133,6 +140,10 @@ public class ScrimDrawable extends Drawable { return PixelFormat.TRANSLUCENT; } + public void setShouldUseLargeScreenSize(boolean v) { + mShouldUseLargeScreenSize = v; + } + /** * Corner radius used by either concave or convex corners. */ @@ -191,6 +202,10 @@ public class ScrimDrawable extends Drawable { invalidateSelf(); } + public void setBottomEdgeRadius(float radius) { + mBottomEdgeRadius = radius; + } + @Override public void draw(@NonNull Canvas canvas) { mPaint.setColor(mMainColor); @@ -198,9 +213,46 @@ public class ScrimDrawable extends Drawable { if (mConcaveInfo != null) { drawConcave(canvas); } else if (mCornerRadiusEnabled && mCornerRadius > 0) { - canvas.drawRoundRect(getBounds().left, getBounds().top, getBounds().right, - getBounds().bottom, - /* x radius*/ mCornerRadius, /* y radius*/ mCornerRadius, mPaint); + float topEdgeRadius = mCornerRadius; + float bottomEdgeRadius = mBottomEdgeRadius == -1.0 ? mCornerRadius : mBottomEdgeRadius; + + mBoundsRectF.set(getBounds()); + + // When the back gesture causes the notification scrim to be scaled down, + // this offset "reveals" the rounded bottom edge as it "pulls away". + // We must *not* make this adjustment on largescreen shades (where the corner is sharp). + if (!mShouldUseLargeScreenSize && mBottomEdgeRadius != -1) { + mBoundsRectF.bottom -= bottomEdgeRadius; + } + + // We need a box with rounded corners but its lower corners are not rounded on large + // screen devices in "portrait" orientation. + // Thus, we cannot draw a symmetric rounded rectangle via canvas.drawRoundRect() + // and must build a box with different corner radii at the top and at the bottom. + // Additionally, when the scrim is pushed to the very bottom of the screen, do not draw + // anything (drawing a rounded box with these specifications is not possible). + // TODO(b/271030611) perhaps this could be accomplished via Path.addRoundRect instead? + if (mBoundsRectF.bottom - mBoundsRectF.top > bottomEdgeRadius) { + mPath.reset(); + mPath.moveTo(mBoundsRectF.right, mBoundsRectF.top + topEdgeRadius); + mPath.cubicTo(mBoundsRectF.right, mBoundsRectF.top + topEdgeRadius, + mBoundsRectF.right, mBoundsRectF.top, + mBoundsRectF.right - topEdgeRadius, mBoundsRectF.top); + mPath.lineTo(mBoundsRectF.left + topEdgeRadius, mBoundsRectF.top); + mPath.cubicTo(mBoundsRectF.left + topEdgeRadius, mBoundsRectF.top, + mBoundsRectF.left, mBoundsRectF.top, + mBoundsRectF.left, mBoundsRectF.top + topEdgeRadius); + mPath.lineTo(mBoundsRectF.left, mBoundsRectF.bottom - bottomEdgeRadius); + mPath.cubicTo(mBoundsRectF.left, mBoundsRectF.bottom - bottomEdgeRadius, + mBoundsRectF.left, mBoundsRectF.bottom, + mBoundsRectF.left + bottomEdgeRadius, mBoundsRectF.bottom); + mPath.lineTo(mBoundsRectF.right - bottomEdgeRadius, mBoundsRectF.bottom); + mPath.cubicTo(mBoundsRectF.right - bottomEdgeRadius, mBoundsRectF.bottom, + mBoundsRectF.right, mBoundsRectF.bottom, + mBoundsRectF.right, mBoundsRectF.bottom - bottomEdgeRadius); + mPath.close(); + canvas.drawPath(mPath, mPaint); + } } else { canvas.drawRect(getBounds().left, getBounds().top, getBounds().right, getBounds().bottom, mPaint); diff --git a/packages/SystemUI/src/com/android/systemui/scrim/ScrimView.java b/packages/SystemUI/src/com/android/systemui/scrim/ScrimView.java index f68e0429ef7c7..fc89a9e637ec6 100644 --- a/packages/SystemUI/src/com/android/systemui/scrim/ScrimView.java +++ b/packages/SystemUI/src/com/android/systemui/scrim/ScrimView.java @@ -20,6 +20,7 @@ import static java.lang.Float.isNaN; import android.annotation.NonNull; import android.content.Context; +import android.content.res.Resources; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.PorterDuff; @@ -37,6 +38,7 @@ import androidx.core.graphics.ColorUtils; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.colorextraction.ColorExtractor; +import com.android.systemui.util.LargeScreenUtils; import java.util.concurrent.Executor; @@ -102,6 +104,13 @@ public class ScrimView extends View { @Override protected void onDraw(Canvas canvas) { if (mDrawable.getAlpha() > 0) { + Resources res = getResources(); + // Scrim behind notification shade has sharp (not rounded) corners on large screens + // which scrim itself cannot know, so we set it here. + if (mDrawable instanceof ScrimDrawable) { + ((ScrimDrawable) mDrawable).setShouldUseLargeScreenSize( + LargeScreenUtils.shouldUseLargeScreenShadeHeader(res)); + } mDrawable.draw(canvas); } } @@ -170,6 +179,15 @@ public class ScrimView extends View { }); } + /** + * Set corner radius of the bottom edge of the Notification scrim. + */ + public void setBottomEdgeRadius(float radius) { + if (mDrawable instanceof ScrimDrawable) { + ((ScrimDrawable) mDrawable).setBottomEdgeRadius(radius); + } + } + @VisibleForTesting Drawable getDrawable() { return mDrawable; diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java index b502b4d02e00b..83db424a82d26 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java @@ -293,7 +293,19 @@ public final class NotificationPanelViewController implements Dumpable { * custom clock animation is in use. */ private static final int KEYGUARD_STATUS_VIEW_CUSTOM_CLOCK_MOVE_DURATION = 1000; + /** + * Whether the Shade should animate to reflect Back gesture progress. + * To minimize latency at runtime, we cache this, else we'd be reading it every time + * updateQsExpansion() is called... and it's called very often. + * + * Whenever we change this flag, SysUI is restarted, so it's never going to be "stale". + */ + public final boolean mAnimateBack; + /** + * The minimum scale to "squish" the Shade and associated elements down to, for Back gesture + */ + public static final float SHADE_BACK_ANIM_MIN_SCALE = 0.9f; private final StatusBarTouchableRegionManager mStatusBarTouchableRegionManager; private final Resources mResources; private final KeyguardStateController mKeyguardStateController; @@ -361,6 +373,8 @@ public final class NotificationPanelViewController implements Dumpable { private CentralSurfaces mCentralSurfaces; private HeadsUpManagerPhone mHeadsUpManager; private float mExpandedHeight = 0; + /** The current squish amount for the predictive back animation */ + private float mCurrentBackProgress = 0.0f; private boolean mTracking; private boolean mHintAnimationRunning; private KeyguardBottomAreaView mKeyguardBottomArea; @@ -816,6 +830,7 @@ public final class NotificationPanelViewController implements Dumpable { mShadeHeaderController = shadeHeaderController; mLayoutInflater = layoutInflater; mFeatureFlags = featureFlags; + mAnimateBack = mFeatureFlags.isEnabled(Flags.WM_SHADE_ANIMATE_BACK_GESTURE); mFalsingCollector = falsingCollector; mPowerManager = powerManager; mWakeUpCoordinator = coordinator; @@ -1965,6 +1980,14 @@ public final class NotificationPanelViewController implements Dumpable { if (mFixedDuration != NO_FIXED_DURATION) { animator.setDuration(mFixedDuration); } + + // Reset Predictive Back animation's transform after Shade is completely hidden. + animator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + resetBackTransformation(); + } + }); } animator.addListener(new AnimatorListenerAdapter() { private boolean mCancelled; @@ -2189,6 +2212,53 @@ public final class NotificationPanelViewController implements Dumpable { } } + /** + * When the back gesture triggers a fully-expanded shade --> QQS shade collapse transition, + * the expansionFraction goes down from 1.0 --> 0.0 (collapsing), so the current "squish" amount + * (mCurrentBackProgress) must be un-applied from various UI elements in tandem, such that, + * as the shade ends up in its half-expanded state (with QQS above), it is back at 100% scale. + * Without this, the shade would collapse, and stay squished. + */ + public void adjustBackAnimationScale(float expansionFraction) { + if (expansionFraction > 0.0f) { // collapsing + float animatedFraction = expansionFraction * mCurrentBackProgress; + applyBackScaling(animatedFraction); + } else { + // collapsed! reset, so that if we re-expand shade, it won't start off "squished" + mCurrentBackProgress = 0; + } + } + + //TODO(b/270981268): allow cancelling back animation mid-flight + /** Called when Back gesture has been committed (i.e. a back event has definitely occurred) */ + public void onBackPressed() { + closeQsIfPossible(); + } + /** Sets back progress. */ + public void onBackProgressed(float progressFraction) { + // TODO: non-linearly transform progress fraction into squish amount (ease-in, linear out) + mCurrentBackProgress = progressFraction; + applyBackScaling(progressFraction); + } + + /** Resets back progress. */ + public void resetBackTransformation() { + mCurrentBackProgress = 0.0f; + applyBackScaling(0.0f); + } + + /** Scales multiple elements in tandem to achieve the illusion of the QS+Shade shrinking + * as a single visual element (used by the Predictive Back Gesture preview animation). + * fraction = 0 implies "no scaling", and 1 means "scale down to minimum size (90%)". + */ + public void applyBackScaling(float fraction) { + if (mNotificationContainerParent == null) { + return; + } + float scale = MathUtils.lerp(1.0f, SHADE_BACK_ANIM_MIN_SCALE, fraction); + mNotificationContainerParent.applyBackScaling(scale, mSplitShadeEnabled); + mScrimController.applyBackScaling(scale); + } /** */ public float getLockscreenShadeDragProgress() { // mTransitioningToFullShadeProgress > 0 means we're doing regular lockscreen to shade @@ -4868,6 +4938,11 @@ public final class NotificationPanelViewController implements Dumpable { switch (event.getActionMasked()) { case MotionEvent.ACTION_DOWN: + if (QuickStepContract.ALLOW_BACK_GESTURE_IN_SHADE && mAnimateBack) { + // Cache the gesture insets now, so we can quickly query them during + // ACTION_MOVE and decide whether to intercept events for back gesture anim. + mQsController.updateGestureInsetsCache(); + } mShadeLog.logMotionEvent(event, "onTouch: down action"); startExpandMotion(x, y, false /* startTracking */, mExpandedHeight); mMinExpandHeight = 0.0f; @@ -4917,6 +4992,12 @@ public final class NotificationPanelViewController implements Dumpable { } break; case MotionEvent.ACTION_MOVE: + // If the shade is half-collapsed, a horizontal swipe inwards from L/R edge + // must be routed to the back gesture (which shows a preview animation). + if (QuickStepContract.ALLOW_BACK_GESTURE_IN_SHADE && mAnimateBack + && mQsController.shouldBackBypassQuickSettings(x)) { + return false; + } if (isFullyCollapsed()) { // If panel is fully collapsed, reset haptic effect before adding movement. mHasVibratedOnOpen = false; diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationsQuickSettingsContainer.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationsQuickSettingsContainer.java index f73dde6320519..7dff6ea990298 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationsQuickSettingsContainer.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationsQuickSettingsContainer.java @@ -20,6 +20,7 @@ import android.app.Fragment; import android.content.Context; import android.content.res.Configuration; import android.graphics.Canvas; +import android.graphics.Rect; import android.util.AttributeSet; import android.view.View; import android.view.WindowInsets; @@ -55,6 +56,13 @@ public class NotificationsQuickSettingsContainer extends ConstraintLayout private QS mQs; private View mQSContainer; + /** + * These are used to compute the bounding box containing the shade and the notification scrim, + * which is then used to drive the Back gesture animation. + */ + private final Rect mUpperRect = new Rect(); + private final Rect mBoundingBoxRect = new Rect(); + @Nullable private Consumer mConfigurationChangedListener; @@ -172,4 +180,37 @@ public class NotificationsQuickSettingsContainer extends ConstraintLayout public void applyConstraints(ConstraintSet constraintSet) { constraintSet.applyTo(this); } + + /** + * Scale multiple elements in tandem, for the predictive back animation. + * This is how the Shade responds to the Back gesture (by scaling). + * Without the common center, individual elements will scale about their respective centers. + * Scaling the entire NotificationsQuickSettingsContainer will also resize the shade header + * (which we don't want). + */ + public void applyBackScaling(float scale, boolean usingSplitShade) { + if (mStackScroller == null || mQSContainer == null) { + return; + } + + mQSContainer.getBoundsOnScreen(mUpperRect); + mStackScroller.getBoundsOnScreen(mBoundingBoxRect); + mBoundingBoxRect.union(mUpperRect); + + float cx = mBoundingBoxRect.centerX(); + float cy = mBoundingBoxRect.centerY(); + + mQSContainer.setPivotX(cx); + mQSContainer.setPivotY(cy); + mQSContainer.setScaleX(scale); + mQSContainer.setScaleY(scale); + + // When in large-screen split-shade mode, the notification stack scroller scales correctly + // only if the pivot point is at the left edge of the screen (because of its dimensions). + // When not in large-screen split-shade mode, we can scale correctly via the (cx,cy) above. + mStackScroller.setPivotX(usingSplitShade ? 0.0f : cx); + mStackScroller.setPivotY(cy); + mStackScroller.setScaleX(scale); + mStackScroller.setScaleY(scale); + } } diff --git a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsController.java b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsController.java index 50f3db3eeb029..bbb43899ce646 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsController.java @@ -31,6 +31,7 @@ import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; import android.app.Fragment; import android.content.res.Resources; +import android.graphics.Insets; import android.graphics.Rect; import android.graphics.Region; import android.util.Log; @@ -40,6 +41,9 @@ import android.view.VelocityTracker; import android.view.View; import android.view.ViewConfiguration; import android.view.ViewGroup; +import android.view.WindowInsets; +import android.view.WindowManager; +import android.view.WindowMetrics; import android.view.accessibility.AccessibilityManager; import android.widget.FrameLayout; @@ -63,6 +67,7 @@ import com.android.systemui.plugins.FalsingManager; import com.android.systemui.plugins.qs.QS; import com.android.systemui.screenrecord.RecordingController; import com.android.systemui.shade.transition.ShadeTransitionController; +import com.android.systemui.shared.system.QuickStepContract; import com.android.systemui.statusbar.LockscreenShadeTransitionController; import com.android.systemui.statusbar.NotificationRemoteInputManager; import com.android.systemui.statusbar.NotificationShadeDepthController; @@ -83,10 +88,10 @@ import com.android.systemui.statusbar.phone.dagger.CentralSurfacesComponent; import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.util.LargeScreenUtils; -import javax.inject.Inject; - import dagger.Lazy; +import javax.inject.Inject; + /** Handles QuickSettings touch handling, expansion and animation state * TODO (b/264460656) make this dumpable */ @@ -217,6 +222,13 @@ public class QuickSettingsController { */ private boolean mAnimatorExpand; + /** + * The gesture inset currently in effect -- used to decide whether a back gesture should + * receive a horizontal swipe inwards from the left/right vertical edge of the screen. + * We cache this on ACTION_DOWN, and query it during both ACTION_DOWN and ACTION_MOVE events. + */ + private Insets mCachedGestureInsets; + /** * The amount of progress we are currently in if we're transitioning to the full shade. * 0.0f means we're not transitioning yet, while 1 means we're all the way in the full @@ -401,6 +413,7 @@ public class QuickSettingsController { mQuickQsHeaderHeight = mLargeScreenShadeHeaderHeight; mEnableClipping = mResources.getBoolean(R.bool.qs_enable_clipping); + updateGestureInsetsCache(); } // TODO (b/265054088): move this and others to a CoreStartable @@ -464,6 +477,26 @@ public class QuickSettingsController { || touchX > mQsFrame.getX() + mQsFrame.getWidth(); } + /** + * Computes (and caches) the gesture insets for the current window. Intended to be called + * on ACTION_DOWN, and safely queried repeatedly thereafter during ACTION_MOVE events. + */ + public void updateGestureInsetsCache() { + WindowManager wm = this.mPanelView.getContext().getSystemService(WindowManager.class); + WindowMetrics windowMetrics = wm.getCurrentWindowMetrics(); + mCachedGestureInsets = windowMetrics.getWindowInsets().getInsets( + WindowInsets.Type.systemGestures()); + } + + /** + * Returns whether x coordinate lies in the vertical edges of the screen + * (the only place where a back gesture can be initiated). + */ + public boolean shouldBackBypassQuickSettings(float touchX) { + return (touchX < mCachedGestureInsets.left) + || (touchX > mKeyguardStatusBar.getWidth() - mCachedGestureInsets.right); + } + /** Returns whether touch is within QS area */ private boolean isTouchInQsArea(float x, float y) { if (isSplitShadeAndTouchXOutsideQs(x)) { @@ -912,6 +945,10 @@ public class QuickSettingsController { getHeaderTranslation(), squishiness ); + if (QuickStepContract.ALLOW_BACK_GESTURE_IN_SHADE + && mPanelViewControllerLazy.get().mAnimateBack) { + mPanelViewControllerLazy.get().adjustBackAnimationScale(adjustedExpansionFraction); + } mMediaHierarchyManager.setQsExpansion(qsExpansionFraction); int qsPanelBottomY = calculateBottomPosition(qsExpansionFraction); mScrimController.setQsPosition(qsExpansionFraction, qsPanelBottomY); @@ -1099,6 +1136,7 @@ public class QuickSettingsController { float screenCornerRadius = mRecordingController.isRecording() ? 0 : mScreenCornerRadius; radius = (int) MathUtils.lerp(screenCornerRadius, mScrimCornerRadius, Math.min(top / (float) mScrimCornerRadius, 1f)); + mScrimController.setNotificationBottomRadius(radius); } if (isQsFragmentCreated()) { float qsTranslation = 0; @@ -1491,18 +1529,31 @@ public class QuickSettingsController { } private void handleDown(MotionEvent event) { - if (event.getActionMasked() == MotionEvent.ACTION_DOWN - && shouldQuickSettingsIntercept(event.getX(), event.getY(), -1)) { - mFalsingCollector.onQsDown(); - mShadeLog.logMotionEvent(event, "handleQsDown: down action, QS tracking enabled"); - mTracking = true; - onExpansionStarted(); - mInitialHeightOnTouch = mExpansionHeight; - mInitialTouchY = event.getY(); - mInitialTouchX = event.getX(); - // TODO (b/265193930): remove dependency on NPVC - // If we interrupt an expansion gesture here, make sure to update the state correctly. - mPanelViewControllerLazy.get().notifyExpandingFinished(); + if (event.getActionMasked() == MotionEvent.ACTION_DOWN) { + // When the shade is fully-expanded, an inward swipe from the L/R edge should first + // allow the back gesture's animation to preview the shade animation (if enabled). + // (swipes starting closer to the center of the screen will not be affected) + if (QuickStepContract.ALLOW_BACK_GESTURE_IN_SHADE + && mPanelViewControllerLazy.get().mAnimateBack) { + updateGestureInsetsCache(); + if (shouldBackBypassQuickSettings(event.getX())) { + return; + } + } + if (shouldQuickSettingsIntercept(event.getX(), event.getY(), -1)) { + mFalsingCollector.onQsDown(); + mShadeLog.logMotionEvent(event, + "handleQsDown: down action, QS tracking enabled"); + mTracking = true; + onExpansionStarted(); + mInitialHeightOnTouch = mExpansionHeight; + mInitialTouchY = event.getY(); + mInitialTouchX = event.getX(); + // TODO (b/265193930): remove dependency on NPVC + // If we interrupt an expansion gesture here, make sure to update the state + // correctly. + mPanelViewControllerLazy.get().notifyExpandingFinished(); + } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java index 17fb05547e00e..ca68d2a069332 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java @@ -104,6 +104,8 @@ import android.view.WindowManager; import android.view.WindowManagerGlobal; import android.view.accessibility.AccessibilityManager; import android.widget.DateTimeView; +import android.window.BackEvent; +import android.window.OnBackAnimationCallback; import android.window.OnBackInvokedCallback; import android.window.OnBackInvokedDispatcher; @@ -507,6 +509,7 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { protected final NotificationInterruptStateProvider mNotificationInterruptStateProvider; private final BrightnessSliderController.Factory mBrightnessSliderFactory; private final FeatureFlags mFeatureFlags; + private final boolean mAnimateBack; private final FragmentService mFragmentService; private final ScreenOffAnimationController mScreenOffAnimationController; private final WallpaperController mWallpaperController; @@ -653,6 +656,7 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { private final InteractionJankMonitor mJankMonitor; + /** Existing callback that handles back gesture invoked for the Shade. */ private final OnBackInvokedCallback mOnBackInvokedCallback = () -> { if (DEBUG) { Log.d(TAG, "mOnBackInvokedCallback() called"); @@ -660,6 +664,33 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { onBackPressed(); }; + private boolean shouldBackBeHandled() { + return (mState != StatusBarState.KEYGUARD && mState != StatusBarState.SHADE_LOCKED + && !isBouncerShowingOverDream()); + } + + /** + * New callback that handles back gesture invoked, cancel, progress + * and provides feedback via Shade animation. + * (enabled via the WM_SHADE_ANIMATE_BACK_GESTURE flag) + */ + private final OnBackAnimationCallback mOnBackAnimationCallback = new OnBackAnimationCallback() { + @Override + public void onBackInvoked() { + onBackPressed(); + } + + @Override + public void onBackProgressed(BackEvent event) { + if (shouldBackBeHandled()) { + if (mNotificationPanelViewController.canPanelBeCollapsed()) { + float fraction = event.getProgress(); + mNotificationPanelViewController.onBackProgressed(fraction); + } + } + } + }; + /** * Public constructor for CentralSurfaces. * @@ -882,6 +913,8 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { if (mFeatureFlags.isEnabled(Flags.WM_ENABLE_PREDICTIVE_BACK_SYSUI)) { mContext.getApplicationInfo().setEnableOnBackInvokedCallback(true); } + // Based on teamfood flag, enable predictive back animation for the Shade. + mAnimateBack = mFeatureFlags.isEnabled(Flags.WM_SHADE_ANIMATE_BACK_GESTURE); } @Override @@ -2699,7 +2732,8 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { if (viewRootImpl != null) { viewRootImpl.getOnBackInvokedDispatcher() .registerOnBackInvokedCallback(OnBackInvokedDispatcher.PRIORITY_DEFAULT, - mOnBackInvokedCallback); + mAnimateBack ? mOnBackAnimationCallback + : mOnBackInvokedCallback); mIsBackCallbackRegistered = true; if (DEBUG) Log.d(TAG, "is now VISIBLE to user AND callback registered"); } @@ -2714,7 +2748,9 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { ViewRootImpl viewRootImpl = getViewRootImpl(); if (viewRootImpl != null) { viewRootImpl.getOnBackInvokedDispatcher() - .unregisterOnBackInvokedCallback(mOnBackInvokedCallback); + .unregisterOnBackInvokedCallback( + mAnimateBack ? mOnBackAnimationCallback + : mOnBackInvokedCallback); mIsBackCallbackRegistered = false; if (DEBUG) Log.d(TAG, "is NOT VISIBLE to user, AND callback unregistered"); } @@ -3256,9 +3292,10 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { if (mNotificationPanelViewController.closeUserSwitcherIfOpen()) { return true; } - if (mState != StatusBarState.KEYGUARD && mState != StatusBarState.SHADE_LOCKED - && !isBouncerShowingOverDream()) { + if (shouldBackBeHandled()) { if (mNotificationPanelViewController.canPanelBeCollapsed()) { + // this is the Shade dismiss animation, so make sure QQS closes when it ends. + mNotificationPanelViewController.onBackPressed(); mShadeController.animateCollapseShade(); } return true; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java index 80093a3da3255..87dd30798e6bc 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java @@ -359,9 +359,16 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump } } - /** - * Sets corner radius of scrims. - */ + // TODO(b/270984686) recompute scrim height accurately, based on shade contents. + /** Set corner radius of the bottom edge of the Notification scrim. */ + public void setNotificationBottomRadius(float radius) { + if (mNotificationsScrim == null) { + return; + } + mNotificationsScrim.setBottomEdgeRadius(radius); + } + + /** Sets corner radius of scrims. */ public void setScrimCornerRadius(int radius) { if (mScrimBehind == null || mNotificationsScrim == null) { return; @@ -525,6 +532,12 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump scheduleUpdate(); } + /** This is used by the predictive back gesture animation to scale the Shade. */ + public void applyBackScaling(float scale) { + mNotificationsScrim.setScaleX(scale); + mNotificationsScrim.setScaleY(scale); + } + public void onTrackingStarted() { mDarkenWhileDragging = !mKeyguardStateController.canDismissLockScreen(); if (!mKeyguardUnlockAnimationController.isPlayingCannedUnlockAnimation()) { diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java index 8f6653f1d6019..4dd86c3583486 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java @@ -619,6 +619,25 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase { mMainHandler.removeCallbacksAndMessages(null); } + /** + * When the Back gesture starts (progress 0%), the scrim will stay at 100% scale (1.0f). + */ + @Test + public void testBackGesture_min_scrimAtMaxScale() { + mNotificationPanelViewController.onBackProgressed(0.0f); + verify(mScrimController).applyBackScaling(1.0f); + } + + /** + * When the Back gesture is at max (progress 100%), the scrim will be scaled to its minimum. + */ + @Test + public void testBackGesture_max_scrimAtMinScale() { + mNotificationPanelViewController.onBackProgressed(1.0f); + verify(mScrimController).applyBackScaling( + NotificationPanelViewController.SHADE_BACK_ANIM_MIN_SCALE); + } + @Test public void onNotificationHeightChangeWhileOnKeyguardWillComputeMaxKeyguardNotifications() { mStatusBarStateController.setState(KEYGUARD); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java index 38e82281856f3..bc9e7a15bc4b0 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java @@ -77,6 +77,8 @@ import android.view.ViewGroup; import android.view.ViewGroup.LayoutParams; import android.view.ViewRootImpl; import android.view.WindowManager; +import android.window.BackEvent; +import android.window.OnBackAnimationCallback; import android.window.OnBackInvokedCallback; import android.window.OnBackInvokedDispatcher; import android.window.WindowOnBackInvokedDispatcher; @@ -181,6 +183,8 @@ import com.android.systemui.volume.VolumeComponent; import com.android.wm.shell.bubbles.Bubbles; import com.android.wm.shell.startingsurface.StartingSurface; +import dagger.Lazy; + import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -193,8 +197,6 @@ import java.io.ByteArrayOutputStream; import java.io.PrintWriter; import java.util.Optional; -import dagger.Lazy; - @SmallTest @RunWith(AndroidTestingRunner.class) @RunWithLooper(setAsMainLooper = true) @@ -331,6 +333,10 @@ public class CentralSurfacesImplTest extends SysuiTestCase { mFeatureFlags.set(Flags.WM_ENABLE_PREDICTIVE_BACK_SYSUI, false); // Set default value to avoid IllegalStateException. mFeatureFlags.set(Flags.SHORTCUT_LIST_SEARCH_LAYOUT, false); + // For the Shade to respond to Back gesture, we must enable the event routing + mFeatureFlags.set(Flags.WM_SHADE_ALLOW_BACK_GESTURE, true); + // For the Shade to animate during the Back gesture, we must enable the animation flag. + mFeatureFlags.set(Flags.WM_SHADE_ANIMATE_BACK_GESTURE, true); IThermalService thermalService = mock(IThermalService.class); mPowerManager = new PowerManager(mContext, mPowerManagerService, thermalService, @@ -852,6 +858,50 @@ public class CentralSurfacesImplTest extends SysuiTestCase { verify(mShadeController).animateCollapseShade(); } + /** + * When back progress is at 100%, the onBackProgressed animation driver inside + * NotificationPanelViewController should be invoked appropriately (with 1.0f passed in). + */ + @Test + public void testPredictiveBackAnimation_progressMaxScalesPanel() { + mCentralSurfaces.setNotificationShadeWindowViewController( + mNotificationShadeWindowViewController); + mCentralSurfaces.handleVisibleToUserChanged(true); + verify(mOnBackInvokedDispatcher).registerOnBackInvokedCallback( + eq(OnBackInvokedDispatcher.PRIORITY_DEFAULT), + mOnBackInvokedCallback.capture()); + + OnBackAnimationCallback onBackAnimationCallback = + (OnBackAnimationCallback) (mOnBackInvokedCallback.getValue()); + when(mNotificationPanelViewController.canPanelBeCollapsed()).thenReturn(true); + + BackEvent fakeSwipeInFromLeftEdge = new BackEvent(20.0f, 100.0f, 1.0f, BackEvent.EDGE_LEFT); + onBackAnimationCallback.onBackProgressed(fakeSwipeInFromLeftEdge); + verify(mNotificationPanelViewController).onBackProgressed(eq(1.0f)); + } + + /** + * When back progress is at 0%, the onBackProgressed animation driver inside + * NotificationPanelViewController should be invoked appropriately (with 0.0f passed in). + */ + @Test + public void testPredictiveBackAnimation_progressMinScalesPanel() { + mCentralSurfaces.setNotificationShadeWindowViewController( + mNotificationShadeWindowViewController); + mCentralSurfaces.handleVisibleToUserChanged(true); + verify(mOnBackInvokedDispatcher).registerOnBackInvokedCallback( + eq(OnBackInvokedDispatcher.PRIORITY_DEFAULT), + mOnBackInvokedCallback.capture()); + + OnBackAnimationCallback onBackAnimationCallback = + (OnBackAnimationCallback) (mOnBackInvokedCallback.getValue()); + when(mNotificationPanelViewController.canPanelBeCollapsed()).thenReturn(true); + + BackEvent fakeSwipeInFromLeftEdge = new BackEvent(20.0f, 10.0f, 0.0f, BackEvent.EDGE_LEFT); + onBackAnimationCallback.onBackProgressed(fakeSwipeInFromLeftEdge); + verify(mNotificationPanelViewController).onBackProgressed(eq(0.0f)); + } + @Test public void testPanelOpenForHeadsUp() { when(mDeviceProvisionedController.isDeviceProvisioned()).thenReturn(true);