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