blob: e114b78bac2a007e49f98b646aa62779ff5b38c7 [file] [log] [blame]
/*
* 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.internal.policy.impl.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 com.android.internal.R;
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<LockPatternView.Cell> 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<LockPatternView.Cell> 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<Bundle> {
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<Bundle> 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);
}
}