Home | History | Annotate | Download | only in phone
      1 /*
      2  * Copyright (C) 2014 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License
     15  */
     16 
     17 package com.android.systemui.statusbar.phone;
     18 
     19 import android.animation.Animator;
     20 import android.animation.AnimatorListenerAdapter;
     21 import android.animation.ValueAnimator;
     22 import android.content.Context;
     23 import android.view.MotionEvent;
     24 import android.view.VelocityTracker;
     25 import android.view.View;
     26 import android.view.ViewConfiguration;
     27 
     28 import com.android.systemui.Interpolators;
     29 import com.android.systemui.R;
     30 import com.android.systemui.classifier.FalsingManager;
     31 import com.android.systemui.statusbar.FlingAnimationUtils;
     32 import com.android.systemui.statusbar.KeyguardAffordanceView;
     33 
     34 /**
     35  * A touch handler of the keyguard which is responsible for launching phone and camera affordances.
     36  */
     37 public class KeyguardAffordanceHelper {
     38 
     39     public static final float SWIPE_RESTING_ALPHA_AMOUNT = 0.5f;
     40     public static final long HINT_PHASE1_DURATION = 200;
     41     private static final long HINT_PHASE2_DURATION = 350;
     42     private static final float BACKGROUND_RADIUS_SCALE_FACTOR = 0.25f;
     43     private static final int HINT_CIRCLE_OPEN_DURATION = 500;
     44 
     45     private final Context mContext;
     46     private final Callback mCallback;
     47 
     48     private FlingAnimationUtils mFlingAnimationUtils;
     49     private VelocityTracker mVelocityTracker;
     50     private boolean mSwipingInProgress;
     51     private float mInitialTouchX;
     52     private float mInitialTouchY;
     53     private float mTranslation;
     54     private float mTranslationOnDown;
     55     private int mTouchSlop;
     56     private int mMinTranslationAmount;
     57     private int mMinFlingVelocity;
     58     private int mHintGrowAmount;
     59     private KeyguardAffordanceView mLeftIcon;
     60     private KeyguardAffordanceView mCenterIcon;
     61     private KeyguardAffordanceView mRightIcon;
     62     private Animator mSwipeAnimator;
     63     private FalsingManager mFalsingManager;
     64     private int mMinBackgroundRadius;
     65     private boolean mMotionCancelled;
     66     private int mTouchTargetSize;
     67     private View mTargetedView;
     68     private boolean mTouchSlopExeeded;
     69     private AnimatorListenerAdapter mFlingEndListener = new AnimatorListenerAdapter() {
     70         @Override
     71         public void onAnimationEnd(Animator animation) {
     72             mSwipeAnimator = null;
     73             mSwipingInProgress = false;
     74             mTargetedView = null;
     75         }
     76     };
     77     private Runnable mAnimationEndRunnable = new Runnable() {
     78         @Override
     79         public void run() {
     80             mCallback.onAnimationToSideEnded();
     81         }
     82     };
     83 
     84     KeyguardAffordanceHelper(Callback callback, Context context) {
     85         mContext = context;
     86         mCallback = callback;
     87         initIcons();
     88         updateIcon(mLeftIcon, 0.0f, mLeftIcon.getRestingAlpha(), false, false, true, false);
     89         updateIcon(mCenterIcon, 0.0f, mCenterIcon.getRestingAlpha(), false, false, true, false);
     90         updateIcon(mRightIcon, 0.0f, mRightIcon.getRestingAlpha(), false, false, true, false);
     91         initDimens();
     92     }
     93 
     94     private void initDimens() {
     95         final ViewConfiguration configuration = ViewConfiguration.get(mContext);
     96         mTouchSlop = configuration.getScaledPagingTouchSlop();
     97         mMinFlingVelocity = configuration.getScaledMinimumFlingVelocity();
     98         mMinTranslationAmount = mContext.getResources().getDimensionPixelSize(
     99                 R.dimen.keyguard_min_swipe_amount);
    100         mMinBackgroundRadius = mContext.getResources().getDimensionPixelSize(
    101                 R.dimen.keyguard_affordance_min_background_radius);
    102         mTouchTargetSize = mContext.getResources().getDimensionPixelSize(
    103                 R.dimen.keyguard_affordance_touch_target_size);
    104         mHintGrowAmount =
    105                 mContext.getResources().getDimensionPixelSize(R.dimen.hint_grow_amount_sideways);
    106         mFlingAnimationUtils = new FlingAnimationUtils(mContext, 0.4f);
    107         mFalsingManager = FalsingManager.getInstance(mContext);
    108     }
    109 
    110     private void initIcons() {
    111         mLeftIcon = mCallback.getLeftIcon();
    112         mCenterIcon = mCallback.getCenterIcon();
    113         mRightIcon = mCallback.getRightIcon();
    114         updatePreviews();
    115     }
    116 
    117     public void updatePreviews() {
    118         mLeftIcon.setPreviewView(mCallback.getLeftPreview());
    119         mRightIcon.setPreviewView(mCallback.getRightPreview());
    120     }
    121 
    122     public boolean onTouchEvent(MotionEvent event) {
    123         int action = event.getActionMasked();
    124         if (mMotionCancelled && action != MotionEvent.ACTION_DOWN) {
    125             return false;
    126         }
    127         final float y = event.getY();
    128         final float x = event.getX();
    129 
    130         boolean isUp = false;
    131         switch (action) {
    132             case MotionEvent.ACTION_DOWN:
    133                 View targetView = getIconAtPosition(x, y);
    134                 if (targetView == null || (mTargetedView != null && mTargetedView != targetView)) {
    135                     mMotionCancelled = true;
    136                     return false;
    137                 }
    138                 if (mTargetedView != null) {
    139                     cancelAnimation();
    140                 } else {
    141                     mTouchSlopExeeded = false;
    142                 }
    143                 startSwiping(targetView);
    144                 mInitialTouchX = x;
    145                 mInitialTouchY = y;
    146                 mTranslationOnDown = mTranslation;
    147                 initVelocityTracker();
    148                 trackMovement(event);
    149                 mMotionCancelled = false;
    150                 break;
    151             case MotionEvent.ACTION_POINTER_DOWN:
    152                 mMotionCancelled = true;
    153                 endMotion(true /* forceSnapBack */, x, y);
    154                 break;
    155             case MotionEvent.ACTION_MOVE:
    156                 trackMovement(event);
    157                 float xDist = x - mInitialTouchX;
    158                 float yDist = y - mInitialTouchY;
    159                 float distance = (float) Math.hypot(xDist, yDist);
    160                 if (!mTouchSlopExeeded && distance > mTouchSlop) {
    161                     mTouchSlopExeeded = true;
    162                 }
    163                 if (mSwipingInProgress) {
    164                     if (mTargetedView == mRightIcon) {
    165                         distance = mTranslationOnDown - distance;
    166                         distance = Math.min(0, distance);
    167                     } else {
    168                         distance = mTranslationOnDown + distance;
    169                         distance = Math.max(0, distance);
    170                     }
    171                     setTranslation(distance, false /* isReset */, false /* animateReset */);
    172                 }
    173                 break;
    174 
    175             case MotionEvent.ACTION_UP:
    176                 isUp = true;
    177             case MotionEvent.ACTION_CANCEL:
    178                 boolean hintOnTheRight = mTargetedView == mRightIcon;
    179                 trackMovement(event);
    180                 endMotion(!isUp, x, y);
    181                 if (!mTouchSlopExeeded && isUp) {
    182                     mCallback.onIconClicked(hintOnTheRight);
    183                 }
    184                 break;
    185         }
    186         return true;
    187     }
    188 
    189     private void startSwiping(View targetView) {
    190         mCallback.onSwipingStarted(targetView == mRightIcon);
    191         mSwipingInProgress = true;
    192         mTargetedView = targetView;
    193     }
    194 
    195     private View getIconAtPosition(float x, float y) {
    196         if (leftSwipePossible() && isOnIcon(mLeftIcon, x, y)) {
    197             return mLeftIcon;
    198         }
    199         if (rightSwipePossible() && isOnIcon(mRightIcon, x, y)) {
    200             return mRightIcon;
    201         }
    202         return null;
    203     }
    204 
    205     public boolean isOnAffordanceIcon(float x, float y) {
    206         return isOnIcon(mLeftIcon, x, y) || isOnIcon(mRightIcon, x, y);
    207     }
    208 
    209     private boolean isOnIcon(View icon, float x, float y) {
    210         float iconX = icon.getX() + icon.getWidth() / 2.0f;
    211         float iconY = icon.getY() + icon.getHeight() / 2.0f;
    212         double distance = Math.hypot(x - iconX, y - iconY);
    213         return distance <= mTouchTargetSize / 2;
    214     }
    215 
    216     private void endMotion(boolean forceSnapBack, float lastX, float lastY) {
    217         if (mSwipingInProgress) {
    218             flingWithCurrentVelocity(forceSnapBack, lastX, lastY);
    219         } else {
    220             mTargetedView = null;
    221         }
    222         if (mVelocityTracker != null) {
    223             mVelocityTracker.recycle();
    224             mVelocityTracker = null;
    225         }
    226     }
    227 
    228     private boolean rightSwipePossible() {
    229         return mRightIcon.getVisibility() == View.VISIBLE;
    230     }
    231 
    232     private boolean leftSwipePossible() {
    233         return mLeftIcon.getVisibility() == View.VISIBLE;
    234     }
    235 
    236     public boolean onInterceptTouchEvent(MotionEvent ev) {
    237         return false;
    238     }
    239 
    240     public void startHintAnimation(boolean right,
    241             Runnable onFinishedListener) {
    242         cancelAnimation();
    243         startHintAnimationPhase1(right, onFinishedListener);
    244     }
    245 
    246     private void startHintAnimationPhase1(final boolean right, final Runnable onFinishedListener) {
    247         final KeyguardAffordanceView targetView = right ? mRightIcon : mLeftIcon;
    248         ValueAnimator animator = getAnimatorToRadius(right, mHintGrowAmount);
    249         animator.addListener(new AnimatorListenerAdapter() {
    250             private boolean mCancelled;
    251 
    252             @Override
    253             public void onAnimationCancel(Animator animation) {
    254                 mCancelled = true;
    255             }
    256 
    257             @Override
    258             public void onAnimationEnd(Animator animation) {
    259                 if (mCancelled) {
    260                     mSwipeAnimator = null;
    261                     mTargetedView = null;
    262                     onFinishedListener.run();
    263                 } else {
    264                     startUnlockHintAnimationPhase2(right, onFinishedListener);
    265                 }
    266             }
    267         });
    268         animator.setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN);
    269         animator.setDuration(HINT_PHASE1_DURATION);
    270         animator.start();
    271         mSwipeAnimator = animator;
    272         mTargetedView = targetView;
    273     }
    274 
    275     /**
    276      * Phase 2: Move back.
    277      */
    278     private void startUnlockHintAnimationPhase2(boolean right, final Runnable onFinishedListener) {
    279         ValueAnimator animator = getAnimatorToRadius(right, 0);
    280         animator.addListener(new AnimatorListenerAdapter() {
    281             @Override
    282             public void onAnimationEnd(Animator animation) {
    283                 mSwipeAnimator = null;
    284                 mTargetedView = null;
    285                 onFinishedListener.run();
    286             }
    287         });
    288         animator.setInterpolator(Interpolators.FAST_OUT_LINEAR_IN);
    289         animator.setDuration(HINT_PHASE2_DURATION);
    290         animator.setStartDelay(HINT_CIRCLE_OPEN_DURATION);
    291         animator.start();
    292         mSwipeAnimator = animator;
    293     }
    294 
    295     private ValueAnimator getAnimatorToRadius(final boolean right, int radius) {
    296         final KeyguardAffordanceView targetView = right ? mRightIcon : mLeftIcon;
    297         ValueAnimator animator = ValueAnimator.ofFloat(targetView.getCircleRadius(), radius);
    298         animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
    299             @Override
    300             public void onAnimationUpdate(ValueAnimator animation) {
    301                 float newRadius = (float) animation.getAnimatedValue();
    302                 targetView.setCircleRadiusWithoutAnimation(newRadius);
    303                 float translation = getTranslationFromRadius(newRadius);
    304                 mTranslation = right ? -translation : translation;
    305                 updateIconsFromTranslation(targetView);
    306             }
    307         });
    308         return animator;
    309     }
    310 
    311     private void cancelAnimation() {
    312         if (mSwipeAnimator != null) {
    313             mSwipeAnimator.cancel();
    314         }
    315     }
    316 
    317     private void flingWithCurrentVelocity(boolean forceSnapBack, float lastX, float lastY) {
    318         float vel = getCurrentVelocity(lastX, lastY);
    319 
    320         // We snap back if the current translation is not far enough
    321         boolean snapBack = false;
    322         if (mCallback.needsAntiFalsing()) {
    323             snapBack = snapBack || mFalsingManager.isFalseTouch();
    324         }
    325         snapBack = snapBack || isBelowFalsingThreshold();
    326 
    327         // or if the velocity is in the opposite direction.
    328         boolean velIsInWrongDirection = vel * mTranslation < 0;
    329         snapBack |= Math.abs(vel) > mMinFlingVelocity && velIsInWrongDirection;
    330         vel = snapBack ^ velIsInWrongDirection ? 0 : vel;
    331         fling(vel, snapBack || forceSnapBack, mTranslation < 0);
    332     }
    333 
    334     private boolean isBelowFalsingThreshold() {
    335         return Math.abs(mTranslation) < Math.abs(mTranslationOnDown) + getMinTranslationAmount();
    336     }
    337 
    338     private int getMinTranslationAmount() {
    339         float factor = mCallback.getAffordanceFalsingFactor();
    340         return (int) (mMinTranslationAmount * factor);
    341     }
    342 
    343     private void fling(float vel, final boolean snapBack, boolean right) {
    344         float target = right ? -mCallback.getMaxTranslationDistance()
    345                 : mCallback.getMaxTranslationDistance();
    346         target = snapBack ? 0 : target;
    347 
    348         ValueAnimator animator = ValueAnimator.ofFloat(mTranslation, target);
    349         mFlingAnimationUtils.apply(animator, mTranslation, target, vel);
    350         animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
    351             @Override
    352             public void onAnimationUpdate(ValueAnimator animation) {
    353                 mTranslation = (float) animation.getAnimatedValue();
    354             }
    355         });
    356         animator.addListener(mFlingEndListener);
    357         if (!snapBack) {
    358             startFinishingCircleAnimation(vel * 0.375f, mAnimationEndRunnable, right);
    359             mCallback.onAnimationToSideStarted(right, mTranslation, vel);
    360         } else {
    361             reset(true);
    362         }
    363         animator.start();
    364         mSwipeAnimator = animator;
    365         if (snapBack) {
    366             mCallback.onSwipingAborted();
    367         }
    368     }
    369 
    370     private void startFinishingCircleAnimation(float velocity, Runnable mAnimationEndRunnable,
    371             boolean right) {
    372         KeyguardAffordanceView targetView = right ? mRightIcon : mLeftIcon;
    373         targetView.finishAnimation(velocity, mAnimationEndRunnable);
    374     }
    375 
    376     private void setTranslation(float translation, boolean isReset, boolean animateReset) {
    377         translation = rightSwipePossible() ? translation : Math.max(0, translation);
    378         translation = leftSwipePossible() ? translation : Math.min(0, translation);
    379         float absTranslation = Math.abs(translation);
    380         if (translation != mTranslation || isReset) {
    381             KeyguardAffordanceView targetView = translation > 0 ? mLeftIcon : mRightIcon;
    382             KeyguardAffordanceView otherView = translation > 0 ? mRightIcon : mLeftIcon;
    383             float alpha = absTranslation / getMinTranslationAmount();
    384 
    385             // We interpolate the alpha of the other icons to 0
    386             float fadeOutAlpha = 1.0f - alpha;
    387             fadeOutAlpha = Math.max(fadeOutAlpha, 0.0f);
    388 
    389             boolean animateIcons = isReset && animateReset;
    390             boolean forceNoCircleAnimation = isReset && !animateReset;
    391             float radius = getRadiusFromTranslation(absTranslation);
    392             boolean slowAnimation = isReset && isBelowFalsingThreshold();
    393             if (!isReset) {
    394                 updateIcon(targetView, radius, alpha + fadeOutAlpha * targetView.getRestingAlpha(),
    395                         false, false, false, false);
    396             } else {
    397                 updateIcon(targetView, 0.0f, fadeOutAlpha * targetView.getRestingAlpha(),
    398                         animateIcons, slowAnimation, false, forceNoCircleAnimation);
    399             }
    400             updateIcon(otherView, 0.0f, fadeOutAlpha * otherView.getRestingAlpha(),
    401                     animateIcons, slowAnimation, false, forceNoCircleAnimation);
    402             updateIcon(mCenterIcon, 0.0f, fadeOutAlpha * mCenterIcon.getRestingAlpha(),
    403                     animateIcons, slowAnimation, false, forceNoCircleAnimation);
    404 
    405             mTranslation = translation;
    406         }
    407     }
    408 
    409     private void updateIconsFromTranslation(KeyguardAffordanceView targetView) {
    410         float absTranslation = Math.abs(mTranslation);
    411         float alpha = absTranslation / getMinTranslationAmount();
    412 
    413         // We interpolate the alpha of the other icons to 0
    414         float fadeOutAlpha =  1.0f - alpha;
    415         fadeOutAlpha = Math.max(0.0f, fadeOutAlpha);
    416 
    417         // We interpolate the alpha of the targetView to 1
    418         KeyguardAffordanceView otherView = targetView == mRightIcon ? mLeftIcon : mRightIcon;
    419         updateIconAlpha(targetView, alpha + fadeOutAlpha * targetView.getRestingAlpha(), false);
    420         updateIconAlpha(otherView, fadeOutAlpha * otherView.getRestingAlpha(), false);
    421         updateIconAlpha(mCenterIcon, fadeOutAlpha * mCenterIcon.getRestingAlpha(), false);
    422     }
    423 
    424     private float getTranslationFromRadius(float circleSize) {
    425         float translation = (circleSize - mMinBackgroundRadius)
    426                 / BACKGROUND_RADIUS_SCALE_FACTOR;
    427         return translation > 0.0f ? translation + mTouchSlop : 0.0f;
    428     }
    429 
    430     private float getRadiusFromTranslation(float translation) {
    431         if (translation <= mTouchSlop) {
    432             return 0.0f;
    433         }
    434         return (translation - mTouchSlop)  * BACKGROUND_RADIUS_SCALE_FACTOR + mMinBackgroundRadius;
    435     }
    436 
    437     public void animateHideLeftRightIcon() {
    438         cancelAnimation();
    439         updateIcon(mRightIcon, 0f, 0f, true, false, false, false);
    440         updateIcon(mLeftIcon, 0f, 0f, true, false, false, false);
    441     }
    442 
    443     private void updateIcon(KeyguardAffordanceView view, float circleRadius, float alpha,
    444                             boolean animate, boolean slowRadiusAnimation, boolean force,
    445                             boolean forceNoCircleAnimation) {
    446         if (view.getVisibility() != View.VISIBLE && !force) {
    447             return;
    448         }
    449         if (forceNoCircleAnimation) {
    450             view.setCircleRadiusWithoutAnimation(circleRadius);
    451         } else {
    452             view.setCircleRadius(circleRadius, slowRadiusAnimation);
    453         }
    454         updateIconAlpha(view, alpha, animate);
    455     }
    456 
    457     private void updateIconAlpha(KeyguardAffordanceView view, float alpha, boolean animate) {
    458         float scale = getScale(alpha, view);
    459         alpha = Math.min(1.0f, alpha);
    460         view.setImageAlpha(alpha, animate);
    461         view.setImageScale(scale, animate);
    462     }
    463 
    464     private float getScale(float alpha, KeyguardAffordanceView icon) {
    465         float scale = alpha / icon.getRestingAlpha() * 0.2f +
    466                 KeyguardAffordanceView.MIN_ICON_SCALE_AMOUNT;
    467         return Math.min(scale, KeyguardAffordanceView.MAX_ICON_SCALE_AMOUNT);
    468     }
    469 
    470     private void trackMovement(MotionEvent event) {
    471         if (mVelocityTracker != null) {
    472             mVelocityTracker.addMovement(event);
    473         }
    474     }
    475 
    476     private void initVelocityTracker() {
    477         if (mVelocityTracker != null) {
    478             mVelocityTracker.recycle();
    479         }
    480         mVelocityTracker = VelocityTracker.obtain();
    481     }
    482 
    483     private float getCurrentVelocity(float lastX, float lastY) {
    484         if (mVelocityTracker == null) {
    485             return 0;
    486         }
    487         mVelocityTracker.computeCurrentVelocity(1000);
    488         float aX = mVelocityTracker.getXVelocity();
    489         float aY = mVelocityTracker.getYVelocity();
    490         float bX = lastX - mInitialTouchX;
    491         float bY = lastY - mInitialTouchY;
    492         float bLen = (float) Math.hypot(bX, bY);
    493         // Project the velocity onto the distance vector: a * b / |b|
    494         float projectedVelocity = (aX * bX + aY * bY) / bLen;
    495         if (mTargetedView == mRightIcon) {
    496             projectedVelocity = -projectedVelocity;
    497         }
    498         return projectedVelocity;
    499     }
    500 
    501     public void onConfigurationChanged() {
    502         initDimens();
    503         initIcons();
    504     }
    505 
    506     public void onRtlPropertiesChanged() {
    507         initIcons();
    508     }
    509 
    510     public void reset(boolean animate) {
    511         cancelAnimation();
    512         setTranslation(0.0f, true, animate);
    513         mMotionCancelled = true;
    514         if (mSwipingInProgress) {
    515             mCallback.onSwipingAborted();
    516             mSwipingInProgress = false;
    517         }
    518     }
    519 
    520     public boolean isSwipingInProgress() {
    521         return mSwipingInProgress;
    522     }
    523 
    524     public void launchAffordance(boolean animate, boolean left) {
    525         if (mSwipingInProgress) {
    526             // We don't want to mess with the state if the user is actually swiping already.
    527             return;
    528         }
    529         KeyguardAffordanceView targetView = left ? mLeftIcon : mRightIcon;
    530         KeyguardAffordanceView otherView = left ? mRightIcon : mLeftIcon;
    531         startSwiping(targetView);
    532         if (animate) {
    533             fling(0, false, !left);
    534             updateIcon(otherView, 0.0f, 0, true, false, true, false);
    535             updateIcon(mCenterIcon, 0.0f, 0, true, false, true, false);
    536         } else {
    537             mCallback.onAnimationToSideStarted(!left, mTranslation, 0);
    538             mTranslation = left ? mCallback.getMaxTranslationDistance()
    539                     : mCallback.getMaxTranslationDistance();
    540             updateIcon(mCenterIcon, 0.0f, 0.0f, false, false, true, false);
    541             updateIcon(otherView, 0.0f, 0.0f, false, false, true, false);
    542             targetView.instantFinishAnimation();
    543             mFlingEndListener.onAnimationEnd(null);
    544             mAnimationEndRunnable.run();
    545         }
    546     }
    547 
    548     public interface Callback {
    549 
    550         /**
    551          * Notifies the callback when an animation to a side page was started.
    552          *
    553          * @param rightPage Is the page animated to the right page?
    554          */
    555         void onAnimationToSideStarted(boolean rightPage, float translation, float vel);
    556 
    557         /**
    558          * Notifies the callback the animation to a side page has ended.
    559          */
    560         void onAnimationToSideEnded();
    561 
    562         float getMaxTranslationDistance();
    563 
    564         void onSwipingStarted(boolean rightIcon);
    565 
    566         void onSwipingAborted();
    567 
    568         void onIconClicked(boolean rightIcon);
    569 
    570         KeyguardAffordanceView getLeftIcon();
    571 
    572         KeyguardAffordanceView getCenterIcon();
    573 
    574         KeyguardAffordanceView getRightIcon();
    575 
    576         View getLeftPreview();
    577 
    578         View getRightPreview();
    579 
    580         /**
    581          * @return The factor the minimum swipe amount should be multiplied with.
    582          */
    583         float getAffordanceFalsingFactor();
    584 
    585         boolean needsAntiFalsing();
    586     }
    587 }
    588