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