PasswordUnlockScreen.java revision b0166bc62f781c3dd921560c0ebafd965603fb16
1/*
2 * Copyright (C) 2010 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 java.util.List;
20
21import android.app.admin.DevicePolicyManager;
22import android.content.Context;
23import android.content.res.Configuration;
24import android.graphics.Rect;
25
26import com.android.internal.widget.LockPatternUtils;
27import com.android.internal.widget.PasswordEntryKeyboardView;
28
29import android.os.CountDownTimer;
30import android.os.SystemClock;
31import android.security.KeyStore;
32import android.text.Editable;
33import android.text.InputType;
34import android.text.TextWatcher;
35import android.text.method.DigitsKeyListener;
36import android.text.method.TextKeyListener;
37import android.view.KeyEvent;
38import android.view.LayoutInflater;
39import android.view.View;
40import android.view.inputmethod.EditorInfo;
41import android.view.inputmethod.InputMethodInfo;
42import android.view.inputmethod.InputMethodManager;
43import android.view.inputmethod.InputMethodSubtype;
44import android.widget.EditText;
45import android.widget.LinearLayout;
46import android.widget.TextView;
47import android.widget.TextView.OnEditorActionListener;
48
49import com.android.internal.R;
50import com.android.internal.widget.PasswordEntryKeyboardHelper;
51
52/**
53 * Displays a dialer-like interface or alphanumeric (latin-1) key entry for the user to enter
54 * an unlock password
55 */
56public class PasswordUnlockScreen extends LinearLayout implements KeyguardScreen,
57        OnEditorActionListener {
58
59    private static final String TAG = "PasswordUnlockScreen";
60    private final KeyguardUpdateMonitor mUpdateMonitor;
61    private final KeyguardScreenCallback mCallback;
62
63    private boolean mIsAlpha;
64
65    private EditText mPasswordEntry;
66    private LockPatternUtils mLockPatternUtils;
67    private PasswordEntryKeyboardView mKeyboardView;
68    private PasswordEntryKeyboardHelper mKeyboardHelper;
69
70    private int mCreationOrientation;
71    private int mCreationHardKeyboardHidden;
72    private CountDownTimer mCountdownTimer;
73
74    private KeyguardStatusViewManager mStatusViewManager;
75    private boolean mUseSystemIME = true; // TODO: Make configurable
76    private boolean mResuming; // used to prevent poking the wakelock during onResume()
77
78    // To avoid accidental lockout due to events while the device in in the pocket, ignore
79    // any passwords with length less than or equal to this length.
80    private static final int MINIMUM_PASSWORD_LENGTH_BEFORE_REPORT = 3;
81
82    public PasswordUnlockScreen(Context context, Configuration configuration,
83            LockPatternUtils lockPatternUtils, KeyguardUpdateMonitor updateMonitor,
84            KeyguardScreenCallback callback) {
85        super(context);
86
87        mCreationHardKeyboardHidden = configuration.hardKeyboardHidden;
88        mCreationOrientation = configuration.orientation;
89        mUpdateMonitor = updateMonitor;
90        mCallback = callback;
91        mLockPatternUtils = lockPatternUtils;
92
93        LayoutInflater layoutInflater = LayoutInflater.from(context);
94        if (mCreationOrientation != Configuration.ORIENTATION_LANDSCAPE) {
95            layoutInflater.inflate(R.layout.keyguard_screen_password_portrait, this, true);
96        } else {
97            layoutInflater.inflate(R.layout.keyguard_screen_password_landscape, this, true);
98        }
99
100        mStatusViewManager = new KeyguardStatusViewManager(this, mUpdateMonitor, mLockPatternUtils,
101                mCallback);
102
103        final int quality = lockPatternUtils.getKeyguardStoredPasswordQuality();
104        mIsAlpha = DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC == quality
105                || DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC == quality
106                || DevicePolicyManager.PASSWORD_QUALITY_COMPLEX == quality;
107
108        mKeyboardView = (PasswordEntryKeyboardView) findViewById(R.id.keyboard);
109        mPasswordEntry = (EditText) findViewById(R.id.passwordEntry);
110        mPasswordEntry.setOnEditorActionListener(this);
111
112        mKeyboardHelper = new PasswordEntryKeyboardHelper(context, mKeyboardView, this, false);
113        if (mIsAlpha) {
114            // We always use the system IME for alpha keyboard, so hide lockscreen's soft keyboard
115            mKeyboardHelper.setKeyboardMode(PasswordEntryKeyboardHelper.KEYBOARD_MODE_ALPHA);
116            mKeyboardView.setVisibility(View.GONE);
117        } else {
118            // Use lockscreen's numeric keyboard if the physical keyboard isn't showing
119            mKeyboardHelper.setKeyboardMode(PasswordEntryKeyboardHelper.KEYBOARD_MODE_NUMERIC);
120            mKeyboardView.setVisibility(mCreationHardKeyboardHidden
121                    == Configuration.HARDKEYBOARDHIDDEN_NO ? View.INVISIBLE : View.VISIBLE);
122        }
123
124        mPasswordEntry.requestFocus();
125
126        // This allows keyboards with overlapping qwerty/numeric keys to choose just numeric keys.
127        if (mIsAlpha) {
128            mPasswordEntry.setKeyListener(TextKeyListener.getInstance());
129            mPasswordEntry.setInputType(InputType.TYPE_CLASS_TEXT
130                    | InputType.TYPE_TEXT_VARIATION_PASSWORD);
131            //mStatusViewManager.setHelpMessage(R.string.keyguard_password_enter_password_code,
132                    //KeyguardStatusViewManager.LOCK_ICON);
133        } else {
134            mPasswordEntry.setKeyListener(DigitsKeyListener.getInstance());
135            mPasswordEntry.setInputType(InputType.TYPE_CLASS_NUMBER
136                    | InputType.TYPE_NUMBER_VARIATION_PASSWORD);
137            //mStatusViewManager.setHelpMessage(R.string.keyguard_password_enter_pin_code,
138                    //KeyguardStatusViewManager.LOCK_ICON);
139        }
140
141        mKeyboardHelper.setVibratePattern(mLockPatternUtils.isTactileFeedbackEnabled() ?
142                com.android.internal.R.array.config_virtualKeyVibePattern : 0);
143
144        // Poke the wakelock any time the text is selected or modified
145        mPasswordEntry.setOnClickListener(new OnClickListener() {
146            public void onClick(View v) {
147                mCallback.pokeWakelock();
148            }
149        });
150        mPasswordEntry.addTextChangedListener(new TextWatcher() {
151            public void onTextChanged(CharSequence s, int start, int before, int count) {
152            }
153
154            public void beforeTextChanged(CharSequence s, int start, int count, int after) {
155            }
156
157            public void afterTextChanged(Editable s) {
158                if (!mResuming) {
159                    mCallback.pokeWakelock();
160                }
161            }
162        });
163
164        // If there's more than one IME, enable the IME switcher button
165        View switchImeButton = findViewById(R.id.switch_ime_button);
166        final InputMethodManager imm = (InputMethodManager) getContext().getSystemService(
167                Context.INPUT_METHOD_SERVICE);
168        if (mIsAlpha && switchImeButton != null && hasMultipleEnabledIMEsOrSubtypes(imm, false)) {
169            switchImeButton.setVisibility(View.VISIBLE);
170            switchImeButton.setOnClickListener(new OnClickListener() {
171                public void onClick(View v) {
172                    mCallback.pokeWakelock(); // Leave the screen on a bit longer
173                    imm.showInputMethodPicker();
174                }
175            });
176        }
177    }
178
179    /**
180     * Method adapted from com.android.inputmethod.latin.Utils
181     *
182     * @param imm The input method manager
183     * @param shouldIncludeAuxiliarySubtypes
184     * @return true if we have multiple IMEs to choose from
185     */
186    private boolean hasMultipleEnabledIMEsOrSubtypes(InputMethodManager imm,
187            final boolean shouldIncludeAuxiliarySubtypes) {
188        final List<InputMethodInfo> enabledImis = imm.getEnabledInputMethodList();
189
190        // Number of the filtered IMEs
191        int filteredImisCount = 0;
192
193        for (InputMethodInfo imi : enabledImis) {
194            // We can return true immediately after we find two or more filtered IMEs.
195            if (filteredImisCount > 1) return true;
196            final List<InputMethodSubtype> subtypes =
197                    imm.getEnabledInputMethodSubtypeList(imi, true);
198            // IMEs that have no subtypes should be counted.
199            if (subtypes.isEmpty()) {
200                ++filteredImisCount;
201                continue;
202            }
203
204            int auxCount = 0;
205            for (InputMethodSubtype subtype : subtypes) {
206                if (subtype.isAuxiliary()) {
207                    ++auxCount;
208                }
209            }
210            final int nonAuxCount = subtypes.size() - auxCount;
211
212            // IMEs that have one or more non-auxiliary subtypes should be counted.
213            // If shouldIncludeAuxiliarySubtypes is true, IMEs that have two or more auxiliary
214            // subtypes should be counted as well.
215            if (nonAuxCount > 0 || (shouldIncludeAuxiliarySubtypes && auxCount > 1)) {
216                ++filteredImisCount;
217                continue;
218            }
219        }
220
221        return filteredImisCount > 1
222        // imm.getEnabledInputMethodSubtypeList(null, false) will return the current IME's enabled
223        // input method subtype (The current IME should be LatinIME.)
224                || imm.getEnabledInputMethodSubtypeList(null, false).size() > 1;
225    }
226
227    @Override
228    protected boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) {
229        // send focus to the password field
230        return mPasswordEntry.requestFocus(direction, previouslyFocusedRect);
231    }
232
233    /** {@inheritDoc} */
234    public boolean needsInput() {
235        return mUseSystemIME && mIsAlpha;
236    }
237
238    /** {@inheritDoc} */
239    public void onPause() {
240        mStatusViewManager.onPause();
241    }
242
243    /** {@inheritDoc} */
244    public void onResume() {
245        mResuming = true;
246        // reset status
247        mStatusViewManager.onResume();
248
249        // start fresh
250        mPasswordEntry.setText("");
251        mPasswordEntry.requestFocus();
252
253        // if the user is currently locked out, enforce it.
254        long deadline = mLockPatternUtils.getLockoutAttemptDeadline();
255        if (deadline != 0) {
256            handleAttemptLockout(deadline);
257        }
258        mResuming = false;
259    }
260
261    /** {@inheritDoc} */
262    public void cleanUp() {
263        mUpdateMonitor.removeCallback(this);
264    }
265
266    private void verifyPasswordAndUnlock() {
267        String entry = mPasswordEntry.getText().toString();
268        if (mLockPatternUtils.checkPassword(entry)) {
269            mCallback.keyguardDone(true);
270            mCallback.reportSuccessfulUnlockAttempt();
271            mStatusViewManager.setInstructionText(null);
272            KeyStore.getInstance().password(entry);
273        } else if (entry.length() > MINIMUM_PASSWORD_LENGTH_BEFORE_REPORT ) {
274            // to avoid accidental lockout, only count attempts that are long enough to be a
275            // real password. This may require some tweaking.
276            mCallback.reportFailedUnlockAttempt();
277            if (0 == (mUpdateMonitor.getFailedAttempts()
278                    % LockPatternUtils.FAILED_ATTEMPTS_BEFORE_TIMEOUT)) {
279                long deadline = mLockPatternUtils.setLockoutAttemptDeadline();
280                handleAttemptLockout(deadline);
281            }
282            mStatusViewManager.setInstructionText(
283                    mContext.getString(R.string.lockscreen_password_wrong));
284        } else if (entry.length() > 0) {
285            mStatusViewManager.setInstructionText(
286                    mContext.getString(R.string.lockscreen_password_wrong));
287        }
288        mPasswordEntry.setText("");
289    }
290
291    // Prevent user from using the PIN/Password entry until scheduled deadline.
292    private void handleAttemptLockout(long elapsedRealtimeDeadline) {
293        mPasswordEntry.setEnabled(false);
294        mKeyboardView.setEnabled(false);
295        long elapsedRealtime = SystemClock.elapsedRealtime();
296        mCountdownTimer = new CountDownTimer(elapsedRealtimeDeadline - elapsedRealtime, 1000) {
297
298            @Override
299            public void onTick(long millisUntilFinished) {
300                int secondsRemaining = (int) (millisUntilFinished / 1000);
301                String instructions = getContext().getString(
302                        R.string.lockscreen_too_many_failed_attempts_countdown,
303                        secondsRemaining);
304                mStatusViewManager.setInstructionText(instructions);
305            }
306
307            @Override
308            public void onFinish() {
309                mPasswordEntry.setEnabled(true);
310                mKeyboardView.setEnabled(true);
311                mStatusViewManager.resetStatusInfo();
312                mCountdownTimer = null;
313            }
314        }.start();
315    }
316
317    @Override
318    public boolean onKeyDown(int keyCode, KeyEvent event) {
319        mCallback.pokeWakelock();
320        return false;
321    }
322
323    @Override
324    protected void onAttachedToWindow() {
325        super.onAttachedToWindow();
326        Configuration config = getResources().getConfiguration();
327        if (config.orientation != mCreationOrientation
328                || config.hardKeyboardHidden != mCreationHardKeyboardHidden) {
329            mCallback.recreateMe(config);
330        }
331    }
332
333    /** {@inheritDoc} */
334    @Override
335    protected void onConfigurationChanged(Configuration newConfig) {
336        super.onConfigurationChanged(newConfig);
337        if (newConfig.orientation != mCreationOrientation
338                || newConfig.hardKeyboardHidden != mCreationHardKeyboardHidden) {
339            mCallback.recreateMe(newConfig);
340        }
341    }
342
343    public void onKeyboardChange(boolean isKeyboardOpen) {
344        // Don't show the soft keyboard when the real keyboard is open
345        mKeyboardView.setVisibility(isKeyboardOpen ? View.INVISIBLE : View.VISIBLE);
346    }
347
348    public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
349        // Check if this was the result of hitting the enter key
350        if (actionId == EditorInfo.IME_NULL || actionId == EditorInfo.IME_ACTION_DONE) {
351            verifyPasswordAndUnlock();
352            return true;
353        }
354        return false;
355    }
356}
357