/* * Copyright (C) 2012 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.keyguard; import android.accounts.Account; import android.accounts.AccountManager; import android.accounts.AccountManagerCallback; import android.accounts.AccountManagerFuture; import android.accounts.AuthenticatorException; import android.accounts.OperationCanceledException; import android.content.Context; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.os.CountDownTimer; import android.os.SystemClock; import android.os.UserHandle; import android.util.AttributeSet; import android.util.Log; import android.view.MotionEvent; import android.view.View; import android.widget.Button; import android.widget.LinearLayout; import com.android.internal.widget.LockPatternUtils; import com.android.internal.widget.LockPatternView; import java.io.IOException; import java.util.List; public class KeyguardPatternView extends LinearLayout implements KeyguardSecurityView { private static final String TAG = "SecurityPatternView"; private static final boolean DEBUG = false; // how long before we clear the wrong pattern private static final int PATTERN_CLEAR_TIMEOUT_MS = 2000; // how long we stay awake after each key beyond MIN_PATTERN_BEFORE_POKE_WAKELOCK private static final int UNLOCK_PATTERN_WAKE_INTERVAL_MS = 7000; // how long we stay awake after the user hits the first dot. private static final int UNLOCK_PATTERN_WAKE_INTERVAL_FIRST_DOTS_MS = 2000; // how many cells the user has to cross before we poke the wakelock private static final int MIN_PATTERN_BEFORE_POKE_WAKELOCK = 2; private int mFailedPatternAttemptsSinceLastTimeout = 0; private int mTotalFailedPatternAttempts = 0; private CountDownTimer mCountdownTimer = null; private LockPatternUtils mLockPatternUtils; private LockPatternView mLockPatternView; private Button mForgotPatternButton; private KeyguardSecurityCallback mCallback; private boolean mEnableFallback; /** * Keeps track of the last time we poked the wake lock during dispatching of the touch event. * Initialized to something guaranteed to make us poke the wakelock when the user starts * drawing the pattern. * @see #dispatchTouchEvent(android.view.MotionEvent) */ private long mLastPokeTime = -UNLOCK_PATTERN_WAKE_INTERVAL_MS; /** * Useful for clearing out the wrong pattern after a delay */ private Runnable mCancelPatternRunnable = new Runnable() { public void run() { mLockPatternView.clearPattern(); } }; private Rect mTempRect = new Rect(); private SecurityMessageDisplay mSecurityMessageDisplay; private View mEcaView; private Drawable mBouncerFrame; enum FooterMode { Normal, ForgotLockPattern, VerifyUnlocked } public KeyguardPatternView(Context context) { this(context, null); } public KeyguardPatternView(Context context, AttributeSet attrs) { super(context, attrs); } public void setKeyguardCallback(KeyguardSecurityCallback callback) { mCallback = callback; } public void setLockPatternUtils(LockPatternUtils utils) { mLockPatternUtils = utils; } @Override protected void onFinishInflate() { super.onFinishInflate(); mLockPatternUtils = mLockPatternUtils == null ? new LockPatternUtils(mContext) : mLockPatternUtils; mLockPatternView = (LockPatternView) findViewById(R.id.lockPatternView); mLockPatternView.setSaveEnabled(false); mLockPatternView.setFocusable(false); mLockPatternView.setOnPatternListener(new UnlockPatternListener()); // stealth mode will be the same for the life of this screen mLockPatternView.setInStealthMode(!mLockPatternUtils.isVisiblePatternEnabled()); // vibrate mode will be the same for the life of this screen mLockPatternView.setTactileFeedbackEnabled(mLockPatternUtils.isTactileFeedbackEnabled()); mForgotPatternButton = (Button) findViewById(R.id.forgot_password_button); // note: some configurations don't have an emergency call area if (mForgotPatternButton != null) { mForgotPatternButton.setText(R.string.kg_forgot_pattern_button_text); mForgotPatternButton.setOnClickListener(new OnClickListener() { public void onClick(View v) { mCallback.showBackupSecurity(); } }); } setFocusableInTouchMode(true); maybeEnableFallback(mContext); mSecurityMessageDisplay = new KeyguardMessageArea.Helper(this); mEcaView = findViewById(R.id.keyguard_selector_fade_container); View bouncerFrameView = findViewById(R.id.keyguard_bouncer_frame); if (bouncerFrameView != null) { mBouncerFrame = bouncerFrameView.getBackground(); } } private void updateFooter(FooterMode mode) { if (mForgotPatternButton == null) return; // no ECA? no footer switch (mode) { case Normal: if (DEBUG) Log.d(TAG, "mode normal"); mForgotPatternButton.setVisibility(View.GONE); break; case ForgotLockPattern: if (DEBUG) Log.d(TAG, "mode ForgotLockPattern"); mForgotPatternButton.setVisibility(View.VISIBLE); break; case VerifyUnlocked: if (DEBUG) Log.d(TAG, "mode VerifyUnlocked"); mForgotPatternButton.setVisibility(View.GONE); } } @Override public boolean onTouchEvent(MotionEvent ev) { boolean result = super.onTouchEvent(ev); // as long as the user is entering a pattern (i.e sending a touch event that was handled // by this screen), keep poking the wake lock so that the screen will stay on. final long elapsed = SystemClock.elapsedRealtime() - mLastPokeTime; if (result && (elapsed > (UNLOCK_PATTERN_WAKE_INTERVAL_MS - 100))) { mLastPokeTime = SystemClock.elapsedRealtime(); } mTempRect.set(0, 0, 0, 0); offsetRectIntoDescendantCoords(mLockPatternView, mTempRect); ev.offsetLocation(mTempRect.left, mTempRect.top); result = mLockPatternView.dispatchTouchEvent(ev) || result; ev.offsetLocation(-mTempRect.left, -mTempRect.top); return result; } public void reset() { // reset lock pattern mLockPatternView.enableInput(); mLockPatternView.setEnabled(true); mLockPatternView.clearPattern(); // if the user is currently locked out, enforce it. long deadline = mLockPatternUtils.getLockoutAttemptDeadline(); if (deadline != 0) { handleAttemptLockout(deadline); } else { displayDefaultSecurityMessage(); } // the footer depends on how many total attempts the user has failed if (mCallback.isVerifyUnlockOnly()) { updateFooter(FooterMode.VerifyUnlocked); } else if (mEnableFallback && (mTotalFailedPatternAttempts >= LockPatternUtils.FAILED_ATTEMPTS_BEFORE_TIMEOUT)) { updateFooter(FooterMode.ForgotLockPattern); } else { updateFooter(FooterMode.Normal); } } private void displayDefaultSecurityMessage() { if (KeyguardUpdateMonitor.getInstance(mContext).getMaxBiometricUnlockAttemptsReached()) { mSecurityMessageDisplay.setMessage(R.string.faceunlock_multiple_failures, true); } else { mSecurityMessageDisplay.setMessage(R.string.kg_pattern_instructions, false); } } @Override public void showUsabilityHint() { } /** TODO: hook this up */ public void cleanUp() { if (DEBUG) Log.v(TAG, "Cleanup() called on " + this); mLockPatternUtils = null; mLockPatternView.setOnPatternListener(null); } @Override public void onWindowFocusChanged(boolean hasWindowFocus) { super.onWindowFocusChanged(hasWindowFocus); if (hasWindowFocus) { // when timeout dialog closes we want to update our state reset(); } } private class UnlockPatternListener implements LockPatternView.OnPatternListener { public void onPatternStart() { mLockPatternView.removeCallbacks(mCancelPatternRunnable); } public void onPatternCleared() { } public void onPatternCellAdded(List pattern) { // To guard against accidental poking of the wakelock, look for // the user actually trying to draw a pattern of some minimal length. if (pattern.size() > MIN_PATTERN_BEFORE_POKE_WAKELOCK) { mCallback.userActivity(UNLOCK_PATTERN_WAKE_INTERVAL_MS); } else { // Give just a little extra time if they hit one of the first few dots mCallback.userActivity(UNLOCK_PATTERN_WAKE_INTERVAL_FIRST_DOTS_MS); } } public void onPatternDetected(List pattern) { if (mLockPatternUtils.checkPattern(pattern)) { mCallback.reportSuccessfulUnlockAttempt(); mLockPatternView.setDisplayMode(LockPatternView.DisplayMode.Correct); mTotalFailedPatternAttempts = 0; mCallback.dismiss(true); } else { if (pattern.size() > MIN_PATTERN_BEFORE_POKE_WAKELOCK) { mCallback.userActivity(UNLOCK_PATTERN_WAKE_INTERVAL_MS); } mLockPatternView.setDisplayMode(LockPatternView.DisplayMode.Wrong); if (pattern.size() >= LockPatternUtils.MIN_PATTERN_REGISTER_FAIL) { mTotalFailedPatternAttempts++; mFailedPatternAttemptsSinceLastTimeout++; mCallback.reportFailedUnlockAttempt(); } if (mFailedPatternAttemptsSinceLastTimeout >= LockPatternUtils.FAILED_ATTEMPTS_BEFORE_TIMEOUT) { long deadline = mLockPatternUtils.setLockoutAttemptDeadline(); handleAttemptLockout(deadline); } else { mSecurityMessageDisplay.setMessage(R.string.kg_wrong_pattern, true); mLockPatternView.postDelayed(mCancelPatternRunnable, PATTERN_CLEAR_TIMEOUT_MS); } } } } private void maybeEnableFallback(Context context) { // Ask the account manager if we have an account that can be used as a // fallback in case the user forgets his pattern. AccountAnalyzer accountAnalyzer = new AccountAnalyzer(AccountManager.get(context)); accountAnalyzer.start(); } private class AccountAnalyzer implements AccountManagerCallback { private final AccountManager mAccountManager; private final Account[] mAccounts; private int mAccountIndex; private AccountAnalyzer(AccountManager accountManager) { mAccountManager = accountManager; mAccounts = accountManager.getAccountsByTypeAsUser("com.google", new UserHandle(mLockPatternUtils.getCurrentUser())); } private void next() { // if we are ready to enable the fallback or if we depleted the list of accounts // then finish and get out if (mEnableFallback || mAccountIndex >= mAccounts.length) { return; } // lookup the confirmCredentials intent for the current account mAccountManager.confirmCredentialsAsUser(mAccounts[mAccountIndex], null, null, this, null, new UserHandle(mLockPatternUtils.getCurrentUser())); } public void start() { mEnableFallback = false; mAccountIndex = 0; next(); } public void run(AccountManagerFuture future) { try { Bundle result = future.getResult(); if (result.getParcelable(AccountManager.KEY_INTENT) != null) { mEnableFallback = true; } } catch (OperationCanceledException e) { // just skip the account if we are unable to query it } catch (IOException e) { // just skip the account if we are unable to query it } catch (AuthenticatorException e) { // just skip the account if we are unable to query it } finally { mAccountIndex++; next(); } } } private void handleAttemptLockout(long elapsedRealtimeDeadline) { mLockPatternView.clearPattern(); mLockPatternView.setEnabled(false); final long elapsedRealtime = SystemClock.elapsedRealtime(); if (mEnableFallback) { updateFooter(FooterMode.ForgotLockPattern); } mCountdownTimer = new CountDownTimer(elapsedRealtimeDeadline - elapsedRealtime, 1000) { @Override public void onTick(long millisUntilFinished) { final int secondsRemaining = (int) (millisUntilFinished / 1000); mSecurityMessageDisplay.setMessage( R.string.kg_too_many_failed_attempts_countdown, true, secondsRemaining); } @Override public void onFinish() { mLockPatternView.setEnabled(true); displayDefaultSecurityMessage(); // TODO mUnlockIcon.setVisibility(View.VISIBLE); mFailedPatternAttemptsSinceLastTimeout = 0; if (mEnableFallback) { updateFooter(FooterMode.ForgotLockPattern); } else { updateFooter(FooterMode.Normal); } } }.start(); } @Override public boolean needsInput() { return false; } @Override public void onPause() { if (mCountdownTimer != null) { mCountdownTimer.cancel(); mCountdownTimer = null; } } @Override public void onResume(int reason) { reset(); } @Override public KeyguardSecurityCallback getCallback() { return mCallback; } @Override public void showBouncer(int duration) { KeyguardSecurityViewHelper. showBouncer(mSecurityMessageDisplay, mEcaView, mBouncerFrame, duration); } @Override public void hideBouncer(int duration) { KeyguardSecurityViewHelper. hideBouncer(mSecurityMessageDisplay, mEcaView, mBouncerFrame, duration); } }