Home | History | Annotate | Download | only in keyguard
      1 /*
      2  * Copyright (C) 2012 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 package com.android.keyguard;
     17 
     18 import static com.android.internal.util.LatencyTracker.ACTION_CHECK_CREDENTIAL;
     19 import static com.android.internal.util.LatencyTracker.ACTION_CHECK_CREDENTIAL_UNLOCKED;
     20 
     21 import android.content.Context;
     22 import android.content.res.ColorStateList;
     23 import android.graphics.Rect;
     24 import android.os.AsyncTask;
     25 import android.os.CountDownTimer;
     26 import android.os.SystemClock;
     27 import android.text.TextUtils;
     28 import android.util.AttributeSet;
     29 import android.util.Log;
     30 import android.view.MotionEvent;
     31 import android.view.View;
     32 import android.view.ViewGroup;
     33 import android.view.animation.AnimationUtils;
     34 import android.view.animation.Interpolator;
     35 import android.widget.LinearLayout;
     36 
     37 import com.android.internal.annotations.VisibleForTesting;
     38 import com.android.internal.util.LatencyTracker;
     39 import com.android.internal.widget.LockPatternChecker;
     40 import com.android.internal.widget.LockPatternUtils;
     41 import com.android.internal.widget.LockPatternView;
     42 import com.android.settingslib.animation.AppearAnimationCreator;
     43 import com.android.settingslib.animation.AppearAnimationUtils;
     44 import com.android.settingslib.animation.DisappearAnimationUtils;
     45 
     46 import java.util.List;
     47 
     48 public class KeyguardPatternView extends LinearLayout implements KeyguardSecurityView,
     49         AppearAnimationCreator<LockPatternView.CellState>,
     50         EmergencyButton.EmergencyButtonCallback {
     51 
     52     private static final String TAG = "SecurityPatternView";
     53     private static final boolean DEBUG = KeyguardConstants.DEBUG;
     54 
     55     // how long before we clear the wrong pattern
     56     private static final int PATTERN_CLEAR_TIMEOUT_MS = 2000;
     57 
     58     // how long we stay awake after each key beyond MIN_PATTERN_BEFORE_POKE_WAKELOCK
     59     private static final int UNLOCK_PATTERN_WAKE_INTERVAL_MS = 7000;
     60 
     61     // how many cells the user has to cross before we poke the wakelock
     62     private static final int MIN_PATTERN_BEFORE_POKE_WAKELOCK = 2;
     63 
     64     // How much we scale up the duration of the disappear animation when the current user is locked
     65     public static final float DISAPPEAR_MULTIPLIER_LOCKED = 1.5f;
     66 
     67     // Extra padding, in pixels, that should eat touch events.
     68     private static final int PATTERNS_TOUCH_AREA_EXTENSION = 40;
     69 
     70     private final KeyguardUpdateMonitor mKeyguardUpdateMonitor;
     71     private final AppearAnimationUtils mAppearAnimationUtils;
     72     private final DisappearAnimationUtils mDisappearAnimationUtils;
     73     private final DisappearAnimationUtils mDisappearAnimationUtilsLocked;
     74     private final int[] mTmpPosition = new int[2];
     75     private final Rect mTempRect = new Rect();
     76     private final Rect mLockPatternScreenBounds = new Rect();
     77 
     78     private CountDownTimer mCountdownTimer = null;
     79     private LockPatternUtils mLockPatternUtils;
     80     private AsyncTask<?, ?, ?> mPendingLockCheck;
     81     private LockPatternView mLockPatternView;
     82     private KeyguardSecurityCallback mCallback;
     83 
     84     /**
     85      * Keeps track of the last time we poked the wake lock during dispatching of the touch event.
     86      * Initialized to something guaranteed to make us poke the wakelock when the user starts
     87      * drawing the pattern.
     88      * @see #dispatchTouchEvent(android.view.MotionEvent)
     89      */
     90     private long mLastPokeTime = -UNLOCK_PATTERN_WAKE_INTERVAL_MS;
     91 
     92     /**
     93      * Useful for clearing out the wrong pattern after a delay
     94      */
     95     private Runnable mCancelPatternRunnable = new Runnable() {
     96         @Override
     97         public void run() {
     98             mLockPatternView.clearPattern();
     99         }
    100     };
    101     @VisibleForTesting
    102     KeyguardMessageArea mSecurityMessageDisplay;
    103     private View mEcaView;
    104     private ViewGroup mContainer;
    105     private int mDisappearYTranslation;
    106 
    107     enum FooterMode {
    108         Normal,
    109         ForgotLockPattern,
    110         VerifyUnlocked
    111     }
    112 
    113     public KeyguardPatternView(Context context) {
    114         this(context, null);
    115     }
    116 
    117     public KeyguardPatternView(Context context, AttributeSet attrs) {
    118         super(context, attrs);
    119         mKeyguardUpdateMonitor = KeyguardUpdateMonitor.getInstance(mContext);
    120         mAppearAnimationUtils = new AppearAnimationUtils(context,
    121                 AppearAnimationUtils.DEFAULT_APPEAR_DURATION, 1.5f /* translationScale */,
    122                 2.0f /* delayScale */, AnimationUtils.loadInterpolator(
    123                         mContext, android.R.interpolator.linear_out_slow_in));
    124         mDisappearAnimationUtils = new DisappearAnimationUtils(context,
    125                 125, 1.2f /* translationScale */,
    126                 0.6f /* delayScale */, AnimationUtils.loadInterpolator(
    127                         mContext, android.R.interpolator.fast_out_linear_in));
    128         mDisappearAnimationUtilsLocked = new DisappearAnimationUtils(context,
    129                 (long) (125 * DISAPPEAR_MULTIPLIER_LOCKED), 1.2f /* translationScale */,
    130                 0.6f /* delayScale */, AnimationUtils.loadInterpolator(
    131                 mContext, android.R.interpolator.fast_out_linear_in));
    132         mDisappearYTranslation = getResources().getDimensionPixelSize(
    133                 R.dimen.disappear_y_translation);
    134     }
    135 
    136     @Override
    137     public void setKeyguardCallback(KeyguardSecurityCallback callback) {
    138         mCallback = callback;
    139     }
    140 
    141     @Override
    142     public void setLockPatternUtils(LockPatternUtils utils) {
    143         mLockPatternUtils = utils;
    144     }
    145 
    146     @Override
    147     protected void onFinishInflate() {
    148         super.onFinishInflate();
    149         mLockPatternUtils = mLockPatternUtils == null
    150                 ? new LockPatternUtils(mContext) : mLockPatternUtils;
    151 
    152         mLockPatternView = findViewById(R.id.lockPatternView);
    153         mLockPatternView.setSaveEnabled(false);
    154         mLockPatternView.setOnPatternListener(new UnlockPatternListener());
    155         mLockPatternView.setInStealthMode(!mLockPatternUtils.isVisiblePatternEnabled(
    156                 KeyguardUpdateMonitor.getCurrentUser()));
    157 
    158         // vibrate mode will be the same for the life of this screen
    159         mLockPatternView.setTactileFeedbackEnabled(mLockPatternUtils.isTactileFeedbackEnabled());
    160 
    161         mEcaView = findViewById(R.id.keyguard_selector_fade_container);
    162         mContainer = findViewById(R.id.container);
    163 
    164         EmergencyButton button = findViewById(R.id.emergency_call_button);
    165         if (button != null) {
    166             button.setCallback(this);
    167         }
    168 
    169         View cancelBtn = findViewById(R.id.cancel_button);
    170         if (cancelBtn != null) {
    171             cancelBtn.setOnClickListener(view -> {
    172                 mCallback.reset();
    173                 mCallback.onCancelClicked();
    174             });
    175         }
    176     }
    177 
    178     @Override
    179     protected void onAttachedToWindow() {
    180         super.onAttachedToWindow();
    181         mSecurityMessageDisplay = KeyguardMessageArea.findSecurityMessageDisplay(this);
    182     }
    183 
    184     @Override
    185     public void onEmergencyButtonClickedWhenInCall() {
    186         mCallback.reset();
    187     }
    188 
    189     @Override
    190     public boolean onTouchEvent(MotionEvent ev) {
    191         boolean result = super.onTouchEvent(ev);
    192         // as long as the user is entering a pattern (i.e sending a touch event that was handled
    193         // by this screen), keep poking the wake lock so that the screen will stay on.
    194         final long elapsed = SystemClock.elapsedRealtime() - mLastPokeTime;
    195         if (result && (elapsed > (UNLOCK_PATTERN_WAKE_INTERVAL_MS - 100))) {
    196             mLastPokeTime = SystemClock.elapsedRealtime();
    197         }
    198         mTempRect.set(0, 0, 0, 0);
    199         offsetRectIntoDescendantCoords(mLockPatternView, mTempRect);
    200         ev.offsetLocation(mTempRect.left, mTempRect.top);
    201         result = mLockPatternView.dispatchTouchEvent(ev) || result;
    202         ev.offsetLocation(-mTempRect.left, -mTempRect.top);
    203         return result;
    204     }
    205 
    206     @Override
    207     protected void onLayout(boolean changed, int l, int t, int r, int b) {
    208         super.onLayout(changed, l, t, r, b);
    209         mLockPatternView.getLocationOnScreen(mTmpPosition);
    210         mLockPatternScreenBounds.set(mTmpPosition[0] - PATTERNS_TOUCH_AREA_EXTENSION,
    211                 mTmpPosition[1] - PATTERNS_TOUCH_AREA_EXTENSION,
    212                 mTmpPosition[0] + mLockPatternView.getWidth() + PATTERNS_TOUCH_AREA_EXTENSION,
    213                 mTmpPosition[1] + mLockPatternView.getHeight() + PATTERNS_TOUCH_AREA_EXTENSION);
    214     }
    215 
    216     @Override
    217     public void reset() {
    218         // reset lock pattern
    219         mLockPatternView.setInStealthMode(!mLockPatternUtils.isVisiblePatternEnabled(
    220                 KeyguardUpdateMonitor.getCurrentUser()));
    221         mLockPatternView.enableInput();
    222         mLockPatternView.setEnabled(true);
    223         mLockPatternView.clearPattern();
    224 
    225         if (mSecurityMessageDisplay == null) {
    226             return;
    227         }
    228 
    229         // if the user is currently locked out, enforce it.
    230         long deadline = mLockPatternUtils.getLockoutAttemptDeadline(
    231                 KeyguardUpdateMonitor.getCurrentUser());
    232         if (deadline != 0) {
    233             handleAttemptLockout(deadline);
    234         } else {
    235             displayDefaultSecurityMessage();
    236         }
    237     }
    238 
    239     private void displayDefaultSecurityMessage() {
    240         if (mSecurityMessageDisplay != null) {
    241             mSecurityMessageDisplay.setMessage("");
    242         }
    243     }
    244 
    245     @Override
    246     public void showUsabilityHint() {
    247     }
    248 
    249     @Override
    250     public boolean disallowInterceptTouch(MotionEvent event) {
    251         return mLockPatternScreenBounds.contains((int) event.getRawX(), (int) event.getRawY());
    252     }
    253 
    254     /** TODO: hook this up */
    255     public void cleanUp() {
    256         if (DEBUG) Log.v(TAG, "Cleanup() called on " + this);
    257         mLockPatternUtils = null;
    258         mLockPatternView.setOnPatternListener(null);
    259     }
    260 
    261     private class UnlockPatternListener implements LockPatternView.OnPatternListener {
    262 
    263         @Override
    264         public void onPatternStart() {
    265             mLockPatternView.removeCallbacks(mCancelPatternRunnable);
    266             mSecurityMessageDisplay.setMessage("");
    267         }
    268 
    269         @Override
    270         public void onPatternCleared() {
    271         }
    272 
    273         @Override
    274         public void onPatternCellAdded(List<LockPatternView.Cell> pattern) {
    275             mCallback.userActivity();
    276         }
    277 
    278         @Override
    279         public void onPatternDetected(final List<LockPatternView.Cell> pattern) {
    280             mLockPatternView.disableInput();
    281             if (mPendingLockCheck != null) {
    282                 mPendingLockCheck.cancel(false);
    283             }
    284 
    285             final int userId = KeyguardUpdateMonitor.getCurrentUser();
    286             if (pattern.size() < LockPatternUtils.MIN_PATTERN_REGISTER_FAIL) {
    287                 mLockPatternView.enableInput();
    288                 onPatternChecked(userId, false, 0, false /* not valid - too short */);
    289                 return;
    290             }
    291 
    292             if (LatencyTracker.isEnabled(mContext)) {
    293                 LatencyTracker.getInstance(mContext).onActionStart(ACTION_CHECK_CREDENTIAL);
    294                 LatencyTracker.getInstance(mContext).onActionStart(ACTION_CHECK_CREDENTIAL_UNLOCKED);
    295             }
    296             mPendingLockCheck = LockPatternChecker.checkPattern(
    297                     mLockPatternUtils,
    298                     pattern,
    299                     userId,
    300                     new LockPatternChecker.OnCheckCallback() {
    301 
    302                         @Override
    303                         public void onEarlyMatched() {
    304                             if (LatencyTracker.isEnabled(mContext)) {
    305                                 LatencyTracker.getInstance(mContext).onActionEnd(
    306                                         ACTION_CHECK_CREDENTIAL);
    307                             }
    308                             onPatternChecked(userId, true /* matched */, 0 /* timeoutMs */,
    309                                     true /* isValidPattern */);
    310                         }
    311 
    312                         @Override
    313                         public void onChecked(boolean matched, int timeoutMs) {
    314                             if (LatencyTracker.isEnabled(mContext)) {
    315                                 LatencyTracker.getInstance(mContext).onActionEnd(
    316                                         ACTION_CHECK_CREDENTIAL_UNLOCKED);
    317                             }
    318                             mLockPatternView.enableInput();
    319                             mPendingLockCheck = null;
    320                             if (!matched) {
    321                                 onPatternChecked(userId, false /* matched */, timeoutMs,
    322                                         true /* isValidPattern */);
    323                             }
    324                         }
    325 
    326                         @Override
    327                         public void onCancelled() {
    328                             // We already got dismissed with the early matched callback, so we
    329                             // cancelled the check. However, we still need to note down the latency.
    330                             if (LatencyTracker.isEnabled(mContext)) {
    331                                 LatencyTracker.getInstance(mContext).onActionEnd(
    332                                         ACTION_CHECK_CREDENTIAL_UNLOCKED);
    333                             }
    334                         }
    335                     });
    336             if (pattern.size() > MIN_PATTERN_BEFORE_POKE_WAKELOCK) {
    337                 mCallback.userActivity();
    338             }
    339         }
    340 
    341         private void onPatternChecked(int userId, boolean matched, int timeoutMs,
    342                 boolean isValidPattern) {
    343             boolean dismissKeyguard = KeyguardUpdateMonitor.getCurrentUser() == userId;
    344             if (matched) {
    345                 mCallback.reportUnlockAttempt(userId, true, 0);
    346                 if (dismissKeyguard) {
    347                     mLockPatternView.setDisplayMode(LockPatternView.DisplayMode.Correct);
    348                     mCallback.dismiss(true, userId);
    349                 }
    350             } else {
    351                 mLockPatternView.setDisplayMode(LockPatternView.DisplayMode.Wrong);
    352                 if (isValidPattern) {
    353                     mCallback.reportUnlockAttempt(userId, false, timeoutMs);
    354                     if (timeoutMs > 0) {
    355                         long deadline = mLockPatternUtils.setLockoutAttemptDeadline(
    356                                 userId, timeoutMs);
    357                         handleAttemptLockout(deadline);
    358                     }
    359                 }
    360                 if (timeoutMs == 0) {
    361                     mSecurityMessageDisplay.setMessage(R.string.kg_wrong_pattern);
    362                     mLockPatternView.postDelayed(mCancelPatternRunnable, PATTERN_CLEAR_TIMEOUT_MS);
    363                 }
    364             }
    365         }
    366     }
    367 
    368     private void handleAttemptLockout(long elapsedRealtimeDeadline) {
    369         mLockPatternView.clearPattern();
    370         mLockPatternView.setEnabled(false);
    371         final long elapsedRealtime = SystemClock.elapsedRealtime();
    372         final long secondsInFuture = (long) Math.ceil(
    373                 (elapsedRealtimeDeadline - elapsedRealtime) / 1000.0);
    374         mCountdownTimer = new CountDownTimer(secondsInFuture * 1000, 1000) {
    375 
    376             @Override
    377             public void onTick(long millisUntilFinished) {
    378                 final int secondsRemaining = (int) Math.round(millisUntilFinished / 1000.0);
    379                 mSecurityMessageDisplay.setMessage(mContext.getResources().getQuantityString(
    380                                 R.plurals.kg_too_many_failed_attempts_countdown,
    381                                 secondsRemaining, secondsRemaining));
    382             }
    383 
    384             @Override
    385             public void onFinish() {
    386                 mLockPatternView.setEnabled(true);
    387                 displayDefaultSecurityMessage();
    388             }
    389 
    390         }.start();
    391     }
    392 
    393     @Override
    394     public boolean needsInput() {
    395         return false;
    396     }
    397 
    398     @Override
    399     public void onPause() {
    400         if (mCountdownTimer != null) {
    401             mCountdownTimer.cancel();
    402             mCountdownTimer = null;
    403         }
    404         if (mPendingLockCheck != null) {
    405             mPendingLockCheck.cancel(false);
    406             mPendingLockCheck = null;
    407         }
    408         displayDefaultSecurityMessage();
    409     }
    410 
    411     @Override
    412     public void onResume(int reason) {
    413     }
    414 
    415     @Override
    416     public KeyguardSecurityCallback getCallback() {
    417         return mCallback;
    418     }
    419 
    420     @Override
    421     public void showPromptReason(int reason) {
    422         switch (reason) {
    423             case PROMPT_REASON_RESTART:
    424                 mSecurityMessageDisplay.setMessage(R.string.kg_prompt_reason_restart_pattern);
    425                 break;
    426             case PROMPT_REASON_TIMEOUT:
    427                 mSecurityMessageDisplay.setMessage(R.string.kg_prompt_reason_timeout_pattern);
    428                 break;
    429             case PROMPT_REASON_DEVICE_ADMIN:
    430                 mSecurityMessageDisplay.setMessage(R.string.kg_prompt_reason_device_admin);
    431                 break;
    432             case PROMPT_REASON_USER_REQUEST:
    433                 mSecurityMessageDisplay.setMessage(R.string.kg_prompt_reason_user_request);
    434                 break;
    435             case PROMPT_REASON_NONE:
    436                 break;
    437             default:
    438                 mSecurityMessageDisplay.setMessage(R.string.kg_prompt_reason_timeout_pattern);
    439                 break;
    440         }
    441     }
    442 
    443     @Override
    444     public void showMessage(CharSequence message, ColorStateList colorState) {
    445         mSecurityMessageDisplay.setNextMessageColor(colorState);
    446         mSecurityMessageDisplay.setMessage(message);
    447     }
    448 
    449     @Override
    450     public void startAppearAnimation() {
    451         enableClipping(false);
    452         setAlpha(1f);
    453         setTranslationY(mAppearAnimationUtils.getStartTranslation());
    454         AppearAnimationUtils.startTranslationYAnimation(this, 0 /* delay */, 500 /* duration */,
    455                 0, mAppearAnimationUtils.getInterpolator());
    456         mAppearAnimationUtils.startAnimation2d(
    457                 mLockPatternView.getCellStates(),
    458                 new Runnable() {
    459                     @Override
    460                     public void run() {
    461                         enableClipping(true);
    462                     }
    463                 },
    464                 this);
    465         if (!TextUtils.isEmpty(mSecurityMessageDisplay.getText())) {
    466             mAppearAnimationUtils.createAnimation(mSecurityMessageDisplay, 0,
    467                     AppearAnimationUtils.DEFAULT_APPEAR_DURATION,
    468                     mAppearAnimationUtils.getStartTranslation(),
    469                     true /* appearing */,
    470                     mAppearAnimationUtils.getInterpolator(),
    471                     null /* finishRunnable */);
    472         }
    473     }
    474 
    475     @Override
    476     public boolean startDisappearAnimation(final Runnable finishRunnable) {
    477         float durationMultiplier = mKeyguardUpdateMonitor.needsSlowUnlockTransition()
    478                 ? DISAPPEAR_MULTIPLIER_LOCKED
    479                 : 1f;
    480         mLockPatternView.clearPattern();
    481         enableClipping(false);
    482         setTranslationY(0);
    483         AppearAnimationUtils.startTranslationYAnimation(this, 0 /* delay */,
    484                 (long) (300 * durationMultiplier),
    485                 -mDisappearAnimationUtils.getStartTranslation(),
    486                 mDisappearAnimationUtils.getInterpolator());
    487 
    488         DisappearAnimationUtils disappearAnimationUtils = mKeyguardUpdateMonitor
    489                 .needsSlowUnlockTransition()
    490                         ? mDisappearAnimationUtilsLocked
    491                         : mDisappearAnimationUtils;
    492         disappearAnimationUtils.startAnimation2d(mLockPatternView.getCellStates(),
    493                 () -> {
    494                     enableClipping(true);
    495                     if (finishRunnable != null) {
    496                         finishRunnable.run();
    497                     }
    498                 }, KeyguardPatternView.this);
    499         if (!TextUtils.isEmpty(mSecurityMessageDisplay.getText())) {
    500             mDisappearAnimationUtils.createAnimation(mSecurityMessageDisplay, 0,
    501                     (long) (200 * durationMultiplier),
    502                     - mDisappearAnimationUtils.getStartTranslation() * 3,
    503                     false /* appearing */,
    504                     mDisappearAnimationUtils.getInterpolator(),
    505                     null /* finishRunnable */);
    506         }
    507         return true;
    508     }
    509 
    510     private void enableClipping(boolean enable) {
    511         setClipChildren(enable);
    512         mContainer.setClipToPadding(enable);
    513         mContainer.setClipChildren(enable);
    514     }
    515 
    516     @Override
    517     public void createAnimation(final LockPatternView.CellState animatedCell, long delay,
    518             long duration, float translationY, final boolean appearing,
    519             Interpolator interpolator,
    520             final Runnable finishListener) {
    521         mLockPatternView.startCellStateAnimation(animatedCell,
    522                 1f, appearing ? 1f : 0f, /* alpha */
    523                 appearing ? translationY : 0f, appearing ? 0f : translationY, /* translation */
    524                 appearing ? 0f : 1f, 1f /* scale */,
    525                 delay, duration, interpolator, finishListener);
    526         if (finishListener != null) {
    527             // Also animate the Emergency call
    528             mAppearAnimationUtils.createAnimation(mEcaView, delay, duration, translationY,
    529                     appearing, interpolator, null);
    530         }
    531     }
    532 
    533     @Override
    534     public boolean hasOverlappingRendering() {
    535         return false;
    536     }
    537 
    538     @Override
    539     public CharSequence getTitle() {
    540         return getContext().getString(
    541                 com.android.internal.R.string.keyguard_accessibility_pattern_unlock);
    542     }
    543 }
    544