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