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