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