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