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