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.app.Dialog;
25import android.app.ProgressDialog;
26import android.content.Context;
27import android.content.Intent;
28import android.graphics.Rect;
29import android.os.Bundle;
30import android.os.UserHandle;
31import android.text.Editable;
32import android.text.InputFilter;
33import android.text.LoginFilter;
34import android.text.TextWatcher;
35import android.util.AttributeSet;
36import android.view.KeyEvent;
37import android.view.View;
38import android.view.WindowManager;
39import android.widget.Button;
40import android.widget.EditText;
41import android.widget.LinearLayout;
42
43import com.android.internal.widget.LockPatternUtils;
44
45import java.io.IOException;
46
47/**
48 * When the user forgets their password a bunch of times, we fall back on their
49 * account's login/password to unlock the phone (and reset their lock pattern).
50 */
51public class KeyguardAccountView extends LinearLayout implements KeyguardSecurityView,
52        View.OnClickListener, TextWatcher {
53    private static final int AWAKE_POKE_MILLIS = 30000;
54    private static final String LOCK_PATTERN_PACKAGE = "com.android.settings";
55    private static final String LOCK_PATTERN_CLASS = LOCK_PATTERN_PACKAGE + ".ChooseLockGeneric";
56
57    private KeyguardSecurityCallback mCallback;
58    private LockPatternUtils mLockPatternUtils;
59    private EditText mLogin;
60    private EditText mPassword;
61    private Button mOk;
62    public boolean mEnableFallback;
63    private SecurityMessageDisplay mSecurityMessageDisplay;
64
65    /**
66     * Shown while making asynchronous check of password.
67     */
68    private ProgressDialog mCheckingDialog;
69
70    public KeyguardAccountView(Context context) {
71        this(context, null, 0);
72    }
73
74    public KeyguardAccountView(Context context, AttributeSet attrs) {
75        this(context, attrs, 0);
76    }
77
78    public KeyguardAccountView(Context context, AttributeSet attrs, int defStyle) {
79        super(context, attrs, defStyle);
80        mLockPatternUtils = new LockPatternUtils(getContext());
81    }
82
83    @Override
84    protected void onFinishInflate() {
85        super.onFinishInflate();
86
87        mLogin = (EditText) findViewById(R.id.login);
88        mLogin.setFilters(new InputFilter[] { new LoginFilter.UsernameFilterGeneric() } );
89        mLogin.addTextChangedListener(this);
90
91        mPassword = (EditText) findViewById(R.id.password);
92        mPassword.addTextChangedListener(this);
93
94        mOk = (Button) findViewById(R.id.ok);
95        mOk.setOnClickListener(this);
96
97        mSecurityMessageDisplay = new KeyguardMessageArea.Helper(this);
98        reset();
99    }
100
101    public void setKeyguardCallback(KeyguardSecurityCallback callback) {
102        mCallback = callback;
103    }
104
105    public void setLockPatternUtils(LockPatternUtils utils) {
106        mLockPatternUtils = utils;
107    }
108
109    public KeyguardSecurityCallback getCallback() {
110        return mCallback;
111    }
112
113
114    public void afterTextChanged(Editable s) {
115    }
116
117    public void beforeTextChanged(CharSequence s, int start, int count, int after) {
118    }
119
120    public void onTextChanged(CharSequence s, int start, int before, int count) {
121        if (mCallback != null) {
122            mCallback.userActivity(AWAKE_POKE_MILLIS);
123        }
124    }
125
126    @Override
127    protected boolean onRequestFocusInDescendants(int direction,
128            Rect previouslyFocusedRect) {
129        // send focus to the login field
130        return mLogin.requestFocus(direction, previouslyFocusedRect);
131    }
132
133    public boolean needsInput() {
134        return true;
135    }
136
137    public void reset() {
138        // start fresh
139        mLogin.setText("");
140        mPassword.setText("");
141        mLogin.requestFocus();
142        boolean permLocked = mLockPatternUtils.isPermanentlyLocked();
143        mSecurityMessageDisplay.setMessage(permLocked ? R.string.kg_login_too_many_attempts :
144            R.string.kg_login_instructions, permLocked ? true : false);
145    }
146
147    /** {@inheritDoc} */
148    public void cleanUp() {
149        if (mCheckingDialog != null) {
150            mCheckingDialog.hide();
151        }
152        mCallback = null;
153        mLockPatternUtils = null;
154    }
155
156    public void onClick(View v) {
157        mCallback.userActivity(0);
158        if (v == mOk) {
159            asyncCheckPassword();
160        }
161    }
162
163    private void postOnCheckPasswordResult(final boolean success) {
164        // ensure this runs on UI thread
165        mLogin.post(new Runnable() {
166            public void run() {
167                if (success) {
168                    // clear out forgotten password
169                    mLockPatternUtils.setPermanentlyLocked(false);
170                    mLockPatternUtils.setLockPatternEnabled(false);
171                    mLockPatternUtils.saveLockPattern(null);
172
173                    // launch the 'choose lock pattern' activity so
174                    // the user can pick a new one if they want to
175                    Intent intent = new Intent();
176                    intent.setClassName(LOCK_PATTERN_PACKAGE, LOCK_PATTERN_CLASS);
177                    intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
178                    mContext.startActivityAsUser(intent,
179                            new UserHandle(mLockPatternUtils.getCurrentUser()));
180                    mCallback.reportSuccessfulUnlockAttempt();
181
182                    // dismiss keyguard
183                    mCallback.dismiss(true);
184                } else {
185                    mSecurityMessageDisplay.setMessage(R.string.kg_login_invalid_input, true);
186                    mPassword.setText("");
187                    mCallback.reportFailedUnlockAttempt();
188                }
189            }
190        });
191    }
192
193    @Override
194    public boolean dispatchKeyEvent(KeyEvent event) {
195        if (event.getAction() == KeyEvent.ACTION_DOWN
196                && event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
197            if (mLockPatternUtils.isPermanentlyLocked()) {
198                mCallback.dismiss(false);
199            } else {
200                // TODO: mCallback.forgotPattern(false);
201            }
202            return true;
203        }
204        return super.dispatchKeyEvent(event);
205    }
206
207    /**
208     * Given the string the user entered in the 'username' field, find
209     * the stored account that they probably intended.  Prefer, in order:
210     *
211     *   - an exact match for what was typed, or
212     *   - a case-insensitive match for what was typed, or
213     *   - if they didn't include a domain, an exact match of the username, or
214     *   - if they didn't include a domain, a case-insensitive
215     *     match of the username.
216     *
217     * If there is a tie for the best match, choose neither --
218     * the user needs to be more specific.
219     *
220     * @return an account name from the database, or null if we can't
221     * find a single best match.
222     */
223    private Account findIntendedAccount(String username) {
224        Account[] accounts = AccountManager.get(mContext).getAccountsByTypeAsUser("com.google",
225                new UserHandle(mLockPatternUtils.getCurrentUser()));
226
227        // Try to figure out which account they meant if they
228        // typed only the username (and not the domain), or got
229        // the case wrong.
230
231        Account bestAccount = null;
232        int bestScore = 0;
233        for (Account a: accounts) {
234            int score = 0;
235            if (username.equals(a.name)) {
236                score = 4;
237            } else if (username.equalsIgnoreCase(a.name)) {
238                score = 3;
239            } else if (username.indexOf('@') < 0) {
240                int i = a.name.indexOf('@');
241                if (i >= 0) {
242                    String aUsername = a.name.substring(0, i);
243                    if (username.equals(aUsername)) {
244                        score = 2;
245                    } else if (username.equalsIgnoreCase(aUsername)) {
246                        score = 1;
247                    }
248                }
249            }
250            if (score > bestScore) {
251                bestAccount = a;
252                bestScore = score;
253            } else if (score == bestScore) {
254                bestAccount = null;
255            }
256        }
257        return bestAccount;
258    }
259
260    private void asyncCheckPassword() {
261        mCallback.userActivity(AWAKE_POKE_MILLIS);
262        final String login = mLogin.getText().toString();
263        final String password = mPassword.getText().toString();
264        Account account = findIntendedAccount(login);
265        if (account == null) {
266            postOnCheckPasswordResult(false);
267            return;
268        }
269        getProgressDialog().show();
270        Bundle options = new Bundle();
271        options.putString(AccountManager.KEY_PASSWORD, password);
272        AccountManager.get(mContext).confirmCredentialsAsUser(account, options, null /* activity */,
273                new AccountManagerCallback<Bundle>() {
274            public void run(AccountManagerFuture<Bundle> future) {
275                try {
276                    mCallback.userActivity(AWAKE_POKE_MILLIS);
277                    final Bundle result = future.getResult();
278                    final boolean verified = result.getBoolean(AccountManager.KEY_BOOLEAN_RESULT);
279                    postOnCheckPasswordResult(verified);
280                } catch (OperationCanceledException e) {
281                    postOnCheckPasswordResult(false);
282                } catch (IOException e) {
283                    postOnCheckPasswordResult(false);
284                } catch (AuthenticatorException e) {
285                    postOnCheckPasswordResult(false);
286                } finally {
287                    mLogin.post(new Runnable() {
288                        public void run() {
289                            getProgressDialog().hide();
290                        }
291                    });
292                }
293            }
294        }, null /* handler */, new UserHandle(mLockPatternUtils.getCurrentUser()));
295    }
296
297    private Dialog getProgressDialog() {
298        if (mCheckingDialog == null) {
299            mCheckingDialog = new ProgressDialog(mContext);
300            mCheckingDialog.setMessage(
301                    mContext.getString(R.string.kg_login_checking_password));
302            mCheckingDialog.setIndeterminate(true);
303            mCheckingDialog.setCancelable(false);
304            mCheckingDialog.getWindow().setType(
305                    WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG);
306        }
307        return mCheckingDialog;
308    }
309
310    @Override
311    public void onPause() {
312
313    }
314
315    @Override
316    public void onResume(int reason) {
317        reset();
318    }
319
320    @Override
321    public void showUsabilityHint() {
322    }
323
324    @Override
325    public void showBouncer(int duration) {
326    }
327
328    @Override
329    public void hideBouncer(int duration) {
330    }
331}
332
333