ConfirmLockPattern.java revision ce25af48383b26535ec07312ab0db6928bba1fe3
1/*
2 * Copyright (C) 2008 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
17package com.android.settings;
18
19import com.android.internal.logging.MetricsLogger;
20import com.android.internal.widget.LockPatternUtils;
21import com.android.internal.widget.LockPatternView;
22import com.android.internal.widget.LinearLayoutWithDefaultTouchRecepient;
23import com.android.internal.widget.LockPatternChecker;
24import com.android.internal.widget.LockPatternView.Cell;
25import com.android.settingslib.animation.AppearAnimationCreator;
26import com.android.settingslib.animation.AppearAnimationUtils;
27import com.android.settingslib.animation.DisappearAnimationUtils;
28
29import android.animation.Animator;
30import android.animation.AnimatorListenerAdapter;
31import android.animation.ValueAnimator;
32import android.annotation.Nullable;
33import android.app.Activity;
34import android.app.Fragment;
35import android.content.Intent;
36import android.graphics.Color;
37import android.graphics.Paint;
38import android.graphics.PorterDuff;
39import android.graphics.PorterDuffColorFilter;
40import android.os.CountDownTimer;
41import android.os.SystemClock;
42import android.os.AsyncTask;
43import android.os.Bundle;
44import android.os.UserHandle;
45import android.os.storage.StorageManager;
46import android.view.MenuItem;
47import android.view.animation.AnimationUtils;
48import android.view.animation.Interpolator;
49import android.widget.TextView;
50import android.view.LayoutInflater;
51import android.view.View;
52import android.view.ViewGroup;
53
54import java.util.ArrayList;
55import java.util.Arrays;
56import java.util.Collections;
57import java.util.List;
58
59/**
60 * Launch this when you want the user to confirm their lock pattern.
61 *
62 * Sets an activity result of {@link Activity#RESULT_OK} when the user
63 * successfully confirmed their pattern.
64 */
65public class ConfirmLockPattern extends ConfirmDeviceCredentialBaseActivity {
66
67    public static class InternalActivity extends ConfirmLockPattern {
68    }
69
70    private enum Stage {
71        NeedToUnlock,
72        NeedToUnlockWrong,
73        LockedOut
74    }
75
76    @Override
77    public Intent getIntent() {
78        Intent modIntent = new Intent(super.getIntent());
79        modIntent.putExtra(EXTRA_SHOW_FRAGMENT, ConfirmLockPatternFragment.class.getName());
80        return modIntent;
81    }
82
83    @Override
84    protected boolean isValidFragment(String fragmentName) {
85        if (ConfirmLockPatternFragment.class.getName().equals(fragmentName)) return true;
86        return false;
87    }
88
89    public static class ConfirmLockPatternFragment extends ConfirmDeviceCredentialBaseFragment
90            implements AppearAnimationCreator<Object> {
91
92        // how long we wait to clear a wrong pattern
93        private static final int WRONG_PATTERN_CLEAR_TIMEOUT_MS = 2000;
94
95        private static final String KEY_NUM_WRONG_ATTEMPTS = "num_wrong_attempts";
96
97        private LockPatternView mLockPatternView;
98        private LockPatternUtils mLockPatternUtils;
99        private AsyncTask<?, ?, ?> mPendingLockCheck;
100        private int mNumWrongConfirmAttempts;
101        private CountDownTimer mCountdownTimer;
102
103        private TextView mHeaderTextView;
104        private TextView mDetailsTextView;
105        private TextView mErrorTextView;
106        private View mLeftSpacerLandscape;
107        private View mRightSpacerLandscape;
108
109        // caller-supplied text for various prompts
110        private CharSequence mHeaderText;
111        private CharSequence mDetailsText;
112
113        private AppearAnimationUtils mAppearAnimationUtils;
114        private DisappearAnimationUtils mDisappearAnimationUtils;
115
116        // required constructor for fragments
117        public ConfirmLockPatternFragment() {
118
119        }
120
121        @Override
122        public void onCreate(Bundle savedInstanceState) {
123            super.onCreate(savedInstanceState);
124            mLockPatternUtils = new LockPatternUtils(getActivity());
125        }
126
127        @Override
128        public View onCreateView(LayoutInflater inflater, ViewGroup container,
129                Bundle savedInstanceState) {
130            View view = inflater.inflate(R.layout.confirm_lock_pattern, null);
131            mHeaderTextView = (TextView) view.findViewById(R.id.headerText);
132            mLockPatternView = (LockPatternView) view.findViewById(R.id.lockPattern);
133            mDetailsTextView = (TextView) view.findViewById(R.id.detailsText);
134            mErrorTextView = (TextView) view.findViewById(R.id.errorText);
135            mLeftSpacerLandscape = view.findViewById(R.id.leftSpacer);
136            mRightSpacerLandscape = view.findViewById(R.id.rightSpacer);
137
138            // make it so unhandled touch events within the unlock screen go to the
139            // lock pattern view.
140            final LinearLayoutWithDefaultTouchRecepient topLayout
141                    = (LinearLayoutWithDefaultTouchRecepient) view.findViewById(R.id.topLayout);
142            topLayout.setDefaultTouchRecepient(mLockPatternView);
143
144            Intent intent = getActivity().getIntent();
145            if (intent != null) {
146                mHeaderText = intent.getCharSequenceExtra(
147                        ConfirmDeviceCredentialBaseFragment.HEADER_TEXT);
148                mDetailsText = intent.getCharSequenceExtra(
149                        ConfirmDeviceCredentialBaseFragment.DETAILS_TEXT);
150            }
151
152            mLockPatternView.setTactileFeedbackEnabled(mLockPatternUtils.isTactileFeedbackEnabled());
153            mLockPatternView.setOnPatternListener(mConfirmExistingLockPatternListener);
154            updateStage(Stage.NeedToUnlock);
155
156            if (savedInstanceState != null) {
157                mNumWrongConfirmAttempts = savedInstanceState.getInt(KEY_NUM_WRONG_ATTEMPTS);
158            } else {
159                // on first launch, if no lock pattern is set, then finish with
160                // success (don't want user to get stuck confirming something that
161                // doesn't exist).
162                if (!mLockPatternUtils.isLockPatternEnabled(UserHandle.myUserId())) {
163                    getActivity().setResult(Activity.RESULT_OK);
164                    getActivity().finish();
165                }
166            }
167            mAppearAnimationUtils = new AppearAnimationUtils(getContext(),
168                    AppearAnimationUtils.DEFAULT_APPEAR_DURATION, 2f /* translationScale */,
169                    1.3f /* delayScale */, AnimationUtils.loadInterpolator(
170                    getContext(), android.R.interpolator.linear_out_slow_in));
171            mDisappearAnimationUtils = new DisappearAnimationUtils(getContext(),
172                    125, 4f /* translationScale */,
173                    0.3f /* delayScale */, AnimationUtils.loadInterpolator(
174                    getContext(), android.R.interpolator.fast_out_linear_in),
175                    new AppearAnimationUtils.RowTranslationScaler() {
176                        @Override
177                        public float getRowTranslationScale(int row, int numRows) {
178                            return (float)(numRows - row) / numRows;
179                        }
180                    });
181            setAccessibilityTitle(mHeaderTextView.getText());
182            return view;
183        }
184
185        @Override
186        public void onSaveInstanceState(Bundle outState) {
187            // deliberately not calling super since we are managing this in full
188            outState.putInt(KEY_NUM_WRONG_ATTEMPTS, mNumWrongConfirmAttempts);
189        }
190
191        @Override
192        public void onPause() {
193            super.onPause();
194
195            if (mCountdownTimer != null) {
196                mCountdownTimer.cancel();
197            }
198            if (mPendingLockCheck != null) {
199                mPendingLockCheck.cancel(false);
200                mPendingLockCheck = null;
201            }
202        }
203
204        @Override
205        protected int getMetricsCategory() {
206            return MetricsLogger.CONFIRM_LOCK_PATTERN;
207        }
208
209        @Override
210        public void onResume() {
211            super.onResume();
212
213            // if the user is currently locked out, enforce it.
214            long deadline = mLockPatternUtils.getLockoutAttemptDeadline(UserHandle.myUserId());
215            if (deadline != 0) {
216                handleAttemptLockout(deadline);
217            } else if (!mLockPatternView.isEnabled()) {
218                // The deadline has passed, but the timer was cancelled. Or the pending lock
219                // check was cancelled. Need to clean up.
220                mNumWrongConfirmAttempts = 0;
221                updateStage(Stage.NeedToUnlock);
222            }
223        }
224
225        @Override
226        public void prepareEnterAnimation() {
227            super.prepareEnterAnimation();
228            mHeaderTextView.setAlpha(0f);
229            mCancelButton.setAlpha(0f);
230            mLockPatternView.setAlpha(0f);
231            mDetailsTextView.setAlpha(0f);
232            mFingerprintIcon.setAlpha(0f);
233        }
234
235        private Object[][] getActiveViews() {
236            ArrayList<ArrayList<Object>> result = new ArrayList<>();
237            result.add(new ArrayList<Object>(Collections.singletonList(mHeaderTextView)));
238            result.add(new ArrayList<Object>(Collections.singletonList(mDetailsTextView)));
239            if (mCancelButton.getVisibility() == View.VISIBLE) {
240                result.add(new ArrayList<Object>(Collections.singletonList(mCancelButton)));
241            }
242            LockPatternView.CellState[][] cellStates = mLockPatternView.getCellStates();
243            for (int i = 0; i < cellStates.length; i++) {
244                ArrayList<Object> row = new ArrayList<>();
245                for (int j = 0; j < cellStates[i].length; j++) {
246                    row.add(cellStates[i][j]);
247                }
248                result.add(row);
249            }
250            if (mFingerprintIcon.getVisibility() == View.VISIBLE) {
251                result.add(new ArrayList<Object>(Collections.singletonList(mFingerprintIcon)));
252            }
253            Object[][] resultArr = new Object[result.size()][cellStates[0].length];
254            for (int i = 0; i < result.size(); i++) {
255                ArrayList<Object> row = result.get(i);
256                for (int j = 0; j < row.size(); j++) {
257                    resultArr[i][j] = row.get(j);
258                }
259            }
260            return resultArr;
261        }
262
263        @Override
264        public void startEnterAnimation() {
265            super.startEnterAnimation();
266            mLockPatternView.setAlpha(1f);
267            mAppearAnimationUtils.startAnimation2d(getActiveViews(), null, this);
268        }
269
270        private void updateStage(Stage stage) {
271            switch (stage) {
272                case NeedToUnlock:
273                    if (mHeaderText != null) {
274                        mHeaderTextView.setText(mHeaderText);
275                    } else {
276                        mHeaderTextView.setText(R.string.lockpassword_confirm_your_pattern_header);
277                    }
278                    if (mDetailsText != null) {
279                        mDetailsTextView.setText(mDetailsText);
280                    } else {
281                        mDetailsTextView.setText(
282                                R.string.lockpassword_confirm_your_pattern_generic);
283                    }
284                    mErrorTextView.setText("");
285
286                    mLockPatternView.setEnabled(true);
287                    mLockPatternView.enableInput();
288                    mLockPatternView.clearPattern();
289                    break;
290                case NeedToUnlockWrong:
291                    mErrorTextView.setText(R.string.lockpattern_need_to_unlock_wrong);
292
293                    mLockPatternView.setDisplayMode(LockPatternView.DisplayMode.Wrong);
294                    mLockPatternView.setEnabled(true);
295                    mLockPatternView.enableInput();
296                    break;
297                case LockedOut:
298                    mLockPatternView.clearPattern();
299                    // enabled = false means: disable input, and have the
300                    // appearance of being disabled.
301                    mLockPatternView.setEnabled(false); // appearance of being disabled
302                    break;
303            }
304
305            // Always announce the header for accessibility. This is a no-op
306            // when accessibility is disabled.
307            mHeaderTextView.announceForAccessibility(mHeaderTextView.getText());
308        }
309
310        private Runnable mClearPatternRunnable = new Runnable() {
311            public void run() {
312                mLockPatternView.clearPattern();
313            }
314        };
315
316        // clear the wrong pattern unless they have started a new one
317        // already
318        private void postClearPatternRunnable() {
319            mLockPatternView.removeCallbacks(mClearPatternRunnable);
320            mLockPatternView.postDelayed(mClearPatternRunnable, WRONG_PATTERN_CLEAR_TIMEOUT_MS);
321        }
322
323        @Override
324        protected void authenticationSucceeded() {
325            startDisappearAnimation(new Intent());
326        }
327
328        private void startDisappearAnimation(final Intent intent) {
329            if (getActivity().getThemeResId() == R.style.Theme_ConfirmDeviceCredentialsDark) {
330                mLockPatternView.clearPattern();
331                mDisappearAnimationUtils.startAnimation2d(getActiveViews(),
332                        new Runnable() {
333                            @Override
334                            public void run() {
335                                getActivity().setResult(RESULT_OK, intent);
336                                getActivity().finish();
337                                getActivity().overridePendingTransition(
338                                        R.anim.confirm_credential_close_enter,
339                                        R.anim.confirm_credential_close_exit);
340                            }
341                        }, this);
342            } else {
343                getActivity().setResult(RESULT_OK, intent);
344                getActivity().finish();
345            }
346        }
347
348        @Override
349        public void onFingerprintIconVisibilityChanged(boolean visible) {
350            if (mLeftSpacerLandscape != null && mRightSpacerLandscape != null) {
351
352                // In landscape, adjust spacing depending on fingerprint icon visibility.
353                mLeftSpacerLandscape.setVisibility(visible ? View.GONE : View.VISIBLE);
354                mRightSpacerLandscape.setVisibility(visible ? View.GONE : View.VISIBLE);
355            }
356        }
357
358        /**
359         * The pattern listener that responds according to a user confirming
360         * an existing lock pattern.
361         */
362        private LockPatternView.OnPatternListener mConfirmExistingLockPatternListener
363                = new LockPatternView.OnPatternListener()  {
364
365            public void onPatternStart() {
366                mLockPatternView.removeCallbacks(mClearPatternRunnable);
367            }
368
369            public void onPatternCleared() {
370                mLockPatternView.removeCallbacks(mClearPatternRunnable);
371            }
372
373            public void onPatternCellAdded(List<Cell> pattern) {
374
375            }
376
377            public void onPatternDetected(List<LockPatternView.Cell> pattern) {
378                mLockPatternView.setEnabled(false);
379                if (mPendingLockCheck != null) {
380                    mPendingLockCheck.cancel(false);
381                }
382
383                final boolean verifyChallenge = getActivity().getIntent().getBooleanExtra(
384                        ChooseLockSettingsHelper.EXTRA_KEY_HAS_CHALLENGE, false);
385                Intent intent = new Intent();
386                if (verifyChallenge) {
387                    if (isInternalActivity()) {
388                        startVerifyPattern(pattern, intent);
389                        return;
390                    }
391                } else {
392                    startCheckPattern(pattern, intent);
393                    return;
394                }
395
396                onPatternChecked(pattern, false, intent, 0);
397            }
398
399            private boolean isInternalActivity() {
400                return getActivity() instanceof ConfirmLockPattern.InternalActivity;
401            }
402
403            private void startVerifyPattern(final List<LockPatternView.Cell> pattern,
404                    final Intent intent) {
405                long challenge = getActivity().getIntent().getLongExtra(
406                        ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE, 0);
407                mPendingLockCheck = LockPatternChecker.verifyPattern(
408                        mLockPatternUtils,
409                        pattern,
410                        challenge,
411                        UserHandle.myUserId(),
412                        new LockPatternChecker.OnVerifyCallback() {
413                            @Override
414                            public void onVerified(byte[] token, int timeoutMs) {
415                                mPendingLockCheck = null;
416                                boolean matched = false;
417                                if (token != null) {
418                                    matched = true;
419                                    intent.putExtra(
420                                            ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN,
421                                            token);
422                                }
423                                onPatternChecked(pattern, matched, intent, timeoutMs);
424                            }
425                        });
426            }
427
428            private void startCheckPattern(final List<LockPatternView.Cell> pattern,
429                    final Intent intent) {
430                if (pattern.size() < LockPatternUtils.MIN_PATTERN_REGISTER_FAIL) {
431                    onPatternChecked(pattern, false, intent, 0);
432                    return;
433                }
434
435                mPendingLockCheck = LockPatternChecker.checkPattern(
436                        mLockPatternUtils,
437                        pattern,
438                        UserHandle.myUserId(),
439                        new LockPatternChecker.OnCheckCallback() {
440                            @Override
441                            public void onChecked(boolean matched, int timeoutMs) {
442                                mPendingLockCheck = null;
443                                if (matched && isInternalActivity()) {
444                                    intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_TYPE,
445                                                    StorageManager.CRYPT_TYPE_PATTERN);
446                                    intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_PASSWORD,
447                                                    LockPatternUtils.patternToString(pattern));
448                                }
449                                onPatternChecked(pattern, matched, intent, timeoutMs);
450                            }
451                        });
452            }
453
454            private void onPatternChecked(List<LockPatternView.Cell> pattern,
455                    boolean matched, Intent intent, int timeoutMs) {
456                mLockPatternView.setEnabled(true);
457                if (matched) {
458                    startDisappearAnimation(intent);
459                } else {
460                    if (timeoutMs > 0) {
461                        long deadline = mLockPatternUtils.setLockoutAttemptDeadline(
462                                UserHandle.myUserId(), timeoutMs);
463                        handleAttemptLockout(deadline);
464                    } else {
465                        updateStage(Stage.NeedToUnlockWrong);
466                        postClearPatternRunnable();
467                    }
468                }
469            }
470        };
471
472        private void handleAttemptLockout(long elapsedRealtimeDeadline) {
473            updateStage(Stage.LockedOut);
474            long elapsedRealtime = SystemClock.elapsedRealtime();
475            mCountdownTimer = new CountDownTimer(
476                    elapsedRealtimeDeadline - elapsedRealtime,
477                    LockPatternUtils.FAILED_ATTEMPT_COUNTDOWN_INTERVAL_MS) {
478
479                @Override
480                public void onTick(long millisUntilFinished) {
481                    final int secondsCountdown = (int) (millisUntilFinished / 1000);
482                    mErrorTextView.setText(getString(
483                            R.string.lockpattern_too_many_failed_confirmation_attempts,
484                            secondsCountdown));
485                }
486
487                @Override
488                public void onFinish() {
489                    mNumWrongConfirmAttempts = 0;
490                    updateStage(Stage.NeedToUnlock);
491                }
492            }.start();
493        }
494
495        @Override
496        public void createAnimation(Object obj, long delay,
497                long duration, float translationY, final boolean appearing,
498                Interpolator interpolator,
499                final Runnable finishListener) {
500            if (obj instanceof LockPatternView.CellState) {
501                final LockPatternView.CellState animatedCell = (LockPatternView.CellState) obj;
502                if (appearing) {
503                    animatedCell.scale = 0.0f;
504                    animatedCell.alpha = 1.0f;
505                }
506                animatedCell.translateY = appearing ? translationY : 0;
507                ValueAnimator animator = ValueAnimator.ofFloat(animatedCell.translateY,
508                        appearing ? 0 : translationY);
509                animator.setInterpolator(interpolator);
510                animator.setDuration(duration);
511                animator.setStartDelay(delay);
512                animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
513                    @Override
514                    public void onAnimationUpdate(ValueAnimator animation) {
515                        float animatedFraction = animation.getAnimatedFraction();
516                        if (appearing) {
517                            animatedCell.scale = animatedFraction;
518                        } else {
519                            animatedCell.alpha = 1 - animatedFraction;
520                        }
521                        animatedCell.translateY = (float) animation.getAnimatedValue();
522                        mLockPatternView.invalidate();
523                    }
524                });
525                if (finishListener != null) {
526                    animator.addListener(new AnimatorListenerAdapter() {
527                        @Override
528                        public void onAnimationEnd(Animator animation) {
529                            finishListener.run();
530                        }
531                    });
532                }
533
534                animator.start();
535                mLockPatternView.invalidate();
536            } else {
537                mAppearAnimationUtils.createAnimation((View) obj, delay, duration, translationY,
538                        appearing, interpolator, finishListener);
539            }
540        }
541    }
542}
543