/* * 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.app.Dialog; import android.app.ProgressDialog; import android.content.Context; import android.content.Intent; import android.graphics.Rect; import android.os.Bundle; import android.os.UserHandle; import android.text.Editable; import android.text.InputFilter; import android.text.LoginFilter; import android.text.TextWatcher; import android.util.AttributeSet; import android.view.KeyEvent; import android.view.View; import android.view.WindowManager; import android.widget.Button; import android.widget.EditText; import android.widget.LinearLayout; import com.android.internal.widget.LockPatternUtils; import com.android.internal.R; import java.io.IOException; /** * When the user forgets their password a bunch of times, we fall back on their * account's login/password to unlock the phone (and reset their lock pattern). */ public class KeyguardAccountView extends LinearLayout implements KeyguardSecurityView, View.OnClickListener, TextWatcher { private static final int AWAKE_POKE_MILLIS = 30000; private static final String LOCK_PATTERN_PACKAGE = "com.android.settings"; private static final String LOCK_PATTERN_CLASS = LOCK_PATTERN_PACKAGE + ".ChooseLockGeneric"; private KeyguardSecurityCallback mCallback; private LockPatternUtils mLockPatternUtils; private EditText mLogin; private EditText mPassword; private Button mOk; public boolean mEnableFallback; private SecurityMessageDisplay mSecurityMessageDisplay; /** * Shown while making asynchronous check of password. */ private ProgressDialog mCheckingDialog; public KeyguardAccountView(Context context) { this(context, null, 0); } public KeyguardAccountView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public KeyguardAccountView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); mLockPatternUtils = new LockPatternUtils(getContext()); } @Override protected void onFinishInflate() { super.onFinishInflate(); mLogin = (EditText) findViewById(R.id.login); mLogin.setFilters(new InputFilter[] { new LoginFilter.UsernameFilterGeneric() } ); mLogin.addTextChangedListener(this); mPassword = (EditText) findViewById(R.id.password); mPassword.addTextChangedListener(this); mOk = (Button) findViewById(R.id.ok); mOk.setOnClickListener(this); mSecurityMessageDisplay = new KeyguardMessageArea.Helper(this); reset(); } public void setKeyguardCallback(KeyguardSecurityCallback callback) { mCallback = callback; } public void setLockPatternUtils(LockPatternUtils utils) { mLockPatternUtils = utils; } public KeyguardSecurityCallback getCallback() { return mCallback; } public void afterTextChanged(Editable s) { } public void beforeTextChanged(CharSequence s, int start, int count, int after) { } public void onTextChanged(CharSequence s, int start, int before, int count) { if (mCallback != null) { mCallback.userActivity(AWAKE_POKE_MILLIS); } } @Override protected boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) { // send focus to the login field return mLogin.requestFocus(direction, previouslyFocusedRect); } public boolean needsInput() { return true; } public void reset() { // start fresh mLogin.setText(""); mPassword.setText(""); mLogin.requestFocus(); boolean permLocked = mLockPatternUtils.isPermanentlyLocked(); mSecurityMessageDisplay.setMessage(permLocked ? R.string.kg_login_too_many_attempts : R.string.kg_login_instructions, permLocked ? true : false); } /** {@inheritDoc} */ public void cleanUp() { if (mCheckingDialog != null) { mCheckingDialog.hide(); } mCallback = null; mLockPatternUtils = null; } public void onClick(View v) { mCallback.userActivity(0); if (v == mOk) { asyncCheckPassword(); } } private void postOnCheckPasswordResult(final boolean success) { // ensure this runs on UI thread mLogin.post(new Runnable() { public void run() { if (success) { // clear out forgotten password mLockPatternUtils.setPermanentlyLocked(false); mLockPatternUtils.setLockPatternEnabled(false); mLockPatternUtils.saveLockPattern(null); // launch the 'choose lock pattern' activity so // the user can pick a new one if they want to Intent intent = new Intent(); intent.setClassName(LOCK_PATTERN_PACKAGE, LOCK_PATTERN_CLASS); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); mContext.startActivityAsUser(intent, new UserHandle(mLockPatternUtils.getCurrentUser())); mCallback.reportSuccessfulUnlockAttempt(); // dismiss keyguard mCallback.dismiss(true); } else { mSecurityMessageDisplay.setMessage(R.string.kg_login_invalid_input, true); mPassword.setText(""); mCallback.reportFailedUnlockAttempt(); } } }); } @Override public boolean dispatchKeyEvent(KeyEvent event) { if (event.getAction() == KeyEvent.ACTION_DOWN && event.getKeyCode() == KeyEvent.KEYCODE_BACK) { if (mLockPatternUtils.isPermanentlyLocked()) { mCallback.dismiss(false); } else { // TODO: mCallback.forgotPattern(false); } return true; } return super.dispatchKeyEvent(event); } /** * Given the string the user entered in the 'username' field, find * the stored account that they probably intended. Prefer, in order: * * - an exact match for what was typed, or * - a case-insensitive match for what was typed, or * - if they didn't include a domain, an exact match of the username, or * - if they didn't include a domain, a case-insensitive * match of the username. * * If there is a tie for the best match, choose neither -- * the user needs to be more specific. * * @return an account name from the database, or null if we can't * find a single best match. */ private Account findIntendedAccount(String username) { Account[] accounts = AccountManager.get(mContext).getAccountsByTypeAsUser("com.google", new UserHandle(mLockPatternUtils.getCurrentUser())); // Try to figure out which account they meant if they // typed only the username (and not the domain), or got // the case wrong. Account bestAccount = null; int bestScore = 0; for (Account a: accounts) { int score = 0; if (username.equals(a.name)) { score = 4; } else if (username.equalsIgnoreCase(a.name)) { score = 3; } else if (username.indexOf('@') < 0) { int i = a.name.indexOf('@'); if (i >= 0) { String aUsername = a.name.substring(0, i); if (username.equals(aUsername)) { score = 2; } else if (username.equalsIgnoreCase(aUsername)) { score = 1; } } } if (score > bestScore) { bestAccount = a; bestScore = score; } else if (score == bestScore) { bestAccount = null; } } return bestAccount; } private void asyncCheckPassword() { mCallback.userActivity(AWAKE_POKE_MILLIS); final String login = mLogin.getText().toString(); final String password = mPassword.getText().toString(); Account account = findIntendedAccount(login); if (account == null) { postOnCheckPasswordResult(false); return; } getProgressDialog().show(); Bundle options = new Bundle(); options.putString(AccountManager.KEY_PASSWORD, password); AccountManager.get(mContext).confirmCredentialsAsUser(account, options, null /* activity */, new AccountManagerCallback() { public void run(AccountManagerFuture future) { try { mCallback.userActivity(AWAKE_POKE_MILLIS); final Bundle result = future.getResult(); final boolean verified = result.getBoolean(AccountManager.KEY_BOOLEAN_RESULT); postOnCheckPasswordResult(verified); } catch (OperationCanceledException e) { postOnCheckPasswordResult(false); } catch (IOException e) { postOnCheckPasswordResult(false); } catch (AuthenticatorException e) { postOnCheckPasswordResult(false); } finally { mLogin.post(new Runnable() { public void run() { getProgressDialog().hide(); } }); } } }, null /* handler */, new UserHandle(mLockPatternUtils.getCurrentUser())); } private Dialog getProgressDialog() { if (mCheckingDialog == null) { mCheckingDialog = new ProgressDialog(mContext); mCheckingDialog.setMessage( mContext.getString(R.string.kg_login_checking_password)); mCheckingDialog.setIndeterminate(true); mCheckingDialog.setCancelable(false); mCheckingDialog.getWindow().setType( WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG); } return mCheckingDialog; } @Override public void onPause() { } @Override public void onResume(int reason) { reset(); } @Override public void showUsabilityHint() { } @Override public void showBouncer(int duration) { } @Override public void hideBouncer(int duration) { } }