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