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.settings;
18
19import android.app.Fragment;
20import android.app.admin.DevicePolicyManager;
21import android.content.Context;
22import android.content.Intent;
23import android.os.AsyncTask;
24import android.os.Bundle;
25import android.os.CountDownTimer;
26import android.os.SystemClock;
27import android.os.UserManager;
28import android.os.storage.StorageManager;
29import android.text.InputType;
30import android.text.TextUtils;
31import android.view.KeyEvent;
32import android.view.LayoutInflater;
33import android.view.View;
34import android.view.View.OnClickListener;
35import android.view.ViewGroup;
36import android.view.animation.AnimationUtils;
37import android.view.inputmethod.EditorInfo;
38import android.view.inputmethod.InputMethodManager;
39import android.widget.TextView;
40import android.widget.TextView.OnEditorActionListener;
41
42import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
43import com.android.internal.widget.LockPatternChecker;
44import com.android.internal.widget.LockPatternUtils;
45import com.android.internal.widget.TextViewInputDisabler;
46import com.android.settingslib.animation.AppearAnimationUtils;
47import com.android.settingslib.animation.DisappearAnimationUtils;
48
49import java.util.ArrayList;
50
51public class ConfirmLockPassword extends ConfirmDeviceCredentialBaseActivity {
52
53    // The index of the array is isStrongAuth << 2 + isProfile << 1 + isAlpha.
54    private static final int[] DETAIL_TEXTS = new int[] {
55        R.string.lockpassword_confirm_your_pin_generic,
56        R.string.lockpassword_confirm_your_password_generic,
57        R.string.lockpassword_confirm_your_pin_generic_profile,
58        R.string.lockpassword_confirm_your_password_generic_profile,
59        R.string.lockpassword_strong_auth_required_reason_restart_device_pin,
60        R.string.lockpassword_strong_auth_required_reason_restart_device_password,
61        R.string.lockpassword_strong_auth_required_reason_restart_work_pin,
62        R.string.lockpassword_strong_auth_required_reason_restart_work_password,
63    };
64
65    public static class InternalActivity extends ConfirmLockPassword {
66    }
67
68    @Override
69    public Intent getIntent() {
70        Intent modIntent = new Intent(super.getIntent());
71        modIntent.putExtra(EXTRA_SHOW_FRAGMENT, ConfirmLockPasswordFragment.class.getName());
72        return modIntent;
73    }
74
75    @Override
76    protected boolean isValidFragment(String fragmentName) {
77        if (ConfirmLockPasswordFragment.class.getName().equals(fragmentName)) return true;
78        return false;
79    }
80
81    @Override
82    public void onWindowFocusChanged(boolean hasFocus) {
83        super.onWindowFocusChanged(hasFocus);
84        Fragment fragment = getFragmentManager().findFragmentById(R.id.main_content);
85        if (fragment != null && fragment instanceof ConfirmLockPasswordFragment) {
86            ((ConfirmLockPasswordFragment)fragment).onWindowFocusChanged(hasFocus);
87        }
88    }
89
90    public static class ConfirmLockPasswordFragment extends ConfirmDeviceCredentialBaseFragment
91            implements OnClickListener, OnEditorActionListener,
92            CredentialCheckResultTracker.Listener {
93        private static final long ERROR_MESSAGE_TIMEOUT = 3000;
94        private static final String FRAGMENT_TAG_CHECK_LOCK_RESULT = "check_lock_result";
95        private TextView mPasswordEntry;
96        private TextViewInputDisabler mPasswordEntryInputDisabler;
97        private AsyncTask<?, ?, ?> mPendingLockCheck;
98        private CredentialCheckResultTracker mCredentialCheckResultTracker;
99        private boolean mDisappearing = false;
100        private TextView mHeaderTextView;
101        private TextView mDetailsTextView;
102        private CountDownTimer mCountdownTimer;
103        private boolean mIsAlpha;
104        private InputMethodManager mImm;
105        private boolean mUsingFingerprint = false;
106        private AppearAnimationUtils mAppearAnimationUtils;
107        private DisappearAnimationUtils mDisappearAnimationUtils;
108        private boolean mBlockImm;
109
110        // required constructor for fragments
111        public ConfirmLockPasswordFragment() {
112
113        }
114
115        @Override
116        public void onCreate(Bundle savedInstanceState) {
117            super.onCreate(savedInstanceState);
118        }
119
120        @Override
121        public View onCreateView(LayoutInflater inflater, ViewGroup container,
122                Bundle savedInstanceState) {
123            final int storedQuality = mLockPatternUtils.getKeyguardStoredPasswordQuality(
124                    mEffectiveUserId);
125
126            ConfirmLockPassword activity = (ConfirmLockPassword) getActivity();
127            View view = inflater.inflate(
128                    activity.getConfirmCredentialTheme() == ConfirmCredentialTheme.INTERNAL
129                            ? R.layout.confirm_lock_password_internal
130                            : R.layout.confirm_lock_password,
131                    container,
132                    false);
133
134            mPasswordEntry = (TextView) view.findViewById(R.id.password_entry);
135            mPasswordEntry.setOnEditorActionListener(this);
136            mPasswordEntryInputDisabler = new TextViewInputDisabler(mPasswordEntry);
137
138            mHeaderTextView = (TextView) view.findViewById(R.id.headerText);
139            mDetailsTextView = (TextView) view.findViewById(R.id.detailsText);
140            mErrorTextView = (TextView) view.findViewById(R.id.errorText);
141            mIsAlpha = DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC == storedQuality
142                    || DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC == storedQuality
143                    || DevicePolicyManager.PASSWORD_QUALITY_COMPLEX == storedQuality
144                    || DevicePolicyManager.PASSWORD_QUALITY_MANAGED == storedQuality;
145
146            mImm = (InputMethodManager) getActivity().getSystemService(
147                    Context.INPUT_METHOD_SERVICE);
148
149            Intent intent = getActivity().getIntent();
150            if (intent != null) {
151                CharSequence headerMessage = intent.getCharSequenceExtra(
152                        ConfirmDeviceCredentialBaseFragment.HEADER_TEXT);
153                CharSequence detailsMessage = intent.getCharSequenceExtra(
154                        ConfirmDeviceCredentialBaseFragment.DETAILS_TEXT);
155                if (TextUtils.isEmpty(headerMessage)) {
156                    headerMessage = getString(getDefaultHeader());
157                }
158                if (TextUtils.isEmpty(detailsMessage)) {
159                    detailsMessage = getString(getDefaultDetails());
160                }
161                mHeaderTextView.setText(headerMessage);
162                mDetailsTextView.setText(detailsMessage);
163            }
164            int currentType = mPasswordEntry.getInputType();
165            mPasswordEntry.setInputType(mIsAlpha ? currentType
166                    : (InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_VARIATION_PASSWORD));
167            mAppearAnimationUtils = new AppearAnimationUtils(getContext(),
168                    220, 2f /* translationScale */, 1f /* delayScale*/,
169                    AnimationUtils.loadInterpolator(getContext(),
170                            android.R.interpolator.linear_out_slow_in));
171            mDisappearAnimationUtils = new DisappearAnimationUtils(getContext(),
172                    110, 1f /* translationScale */,
173                    0.5f /* delayScale */, AnimationUtils.loadInterpolator(
174                            getContext(), android.R.interpolator.fast_out_linear_in));
175            setAccessibilityTitle(mHeaderTextView.getText());
176
177            mCredentialCheckResultTracker = (CredentialCheckResultTracker) getFragmentManager()
178                    .findFragmentByTag(FRAGMENT_TAG_CHECK_LOCK_RESULT);
179            if (mCredentialCheckResultTracker == null) {
180                mCredentialCheckResultTracker = new CredentialCheckResultTracker();
181                getFragmentManager().beginTransaction().add(mCredentialCheckResultTracker,
182                        FRAGMENT_TAG_CHECK_LOCK_RESULT).commit();
183            }
184
185            return view;
186        }
187
188        private int getDefaultHeader() {
189            return mIsAlpha ? R.string.lockpassword_confirm_your_password_header
190                    : R.string.lockpassword_confirm_your_pin_header;
191        }
192
193        private int getDefaultDetails() {
194            boolean isStrongAuthRequired = isFingerprintDisallowedByStrongAuth();
195            boolean isProfile = UserManager.get(getActivity()).isManagedProfile(mEffectiveUserId);
196            // Map boolean flags to an index by isStrongAuth << 2 + isProfile << 1 + isAlpha.
197            int index = ((isStrongAuthRequired ? 1 : 0) << 2) + ((isProfile ? 1 : 0) << 1)
198                    + (mIsAlpha ? 1 : 0);
199            return DETAIL_TEXTS[index];
200        }
201
202        private int getErrorMessage() {
203            return mIsAlpha ? R.string.lockpassword_invalid_password
204                    : R.string.lockpassword_invalid_pin;
205        }
206
207        @Override
208        protected int getLastTryErrorMessage() {
209            return mIsAlpha ? R.string.lock_profile_wipe_warning_content_password
210                    : R.string.lock_profile_wipe_warning_content_pin;
211        }
212
213        @Override
214        public void prepareEnterAnimation() {
215            super.prepareEnterAnimation();
216            mHeaderTextView.setAlpha(0f);
217            mDetailsTextView.setAlpha(0f);
218            mCancelButton.setAlpha(0f);
219            mPasswordEntry.setAlpha(0f);
220            mFingerprintIcon.setAlpha(0f);
221            mBlockImm = true;
222        }
223
224        private View[] getActiveViews() {
225            ArrayList<View> result = new ArrayList<>();
226            result.add(mHeaderTextView);
227            result.add(mDetailsTextView);
228            if (mCancelButton.getVisibility() == View.VISIBLE) {
229                result.add(mCancelButton);
230            }
231            result.add(mPasswordEntry);
232            if (mFingerprintIcon.getVisibility() == View.VISIBLE) {
233                result.add(mFingerprintIcon);
234            }
235            return result.toArray(new View[] {});
236        }
237
238        @Override
239        public void startEnterAnimation() {
240            super.startEnterAnimation();
241            mAppearAnimationUtils.startAnimation(getActiveViews(), new Runnable() {
242                @Override
243                public void run() {
244                    mBlockImm = false;
245                    resetState();
246                }
247            });
248        }
249
250        @Override
251        public void onPause() {
252            super.onPause();
253            if (mCountdownTimer != null) {
254                mCountdownTimer.cancel();
255                mCountdownTimer = null;
256            }
257            mCredentialCheckResultTracker.setListener(null);
258        }
259
260        @Override
261        public int getMetricsCategory() {
262            return MetricsEvent.CONFIRM_LOCK_PASSWORD;
263        }
264
265        @Override
266        public void onResume() {
267            super.onResume();
268            long deadline = mLockPatternUtils.getLockoutAttemptDeadline(mEffectiveUserId);
269            if (deadline != 0) {
270                mCredentialCheckResultTracker.clearResult();
271                handleAttemptLockout(deadline);
272            } else {
273                resetState();
274                mErrorTextView.setText("");
275                if (isProfileChallenge()) {
276                    updateErrorMessage(mLockPatternUtils.getCurrentFailedPasswordAttempts(
277                            mEffectiveUserId));
278                }
279            }
280            mCredentialCheckResultTracker.setListener(this);
281        }
282
283        @Override
284        protected void authenticationSucceeded() {
285            mCredentialCheckResultTracker.setResult(true, new Intent(), 0, mEffectiveUserId);
286        }
287
288        @Override
289        public void onFingerprintIconVisibilityChanged(boolean visible) {
290            mUsingFingerprint = visible;
291        }
292
293        private void resetState() {
294            if (mBlockImm) return;
295            mPasswordEntry.setEnabled(true);
296            mPasswordEntryInputDisabler.setInputEnabled(true);
297            if (shouldAutoShowSoftKeyboard()) {
298                mImm.showSoftInput(mPasswordEntry, InputMethodManager.SHOW_IMPLICIT);
299            }
300        }
301
302        private boolean shouldAutoShowSoftKeyboard() {
303            return mPasswordEntry.isEnabled() && !mUsingFingerprint;
304        }
305
306        public void onWindowFocusChanged(boolean hasFocus) {
307            if (!hasFocus || mBlockImm) {
308                return;
309            }
310            // Post to let window focus logic to finish to allow soft input show/hide properly.
311            mPasswordEntry.post(new Runnable() {
312                @Override
313                public void run() {
314                    if (shouldAutoShowSoftKeyboard()) {
315                        resetState();
316                        return;
317                    }
318
319                    mImm.hideSoftInputFromWindow(mPasswordEntry.getWindowToken(),
320                            InputMethodManager.HIDE_IMPLICIT_ONLY);
321                }
322            });
323        }
324
325        private void handleNext() {
326            if (mPendingLockCheck != null || mDisappearing) {
327                return;
328            }
329
330            final String pin = mPasswordEntry.getText().toString();
331            if (TextUtils.isEmpty(pin)) {
332                return;
333            }
334
335            mPasswordEntryInputDisabler.setInputEnabled(false);
336            final boolean verifyChallenge = getActivity().getIntent().getBooleanExtra(
337                    ChooseLockSettingsHelper.EXTRA_KEY_HAS_CHALLENGE, false);
338
339            Intent intent = new Intent();
340            if (verifyChallenge)  {
341                if (isInternalActivity()) {
342                    startVerifyPassword(pin, intent);
343                    return;
344                }
345            } else {
346                startCheckPassword(pin, intent);
347                return;
348            }
349
350            mCredentialCheckResultTracker.setResult(false, intent, 0, mEffectiveUserId);
351        }
352
353        private boolean isInternalActivity() {
354            return getActivity() instanceof ConfirmLockPassword.InternalActivity;
355        }
356
357        private void startVerifyPassword(final String pin, final Intent intent) {
358            long challenge = getActivity().getIntent().getLongExtra(
359                    ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE, 0);
360            final int localEffectiveUserId = mEffectiveUserId;
361            final int localUserId = mUserId;
362            final LockPatternChecker.OnVerifyCallback onVerifyCallback =
363                    new LockPatternChecker.OnVerifyCallback() {
364                        @Override
365                        public void onVerified(byte[] token, int timeoutMs) {
366                            mPendingLockCheck = null;
367                            boolean matched = false;
368                            if (token != null) {
369                                matched = true;
370                                if (mReturnCredentials) {
371                                    intent.putExtra(
372                                            ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN,
373                                            token);
374                                }
375                            }
376                            mCredentialCheckResultTracker.setResult(matched, intent, timeoutMs,
377                                    localUserId);
378                        }
379            };
380            mPendingLockCheck = (localEffectiveUserId == localUserId)
381                    ? LockPatternChecker.verifyPassword(
382                            mLockPatternUtils, pin, challenge, localUserId, onVerifyCallback)
383                    : LockPatternChecker.verifyTiedProfileChallenge(
384                            mLockPatternUtils, pin, false, challenge, localUserId,
385                            onVerifyCallback);
386        }
387
388        private void startCheckPassword(final String pin, final Intent intent) {
389            final int localEffectiveUserId = mEffectiveUserId;
390            mPendingLockCheck = LockPatternChecker.checkPassword(
391                    mLockPatternUtils,
392                    pin,
393                    localEffectiveUserId,
394                    new LockPatternChecker.OnCheckCallback() {
395                        @Override
396                        public void onChecked(boolean matched, int timeoutMs) {
397                            mPendingLockCheck = null;
398                            if (matched && isInternalActivity() && mReturnCredentials) {
399                                intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_TYPE,
400                                                mIsAlpha ? StorageManager.CRYPT_TYPE_PASSWORD
401                                                         : StorageManager.CRYPT_TYPE_PIN);
402                                intent.putExtra(
403                                        ChooseLockSettingsHelper.EXTRA_KEY_PASSWORD, pin);
404                            }
405                            mCredentialCheckResultTracker.setResult(matched, intent, timeoutMs,
406                                    localEffectiveUserId);
407                        }
408                    });
409        }
410
411        private void startDisappearAnimation(final Intent intent) {
412            if (mDisappearing) {
413                return;
414            }
415            mDisappearing = true;
416
417            final ConfirmLockPassword activity = (ConfirmLockPassword) getActivity();
418            // Bail if there is no active activity.
419            if (activity == null || activity.isFinishing()) {
420                return;
421            }
422            if (activity.getConfirmCredentialTheme() == ConfirmCredentialTheme.DARK) {
423                mDisappearAnimationUtils.startAnimation(getActiveViews(), () -> {
424                    activity.setResult(RESULT_OK, intent);
425                    activity.finish();
426                    activity.overridePendingTransition(
427                            R.anim.confirm_credential_close_enter,
428                            R.anim.confirm_credential_close_exit);
429                });
430            } else {
431                activity.setResult(RESULT_OK, intent);
432                activity.finish();
433            }
434        }
435
436        private void onPasswordChecked(boolean matched, Intent intent, int timeoutMs,
437                int effectiveUserId, boolean newResult) {
438            mPasswordEntryInputDisabler.setInputEnabled(true);
439            if (matched) {
440                if (newResult) {
441                    reportSuccessfullAttempt();
442                }
443                startDisappearAnimation(intent);
444                checkForPendingIntent();
445            } else {
446                if (timeoutMs > 0) {
447                    refreshLockScreen();
448                    long deadline = mLockPatternUtils.setLockoutAttemptDeadline(
449                            effectiveUserId, timeoutMs);
450                    handleAttemptLockout(deadline);
451                } else {
452                    showError(getErrorMessage(), ERROR_MESSAGE_TIMEOUT);
453                }
454                if (newResult) {
455                    reportFailedAttempt();
456                }
457            }
458        }
459
460        @Override
461        public void onCredentialChecked(boolean matched, Intent intent, int timeoutMs,
462                int effectiveUserId, boolean newResult) {
463            onPasswordChecked(matched, intent, timeoutMs, effectiveUserId, newResult);
464        }
465
466        @Override
467        protected void onShowError() {
468            mPasswordEntry.setText(null);
469        }
470
471        private void handleAttemptLockout(long elapsedRealtimeDeadline) {
472            long elapsedRealtime = SystemClock.elapsedRealtime();
473            mPasswordEntry.setEnabled(false);
474            mCountdownTimer = new CountDownTimer(
475                    elapsedRealtimeDeadline - elapsedRealtime,
476                    LockPatternUtils.FAILED_ATTEMPT_COUNTDOWN_INTERVAL_MS) {
477
478                @Override
479                public void onTick(long millisUntilFinished) {
480                    final int secondsCountdown = (int) (millisUntilFinished / 1000);
481                    showError(getString(
482                            R.string.lockpattern_too_many_failed_confirmation_attempts,
483                            secondsCountdown), 0);
484                }
485
486                @Override
487                public void onFinish() {
488                    resetState();
489                    mErrorTextView.setText("");
490                    if (isProfileChallenge()) {
491                        updateErrorMessage(mLockPatternUtils.getCurrentFailedPasswordAttempts(
492                                mEffectiveUserId));
493                    }
494                }
495            }.start();
496        }
497
498        public void onClick(View v) {
499            switch (v.getId()) {
500                case R.id.next_button:
501                    handleNext();
502                    break;
503
504                case R.id.cancel_button:
505                    getActivity().setResult(RESULT_CANCELED);
506                    getActivity().finish();
507                    break;
508            }
509        }
510
511        // {@link OnEditorActionListener} methods.
512        public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
513            // Check if this was the result of hitting the enter or "done" key
514            if (actionId == EditorInfo.IME_NULL
515                    || actionId == EditorInfo.IME_ACTION_DONE
516                    || actionId == EditorInfo.IME_ACTION_NEXT) {
517                handleNext();
518                return true;
519            }
520            return false;
521        }
522    }
523}
524