blob: fbca7b1d86d33ab716b8ccd7be34caedafa4a663 [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.DisplayMetrics;
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.statusbar.NotificationShadeWindowController;
import com.android.systemui.statusbar.phone.CentralSurfaces;
import com.android.systemui.statusbar.phone.KeyguardBouncer;
import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
import com.android.systemui.statusbar.phone.panelstate.PanelExpansionChangeEvent;
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 StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
private float mCurrentExpansion;
private final Optional<CentralSurfaces> mCentralSurfaces;
private VelocityTracker mVelocityTracker;
private final FlingAnimationUtils mFlingAnimationUtils;
private final FlingAnimationUtils mFlingAnimationUtilsClosing;
private final DisplayMetrics mDisplayMetrics;
private Boolean mCapture;
private boolean mBouncerInitiallyShowing;
private TouchSession mTouchSession;
private ValueAnimatorCreator mValueAnimatorCreator;
private VelocityTrackerFactory mVelocityTrackerFactory;
private final UiEventLogger mUiEventLogger;
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) {
// Since the user is dragging the bouncer up, set scrimmed to false.
mStatusBarKeyguardViewManager.showBouncer(false);
}
}
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())
/ mCentralSurfaces.get().getDisplayHeight();
setPanelExpansion(mBouncerInitiallyShowing
? screenTravelPercentage : 1 - screenTravelPercentage, dragDownAmount);
return true;
}
};
private void setPanelExpansion(float expansion, float dragDownAmount) {
mCurrentExpansion = expansion;
PanelExpansionChangeEvent event =
new PanelExpansionChangeEvent(
/* fraction= */ mCurrentExpansion,
/* expanded= */ false,
/* tracking= */ true,
/* dragDownPxAmount= */ dragDownAmount);
mStatusBarKeyguardViewManager.onPanelExpansionChanged(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(
DisplayMetrics displayMetrics,
StatusBarKeyguardViewManager statusBarKeyguardViewManager,
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) {
mDisplayMetrics = displayMetrics;
mCentralSurfaces = centralSurfaces;
mStatusBarKeyguardViewManager = statusBarKeyguardViewManager;
mNotificationShadeWindowController = notificationShadeWindowController;
mBouncerZoneScreenPercentage = swipeRegionPercentage;
mFlingAnimationUtils = flingAnimationUtils;
mFlingAnimationUtilsClosing = flingAnimationUtilsClosing;
mValueAnimatorCreator = valueAnimatorCreator;
mVelocityTrackerFactory = velocityTrackerFactory;
mUiEventLogger = uiEventLogger;
}
@Override
public void getTouchInitiationRegion(Region region) {
if (mCentralSurfaces.map(CentralSurfaces::isBouncerShowing).orElse(false)) {
region.op(new Rect(0, 0, mDisplayMetrics.widthPixels,
Math.round(
mDisplayMetrics.heightPixels * mBouncerZoneScreenPercentage)),
Region.Op.UNION);
} else {
region.op(new Rect(0,
Math.round(
mDisplayMetrics.heightPixels
* (1 - mBouncerZoneScreenPercentage)),
mDisplayMetrics.widthPixels,
mDisplayMetrics.heightPixels),
Region.Op.UNION);
}
}
@Override
public void onSessionStart(TouchSession session) {
mVelocityTracker = mVelocityTrackerFactory.obtain();
mTouchSession = session;
mVelocityTracker.clear();
mNotificationShadeWindowController.setForcePluginOpen(true, this);
session.registerCallback(() -> {
mVelocityTracker.recycle();
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);
final float expansion = flingRevealsOverlay(verticalVelocity, velocityVector)
? KeyguardBouncer.EXPANSION_HIDDEN : KeyguardBouncer.EXPANSION_VISIBLE;
// Log the swiping up to show Bouncer event.
if (!mBouncerInitiallyShowing && expansion == KeyguardBouncer.EXPANSION_VISIBLE) {
mUiEventLogger.log(DreamEvent.DREAM_SWIPED);
}
flingToExpansion(verticalVelocity, expansion);
if (expansion == KeyguardBouncer.EXPANSION_HIDDEN) {
mStatusBarKeyguardViewManager.reset(false);
}
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 == KeyguardBouncer.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 = mCentralSurfaces.get().getDisplayHeight();
final float currentHeight = viewHeight * mCurrentExpansion;
final float targetHeight = viewHeight * expansion;
final float expansionHeight = targetHeight - currentHeight;
final ValueAnimator animator = createExpansionAnimator(expansion, expansionHeight);
if (expansion == KeyguardBouncer.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();
}
}