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 */
16package com.android.keyguard;
17
18import android.accounts.Account;
19import android.accounts.AccountManager;
20import android.accounts.AccountManagerCallback;
21import android.accounts.AccountManagerFuture;
22import android.accounts.AuthenticatorException;
23import android.accounts.OperationCanceledException;
24import android.content.Context;
25import android.graphics.Rect;
26import android.graphics.drawable.Drawable;
27import android.os.Bundle;
28import android.os.CountDownTimer;
29import android.os.SystemClock;
30import android.os.UserHandle;
31import android.util.AttributeSet;
32import android.util.Log;
33import android.view.MotionEvent;
34import android.view.View;
35import android.widget.Button;
36import android.widget.LinearLayout;
37
38import com.android.internal.widget.LockPatternUtils;
39import com.android.internal.widget.LockPatternView;
40
41import java.io.IOException;
42import java.util.List;
43
44public class KeyguardPatternView extends LinearLayout implements KeyguardSecurityView {
45
46    private static final String TAG = "SecurityPatternView";
47    private static final boolean DEBUG = false;
48
49    // how long before we clear the wrong pattern
50    private static final int PATTERN_CLEAR_TIMEOUT_MS = 2000;
51
52    // how long we stay awake after each key beyond MIN_PATTERN_BEFORE_POKE_WAKELOCK
53    private static final int UNLOCK_PATTERN_WAKE_INTERVAL_MS = 7000;
54
55    // how long we stay awake after the user hits the first dot.
56    private static final int UNLOCK_PATTERN_WAKE_INTERVAL_FIRST_DOTS_MS = 2000;
57
58    // how many cells the user has to cross before we poke the wakelock
59    private static final int MIN_PATTERN_BEFORE_POKE_WAKELOCK = 2;
60
61    private int mFailedPatternAttemptsSinceLastTimeout = 0;
62    private int mTotalFailedPatternAttempts = 0;
63    private CountDownTimer mCountdownTimer = null;
64    private LockPatternUtils mLockPatternUtils;
65    private LockPatternView mLockPatternView;
66    private Button mForgotPatternButton;
67    private KeyguardSecurityCallback mCallback;
68    private boolean mEnableFallback;
69
70    /**
71     * Keeps track of the last time we poked the wake lock during dispatching of the touch event.
72     * Initialized to something guaranteed to make us poke the wakelock when the user starts
73     * drawing the pattern.
74     * @see #dispatchTouchEvent(android.view.MotionEvent)
75     */
76    private long mLastPokeTime = -UNLOCK_PATTERN_WAKE_INTERVAL_MS;
77
78    /**
79     * Useful for clearing out the wrong pattern after a delay
80     */
81    private Runnable mCancelPatternRunnable = new Runnable() {
82        public void run() {
83            mLockPatternView.clearPattern();
84        }
85    };
86    private Rect mTempRect = new Rect();
87    private SecurityMessageDisplay mSecurityMessageDisplay;
88    private View mEcaView;
89    private Drawable mBouncerFrame;
90
91    enum FooterMode {
92        Normal,
93        ForgotLockPattern,
94        VerifyUnlocked
95    }
96
97    public KeyguardPatternView(Context context) {
98        this(context, null);
99    }
100
101    public KeyguardPatternView(Context context, AttributeSet attrs) {
102        super(context, attrs);
103    }
104
105    public void setKeyguardCallback(KeyguardSecurityCallback callback) {
106        mCallback = callback;
107    }
108
109    public void setLockPatternUtils(LockPatternUtils utils) {
110        mLockPatternUtils = utils;
111    }
112
113    @Override
114    protected void onFinishInflate() {
115        super.onFinishInflate();
116        mLockPatternUtils = mLockPatternUtils == null
117                ? new LockPatternUtils(mContext) : mLockPatternUtils;
118
119        mLockPatternView = (LockPatternView) findViewById(R.id.lockPatternView);
120        mLockPatternView.setSaveEnabled(false);
121        mLockPatternView.setFocusable(false);
122        mLockPatternView.setOnPatternListener(new UnlockPatternListener());
123
124        // stealth mode will be the same for the life of this screen
125        mLockPatternView.setInStealthMode(!mLockPatternUtils.isVisiblePatternEnabled());
126
127        // vibrate mode will be the same for the life of this screen
128        mLockPatternView.setTactileFeedbackEnabled(mLockPatternUtils.isTactileFeedbackEnabled());
129
130        mForgotPatternButton = (Button) findViewById(R.id.forgot_password_button);
131        // note: some configurations don't have an emergency call area
132        if (mForgotPatternButton != null) {
133            mForgotPatternButton.setText(R.string.kg_forgot_pattern_button_text);
134            mForgotPatternButton.setOnClickListener(new OnClickListener() {
135                public void onClick(View v) {
136                    mCallback.showBackupSecurity();
137                }
138            });
139        }
140
141        setFocusableInTouchMode(true);
142
143        maybeEnableFallback(mContext);
144        mSecurityMessageDisplay = new KeyguardMessageArea.Helper(this);
145        mEcaView = findViewById(R.id.keyguard_selector_fade_container);
146        View bouncerFrameView = findViewById(R.id.keyguard_bouncer_frame);
147        if (bouncerFrameView != null) {
148            mBouncerFrame = bouncerFrameView.getBackground();
149        }
150    }
151
152    private void updateFooter(FooterMode mode) {
153        if (mForgotPatternButton == null) return; // no ECA? no footer
154
155        switch (mode) {
156            case Normal:
157                if (DEBUG) Log.d(TAG, "mode normal");
158                mForgotPatternButton.setVisibility(View.GONE);
159                break;
160            case ForgotLockPattern:
161                if (DEBUG) Log.d(TAG, "mode ForgotLockPattern");
162                mForgotPatternButton.setVisibility(View.VISIBLE);
163                break;
164            case VerifyUnlocked:
165                if (DEBUG) Log.d(TAG, "mode VerifyUnlocked");
166                mForgotPatternButton.setVisibility(View.GONE);
167        }
168    }
169
170    @Override
171    public boolean onTouchEvent(MotionEvent ev) {
172        boolean result = super.onTouchEvent(ev);
173        // as long as the user is entering a pattern (i.e sending a touch event that was handled
174        // by this screen), keep poking the wake lock so that the screen will stay on.
175        final long elapsed = SystemClock.elapsedRealtime() - mLastPokeTime;
176        if (result && (elapsed > (UNLOCK_PATTERN_WAKE_INTERVAL_MS - 100))) {
177            mLastPokeTime = SystemClock.elapsedRealtime();
178        }
179        mTempRect.set(0, 0, 0, 0);
180        offsetRectIntoDescendantCoords(mLockPatternView, mTempRect);
181        ev.offsetLocation(mTempRect.left, mTempRect.top);
182        result = mLockPatternView.dispatchTouchEvent(ev) || result;
183        ev.offsetLocation(-mTempRect.left, -mTempRect.top);
184        return result;
185    }
186
187    public void reset() {
188        // reset lock pattern
189        mLockPatternView.enableInput();
190        mLockPatternView.setEnabled(true);
191        mLockPatternView.clearPattern();
192
193        // if the user is currently locked out, enforce it.
194        long deadline = mLockPatternUtils.getLockoutAttemptDeadline();
195        if (deadline != 0) {
196            handleAttemptLockout(deadline);
197        } else {
198            displayDefaultSecurityMessage();
199        }
200
201        // the footer depends on how many total attempts the user has failed
202        if (mCallback.isVerifyUnlockOnly()) {
203            updateFooter(FooterMode.VerifyUnlocked);
204        } else if (mEnableFallback &&
205                (mTotalFailedPatternAttempts >= LockPatternUtils.FAILED_ATTEMPTS_BEFORE_TIMEOUT)) {
206            updateFooter(FooterMode.ForgotLockPattern);
207        } else {
208            updateFooter(FooterMode.Normal);
209        }
210
211    }
212
213    private void displayDefaultSecurityMessage() {
214        if (KeyguardUpdateMonitor.getInstance(mContext).getMaxBiometricUnlockAttemptsReached()) {
215            mSecurityMessageDisplay.setMessage(R.string.faceunlock_multiple_failures, true);
216        } else {
217            mSecurityMessageDisplay.setMessage(R.string.kg_pattern_instructions, false);
218        }
219    }
220
221    @Override
222    public void showUsabilityHint() {
223    }
224
225    /** TODO: hook this up */
226    public void cleanUp() {
227        if (DEBUG) Log.v(TAG, "Cleanup() called on " + this);
228        mLockPatternUtils = null;
229        mLockPatternView.setOnPatternListener(null);
230    }
231
232    @Override
233    public void onWindowFocusChanged(boolean hasWindowFocus) {
234        super.onWindowFocusChanged(hasWindowFocus);
235        if (hasWindowFocus) {
236            // when timeout dialog closes we want to update our state
237            reset();
238        }
239    }
240
241    private class UnlockPatternListener implements LockPatternView.OnPatternListener {
242
243        public void onPatternStart() {
244            mLockPatternView.removeCallbacks(mCancelPatternRunnable);
245        }
246
247        public void onPatternCleared() {
248        }
249
250        public void onPatternCellAdded(List<LockPatternView.Cell> pattern) {
251            // To guard against accidental poking of the wakelock, look for
252            // the user actually trying to draw a pattern of some minimal length.
253            if (pattern.size() > MIN_PATTERN_BEFORE_POKE_WAKELOCK) {
254                mCallback.userActivity(UNLOCK_PATTERN_WAKE_INTERVAL_MS);
255            } else {
256                // Give just a little extra time if they hit one of the first few dots
257                mCallback.userActivity(UNLOCK_PATTERN_WAKE_INTERVAL_FIRST_DOTS_MS);
258            }
259        }
260
261        public void onPatternDetected(List<LockPatternView.Cell> pattern) {
262            if (mLockPatternUtils.checkPattern(pattern)) {
263                mCallback.reportSuccessfulUnlockAttempt();
264                mLockPatternView.setDisplayMode(LockPatternView.DisplayMode.Correct);
265                mTotalFailedPatternAttempts = 0;
266                mCallback.dismiss(true);
267            } else {
268                if (pattern.size() > MIN_PATTERN_BEFORE_POKE_WAKELOCK) {
269                    mCallback.userActivity(UNLOCK_PATTERN_WAKE_INTERVAL_MS);
270                }
271                mLockPatternView.setDisplayMode(LockPatternView.DisplayMode.Wrong);
272                if (pattern.size() >= LockPatternUtils.MIN_PATTERN_REGISTER_FAIL) {
273                    mTotalFailedPatternAttempts++;
274                    mFailedPatternAttemptsSinceLastTimeout++;
275                    mCallback.reportFailedUnlockAttempt();
276                }
277                if (mFailedPatternAttemptsSinceLastTimeout
278                        >= LockPatternUtils.FAILED_ATTEMPTS_BEFORE_TIMEOUT) {
279                    long deadline = mLockPatternUtils.setLockoutAttemptDeadline();
280                    handleAttemptLockout(deadline);
281                } else {
282                    mSecurityMessageDisplay.setMessage(R.string.kg_wrong_pattern, true);
283                    mLockPatternView.postDelayed(mCancelPatternRunnable, PATTERN_CLEAR_TIMEOUT_MS);
284                }
285            }
286        }
287    }
288
289    private void maybeEnableFallback(Context context) {
290        // Ask the account manager if we have an account that can be used as a
291        // fallback in case the user forgets his pattern.
292        AccountAnalyzer accountAnalyzer = new AccountAnalyzer(AccountManager.get(context));
293        accountAnalyzer.start();
294    }
295
296    private class AccountAnalyzer implements AccountManagerCallback<Bundle> {
297        private final AccountManager mAccountManager;
298        private final Account[] mAccounts;
299        private int mAccountIndex;
300
301        private AccountAnalyzer(AccountManager accountManager) {
302            mAccountManager = accountManager;
303            mAccounts = accountManager.getAccountsByTypeAsUser("com.google",
304                    new UserHandle(mLockPatternUtils.getCurrentUser()));
305        }
306
307        private void next() {
308            // if we are ready to enable the fallback or if we depleted the list of accounts
309            // then finish and get out
310            if (mEnableFallback || mAccountIndex >= mAccounts.length) {
311                return;
312            }
313
314            // lookup the confirmCredentials intent for the current account
315            mAccountManager.confirmCredentialsAsUser(mAccounts[mAccountIndex], null, null, this,
316                    null, new UserHandle(mLockPatternUtils.getCurrentUser()));
317        }
318
319        public void start() {
320            mEnableFallback = false;
321            mAccountIndex = 0;
322            next();
323        }
324
325        public void run(AccountManagerFuture<Bundle> future) {
326            try {
327                Bundle result = future.getResult();
328                if (result.getParcelable(AccountManager.KEY_INTENT) != null) {
329                    mEnableFallback = true;
330                }
331            } catch (OperationCanceledException e) {
332                // just skip the account if we are unable to query it
333            } catch (IOException e) {
334                // just skip the account if we are unable to query it
335            } catch (AuthenticatorException e) {
336                // just skip the account if we are unable to query it
337            } finally {
338                mAccountIndex++;
339                next();
340            }
341        }
342    }
343
344    private void handleAttemptLockout(long elapsedRealtimeDeadline) {
345        mLockPatternView.clearPattern();
346        mLockPatternView.setEnabled(false);
347        final long elapsedRealtime = SystemClock.elapsedRealtime();
348        if (mEnableFallback) {
349            updateFooter(FooterMode.ForgotLockPattern);
350        }
351
352        mCountdownTimer = new CountDownTimer(elapsedRealtimeDeadline - elapsedRealtime, 1000) {
353
354            @Override
355            public void onTick(long millisUntilFinished) {
356                final int secondsRemaining = (int) (millisUntilFinished / 1000);
357                mSecurityMessageDisplay.setMessage(
358                        R.string.kg_too_many_failed_attempts_countdown, true, secondsRemaining);
359            }
360
361            @Override
362            public void onFinish() {
363                mLockPatternView.setEnabled(true);
364                displayDefaultSecurityMessage();
365                // TODO mUnlockIcon.setVisibility(View.VISIBLE);
366                mFailedPatternAttemptsSinceLastTimeout = 0;
367                if (mEnableFallback) {
368                    updateFooter(FooterMode.ForgotLockPattern);
369                } else {
370                    updateFooter(FooterMode.Normal);
371                }
372            }
373
374        }.start();
375    }
376
377    @Override
378    public boolean needsInput() {
379        return false;
380    }
381
382    @Override
383    public void onPause() {
384        if (mCountdownTimer != null) {
385            mCountdownTimer.cancel();
386            mCountdownTimer = null;
387        }
388    }
389
390    @Override
391    public void onResume(int reason) {
392        reset();
393    }
394
395    @Override
396    public KeyguardSecurityCallback getCallback() {
397        return mCallback;
398    }
399
400    @Override
401    public void showBouncer(int duration) {
402        KeyguardSecurityViewHelper.
403                showBouncer(mSecurityMessageDisplay, mEcaView, mBouncerFrame, duration);
404    }
405
406    @Override
407    public void hideBouncer(int duration) {
408        KeyguardSecurityViewHelper.
409                hideBouncer(mSecurityMessageDisplay, mEcaView, mBouncerFrame, duration);
410    }
411}
412