/*
* Copyright (C) 2014 The CyanogenMod 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.settings.cyanogenmod;
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.Activity;
import android.app.ActivityManager;
import android.app.Dialog;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.SharedPreferences;
import android.graphics.Rect;
import android.os.Bundle;
import android.os.UserHandle;
import android.preference.PreferenceManager;
import android.text.InputFilter;
import android.text.LoginFilter;
import android.util.AttributeSet;
import android.view.View;
import android.view.WindowManager;
import android.view.inputmethod.InputMethodManager;
import android.widget.Button;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.Toast;
import com.android.internal.widget.LockPatternUtils;
import com.android.settings.R;
import com.android.settings.applications.LockPatternActivity;
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 protected apps (and reset their lock pattern).
*/
public class ProtectedAccountView extends LinearLayout implements View.OnClickListener {
public static interface OnNotifyAccountReset {
void onNotifyAccountReset();
}
private EditText mLogin;
private EditText mPassword;
private Button mOk;
private Context mContext;
private LockPatternUtils mLockPatternUtils;
private OnNotifyAccountReset mNotifyAccountResetCb;
/**
* Shown while making asynchronous check of password.
*/
private ProgressDialog mCheckingDialog;
public ProtectedAccountView(Context context) {
this(context, null);
}
public ProtectedAccountView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public ProtectedAccountView(Context context, AttributeSet st, int ds) {
super(context, st, ds);
mContext = context;
mLockPatternUtils = new LockPatternUtils(mContext);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mLogin = (EditText) findViewById(R.id.login);
mLogin.setFilters(new InputFilter[] { new LoginFilter.UsernameFilterGeneric() } );
mPassword = (EditText) findViewById(R.id.password);
mOk = (Button) findViewById(R.id.ok);
mOk.setOnClickListener(this);
reset();
}
@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 setOnNotifyAccountResetCb(OnNotifyAccountReset callback) {
this.mNotifyAccountResetCb = callback;
}
public void clearFocusOnInput() {
mLogin.clearFocus();
mPassword.clearFocus();
// hide keyboard
final InputMethodManager imm = (InputMethodManager)
mContext.getSystemService(Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(mLogin.getWindowToken(), 0);
imm.hideSoftInputFromWindow(mPassword.getWindowToken(), 0);
}
public void reset() {
mLogin.setText("");
mPassword.setText("");
mLogin.requestFocus();
}
/** {@inheritDoc} */
public void cleanUp() {
if (mCheckingDialog != null) {
mCheckingDialog.hide();
}
}
public void onClick(View v) {
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) {
Activity baseActivity = (Activity) mContext;
if (!baseActivity.isFinishing()) {
// Remove pattern
SharedPreferences prefs = PreferenceManager
.getDefaultSharedPreferences(mContext);
SharedPreferences.Editor editor = prefs.edit();
editor.remove(LockPatternActivity.PATTERN_LOCK_PROTECTED_APPS);
editor.commit();
if (mNotifyAccountResetCb != null) {
mNotifyAccountResetCb.onNotifyAccountReset();
} else {
baseActivity.setResult(Activity.RESULT_OK);
baseActivity.finish();
}
}
} else {
Toast.makeText(mContext,
getResources().getString(
R.string.pa_login_incorrect_login),
Toast.LENGTH_SHORT).show();
mPassword.setText("");
}
}
});
}
/**
* 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(ActivityManager.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() {
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<Bundle>() {
public void run(AccountManagerFuture<Bundle> future) {
try {
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(ActivityManager.getCurrentUser()));
}
private Dialog getProgressDialog() {
if (mCheckingDialog == null) {
mCheckingDialog = new ProgressDialog(mContext);
mCheckingDialog.setMessage(
mContext.getString(R.string.pa_login_checking_password));
mCheckingDialog.setIndeterminate(true);
mCheckingDialog.setCancelable(false);
mCheckingDialog.getWindow().setType(
WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG);
}
return mCheckingDialog;
}
}