blob: 570132e111ebf3d8885482ee1abe00a3b69b8f01 [file] [log] [blame]
/*
* Copyright (C) 2022 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.dreams.touch;
import static com.android.systemui.dreams.touch.dagger.BouncerSwipeModule.SWIPE_TO_BOUNCER_FLING_ANIMATION_UTILS_CLOSING;
import static com.android.systemui.dreams.touch.dagger.BouncerSwipeModule.SWIPE_TO_BOUNCER_FLING_ANIMATION_UTILS_OPENING;
import static com.android.systemui.dreams.touch.dagger.BouncerSwipeModule.SWIPE_TO_BOUNCER_START_REGION;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.graphics.Rect;
import android.graphics.Region;
import android.util.Log;
import android.view.GestureDetector;
import android.view.InputEvent;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import androidx.annotation.VisibleForTesting;
import com.android.internal.logging.UiEvent;
import com.android.internal.logging.UiEventLogger;
import com.android.systemui.dreams.touch.scrim.ScrimController;
import com.android.systemui.dreams.touch.scrim.ScrimManager;
import com.android.systemui.keyguard.shared.constants.KeyguardBouncerConstants;
import com.android.systemui.shade.ShadeExpansionChangeEvent;
import com.android.systemui.statusbar.NotificationShadeWindowController;
import com.android.systemui.statusbar.phone.CentralSurfaces;
import com.android.wm.shell.animation.FlingAnimationUtils;
import java.util.Optional;
import javax.inject.Inject;
import javax.inject.Named;
/**
* Monitor for tracking touches on the DreamOverlay to bring up the bouncer.
*/
public class BouncerSwipeTouchHandler implements DreamTouchHandler {
/**
* An interface for creating ValueAnimators.
*/
public interface ValueAnimatorCreator {
/**
* Creates {@link ValueAnimator}.
*/
ValueAnimator create(float start, float finish);
}
/**
* An interface for obtaining VelocityTrackers.
*/
public interface VelocityTrackerFactory {
/**
* Obtains {@link VelocityTracker}.
*/
VelocityTracker obtain();
}
public static final float FLING_PERCENTAGE_THRESHOLD = 0.5f;
private static final String TAG = "BouncerSwipeTouchHandler";
private final NotificationShadeWindowController mNotificationShadeWindowController;
private final float mBouncerZoneScreenPercentage;
private final ScrimManager mScrimManager;
private ScrimController mCurrentScrimController;
private float mCurrentExpansion;
private final Optional<CentralSurfaces> mCentralSurfaces;
private VelocityTracker mVelocityTracker;
private final FlingAnimationUtils mFlingAnimationUtils;
private final FlingAnimationUtils mFlingAnimationUtilsClosing;
private Boolean mCapture;
private Boolean mExpanded;
private boolean mBouncerInitiallyShowing;
private TouchSession mTouchSession;
private ValueAnimatorCreator mValueAnimatorCreator;
private VelocityTrackerFactory mVelocityTrackerFactory;
private final UiEventLogger mUiEventLogger;
private final ScrimManager.Callback mScrimManagerCallback = new ScrimManager.Callback() {
@Override
public void onScrimControllerChanged(ScrimController controller) {
if (mCurrentScrimController != null) {
mCurrentScrimController.reset();
}
mCurrentScrimController = controller;
}
};
private final GestureDetector.OnGestureListener mOnGestureListener =
new GestureDetector.SimpleOnGestureListener() {
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX,
float distanceY) {
if (mCapture == null) {
// If the user scrolling favors a vertical direction, begin capturing
// scrolls.
mCapture = Math.abs(distanceY) > Math.abs(distanceX);
mBouncerInitiallyShowing = mCentralSurfaces
.map(CentralSurfaces::isBouncerShowing)
.orElse(false);
if (mCapture) {
// reset expanding
mExpanded = false;
// Since the user is dragging the bouncer up, set scrimmed to false.
mCurrentScrimController.show();
}
}
if (!mCapture) {
return false;
}
// Don't set expansion for downward scroll when the bouncer is hidden.
if (!mBouncerInitiallyShowing && (e1.getY() < e2.getY())) {
return true;
}
// Don't set expansion for upward scroll when the bouncer is shown.
if (mBouncerInitiallyShowing && (e1.getY() > e2.getY())) {
return true;
}
if (!mCentralSurfaces.isPresent()) {
return true;
}
// For consistency, we adopt the expansion definition found in the
// PanelViewController. In this case, expansion refers to the view above the
// bouncer. As that view's expansion shrinks, the bouncer appears. The bouncer
// is fully hidden at full expansion (1) and fully visible when fully collapsed
// (0).
final float dragDownAmount = e2.getY() - e1.getY();
final float screenTravelPercentage = Math.abs(e1.getY() - e2.getY())
/ mTouchSession.getBounds().height();
setPanelExpansion(mBouncerInitiallyShowing
? screenTravelPercentage : 1 - screenTravelPercentage, dragDownAmount);
return true;
}
};
private void setPanelExpansion(float expansion, float dragDownAmount) {
mCurrentExpansion = expansion;
ShadeExpansionChangeEvent event =
new ShadeExpansionChangeEvent(
/* fraction= */ mCurrentExpansion,
/* expanded= */ mExpanded,
/* tracking= */ true,
/* dragDownPxAmount= */ dragDownAmount);
mCurrentScrimController.expand(event);
}
@VisibleForTesting
public enum DreamEvent implements UiEventLogger.UiEventEnum {
@UiEvent(doc = "The screensaver has been swiped up.")
DREAM_SWIPED(988),
@UiEvent(doc = "The bouncer has become fully visible over dream.")
DREAM_BOUNCER_FULLY_VISIBLE(1056);
private final int mId;
DreamEvent(int id) {
mId = id;
}
@Override
public int getId() {
return mId;
}
}
@Inject
public BouncerSwipeTouchHandler(
ScrimManager scrimManager,
Optional<CentralSurfaces> centralSurfaces,
NotificationShadeWindowController notificationShadeWindowController,
ValueAnimatorCreator valueAnimatorCreator,
VelocityTrackerFactory velocityTrackerFactory,
@Named(SWIPE_TO_BOUNCER_FLING_ANIMATION_UTILS_OPENING)
FlingAnimationUtils flingAnimationUtils,
@Named(SWIPE_TO_BOUNCER_FLING_ANIMATION_UTILS_CLOSING)
FlingAnimationUtils flingAnimationUtilsClosing,
@Named(SWIPE_TO_BOUNCER_START_REGION) float swipeRegionPercentage,
UiEventLogger uiEventLogger) {
mCentralSurfaces = centralSurfaces;
mScrimManager = scrimManager;
mNotificationShadeWindowController = notificationShadeWindowController;
mBouncerZoneScreenPercentage = swipeRegionPercentage;
mFlingAnimationUtils = flingAnimationUtils;
mFlingAnimationUtilsClosing = flingAnimationUtilsClosing;
mValueAnimatorCreator = valueAnimatorCreator;
mVelocityTrackerFactory = velocityTrackerFactory;
mUiEventLogger = uiEventLogger;
}
@Override
public void getTouchInitiationRegion(Rect bounds, Region region) {
final int width = bounds.width();
final int height = bounds.height();
if (mCentralSurfaces.map(CentralSurfaces::isBouncerShowing).orElse(false)) {
region.op(new Rect(0, 0, width,
Math.round(
height * mBouncerZoneScreenPercentage)),
Region.Op.UNION);
} else {
region.op(new Rect(0,
Math.round(height * (1 - mBouncerZoneScreenPercentage)),
width,
height),
Region.Op.UNION);
}
}
@Override
public void onSessionStart(TouchSession session) {
mVelocityTracker = mVelocityTrackerFactory.obtain();
mTouchSession = session;
mVelocityTracker.clear();
mNotificationShadeWindowController.setForcePluginOpen(true, this);
mScrimManager.addCallback(mScrimManagerCallback);
mCurrentScrimController = mScrimManager.getCurrentController();
session.registerCallback(() -> {
if (mVelocityTracker != null) {
mVelocityTracker.recycle();
mVelocityTracker = null;
}
mScrimManager.removeCallback(mScrimManagerCallback);
mCapture = null;
mNotificationShadeWindowController.setForcePluginOpen(false, this);
});
session.registerGestureListener(mOnGestureListener);
session.registerInputListener(ev -> onMotionEvent(ev));
}
private void onMotionEvent(InputEvent event) {
if (!(event instanceof MotionEvent)) {
Log.e(TAG, "non MotionEvent received:" + event);
return;
}
final MotionEvent motionEvent = (MotionEvent) event;
switch (motionEvent.getAction()) {
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
mTouchSession.pop();
// If we are not capturing any input, there is no need to consider animating to
// finish transition.
if (mCapture == null || !mCapture) {
break;
}
// We must capture the resulting velocities as resetMonitor() will clear these
// values.
mVelocityTracker.computeCurrentVelocity(1000);
final float verticalVelocity = mVelocityTracker.getYVelocity();
final float horizontalVelocity = mVelocityTracker.getXVelocity();
final float velocityVector =
(float) Math.hypot(horizontalVelocity, verticalVelocity);
mExpanded = !flingRevealsOverlay(verticalVelocity, velocityVector);
final float expansion = mExpanded
? KeyguardBouncerConstants.EXPANSION_VISIBLE
: KeyguardBouncerConstants.EXPANSION_HIDDEN;
// Log the swiping up to show Bouncer event.
if (!mBouncerInitiallyShowing
&& expansion == KeyguardBouncerConstants.EXPANSION_VISIBLE) {
mUiEventLogger.log(DreamEvent.DREAM_SWIPED);
}
flingToExpansion(verticalVelocity, expansion);
break;
default:
mVelocityTracker.addMovement(motionEvent);
break;
}
}
private ValueAnimator createExpansionAnimator(float targetExpansion, float expansionHeight) {
final ValueAnimator animator =
mValueAnimatorCreator.create(mCurrentExpansion, targetExpansion);
animator.addUpdateListener(
animation -> {
float expansionFraction = (float) animation.getAnimatedValue();
float dragDownAmount = expansionFraction * expansionHeight;
setPanelExpansion(expansionFraction, dragDownAmount);
});
if (!mBouncerInitiallyShowing
&& targetExpansion == KeyguardBouncerConstants.EXPANSION_VISIBLE) {
animator.addListener(
new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
mUiEventLogger.log(DreamEvent.DREAM_BOUNCER_FULLY_VISIBLE);
}
});
}
return animator;
}
protected boolean flingRevealsOverlay(float velocity, float velocityVector) {
// Fully expand the space above the bouncer, if the user has expanded the bouncer less
// than halfway or final velocity was positive, indicating a downward direction.
if (Math.abs(velocityVector) < mFlingAnimationUtils.getMinVelocityPxPerSecond()) {
return mCurrentExpansion > FLING_PERCENTAGE_THRESHOLD;
} else {
return velocity > 0;
}
}
protected void flingToExpansion(float velocity, float expansion) {
if (!mCentralSurfaces.isPresent()) {
return;
}
// The animation utils deal in pixel units, rather than expansion height.
final float viewHeight = mTouchSession.getBounds().height();
final float currentHeight = viewHeight * mCurrentExpansion;
final float targetHeight = viewHeight * expansion;
final float expansionHeight = targetHeight - currentHeight;
final ValueAnimator animator = createExpansionAnimator(expansion, expansionHeight);
if (expansion == KeyguardBouncerConstants.EXPANSION_HIDDEN) {
// Hides the bouncer, i.e., fully expands the space above the bouncer.
mFlingAnimationUtilsClosing.apply(animator, currentHeight, targetHeight, velocity,
viewHeight);
} else {
// Shows the bouncer, i.e., fully collapses the space above the bouncer.
mFlingAnimationUtils.apply(
animator, currentHeight, targetHeight, velocity, viewHeight);
}
animator.start();
}
}