| /* |
| * Copyright (C) 2020 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.keyguard; |
| |
| import static com.android.internal.util.LatencyTracker.ACTION_CHECK_CREDENTIAL; |
| import static com.android.internal.util.LatencyTracker.ACTION_CHECK_CREDENTIAL_UNLOCKED; |
| |
| import android.content.res.ColorStateList; |
| import android.os.AsyncTask; |
| import android.os.CountDownTimer; |
| import android.os.SystemClock; |
| import android.util.PluralsMessageFormatter; |
| import android.view.MotionEvent; |
| import android.view.View; |
| |
| import com.android.internal.util.LatencyTracker; |
| import com.android.internal.widget.LockPatternChecker; |
| import com.android.internal.widget.LockPatternUtils; |
| import com.android.internal.widget.LockPatternView; |
| import com.android.internal.widget.LockPatternView.Cell; |
| import com.android.internal.widget.LockscreenCredential; |
| import com.android.keyguard.EmergencyButtonController.EmergencyButtonCallback; |
| import com.android.keyguard.KeyguardSecurityModel.SecurityMode; |
| import com.android.settingslib.Utils; |
| import com.android.systemui.R; |
| import com.android.systemui.classifier.FalsingClassifier; |
| import com.android.systemui.classifier.FalsingCollector; |
| import com.android.systemui.statusbar.policy.DevicePostureController; |
| |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| |
| public class KeyguardPatternViewController |
| extends KeyguardInputViewController<KeyguardPatternView> { |
| |
| // how many cells the user has to cross before we poke the wakelock |
| private static final int MIN_PATTERN_BEFORE_POKE_WAKELOCK = 2; |
| |
| // how long before we clear the wrong pattern |
| private static final int PATTERN_CLEAR_TIMEOUT_MS = 2000; |
| |
| private final KeyguardUpdateMonitor mKeyguardUpdateMonitor; |
| private final LockPatternUtils mLockPatternUtils; |
| private final LatencyTracker mLatencyTracker; |
| private final FalsingCollector mFalsingCollector; |
| private final EmergencyButtonController mEmergencyButtonController; |
| private final KeyguardMessageAreaController.Factory mMessageAreaControllerFactory; |
| private final DevicePostureController mPostureController; |
| private final DevicePostureController.Callback mPostureCallback = |
| posture -> mView.onDevicePostureChanged(posture); |
| |
| private KeyguardMessageAreaController mMessageAreaController; |
| private LockPatternView mLockPatternView; |
| private CountDownTimer mCountdownTimer; |
| private AsyncTask<?, ?, ?> mPendingLockCheck; |
| |
| private EmergencyButtonCallback mEmergencyButtonCallback = new EmergencyButtonCallback() { |
| @Override |
| public void onEmergencyButtonClickedWhenInCall() { |
| getKeyguardSecurityCallback().reset(); |
| } |
| }; |
| |
| /** |
| * Useful for clearing out the wrong pattern after a delay |
| */ |
| private Runnable mCancelPatternRunnable = new Runnable() { |
| @Override |
| public void run() { |
| mLockPatternView.clearPattern(); |
| } |
| }; |
| |
| private class UnlockPatternListener implements LockPatternView.OnPatternListener { |
| |
| @Override |
| public void onPatternStart() { |
| mLockPatternView.removeCallbacks(mCancelPatternRunnable); |
| mMessageAreaController.setMessage(""); |
| } |
| |
| @Override |
| public void onPatternCleared() { |
| } |
| |
| @Override |
| public void onPatternCellAdded(List<Cell> pattern) { |
| getKeyguardSecurityCallback().userActivity(); |
| getKeyguardSecurityCallback().onUserInput(); |
| } |
| |
| @Override |
| public void onPatternDetected(final List<LockPatternView.Cell> pattern) { |
| mKeyguardUpdateMonitor.setCredentialAttempted(); |
| mLockPatternView.disableInput(); |
| if (mPendingLockCheck != null) { |
| mPendingLockCheck.cancel(false); |
| } |
| |
| final int userId = KeyguardUpdateMonitor.getCurrentUser(); |
| if (pattern.size() < LockPatternUtils.MIN_PATTERN_REGISTER_FAIL) { |
| // Treat single-sized patterns as erroneous taps. |
| if (pattern.size() == 1) { |
| mFalsingCollector.updateFalseConfidence(FalsingClassifier.Result.falsed( |
| 0.7, getClass().getSimpleName(), "empty pattern input")); |
| } |
| mLockPatternView.enableInput(); |
| onPatternChecked(userId, false, 0, false /* not valid - too short */); |
| return; |
| } |
| |
| mLatencyTracker.onActionStart(ACTION_CHECK_CREDENTIAL); |
| mLatencyTracker.onActionStart(ACTION_CHECK_CREDENTIAL_UNLOCKED); |
| mPendingLockCheck = LockPatternChecker.checkCredential( |
| mLockPatternUtils, |
| LockscreenCredential.createPattern(pattern), |
| userId, |
| new LockPatternChecker.OnCheckCallback() { |
| |
| @Override |
| public void onEarlyMatched() { |
| mLatencyTracker.onActionEnd(ACTION_CHECK_CREDENTIAL); |
| onPatternChecked(userId, true /* matched */, 0 /* timeoutMs */, |
| true /* isValidPattern */); |
| } |
| |
| @Override |
| public void onChecked(boolean matched, int timeoutMs) { |
| mLatencyTracker.onActionEnd(ACTION_CHECK_CREDENTIAL_UNLOCKED); |
| mLockPatternView.enableInput(); |
| mPendingLockCheck = null; |
| if (!matched) { |
| onPatternChecked(userId, false /* matched */, timeoutMs, |
| true /* isValidPattern */); |
| } |
| } |
| |
| @Override |
| public void onCancelled() { |
| // We already got dismissed with the early matched callback, so we |
| // cancelled the check. However, we still need to note down the latency. |
| mLatencyTracker.onActionEnd(ACTION_CHECK_CREDENTIAL_UNLOCKED); |
| } |
| }); |
| if (pattern.size() > MIN_PATTERN_BEFORE_POKE_WAKELOCK) { |
| getKeyguardSecurityCallback().userActivity(); |
| getKeyguardSecurityCallback().onUserInput(); |
| } |
| } |
| |
| private void onPatternChecked(int userId, boolean matched, int timeoutMs, |
| boolean isValidPattern) { |
| boolean dismissKeyguard = KeyguardUpdateMonitor.getCurrentUser() == userId; |
| if (matched) { |
| getKeyguardSecurityCallback().reportUnlockAttempt(userId, true, 0); |
| if (dismissKeyguard) { |
| mLockPatternView.setDisplayMode(LockPatternView.DisplayMode.Correct); |
| mLatencyTracker.onActionStart(LatencyTracker.ACTION_LOCKSCREEN_UNLOCK); |
| getKeyguardSecurityCallback().dismiss(true, userId, SecurityMode.Pattern); |
| } |
| } else { |
| mLockPatternView.setDisplayMode(LockPatternView.DisplayMode.Wrong); |
| if (isValidPattern) { |
| getKeyguardSecurityCallback().reportUnlockAttempt(userId, false, timeoutMs); |
| if (timeoutMs > 0) { |
| long deadline = mLockPatternUtils.setLockoutAttemptDeadline( |
| userId, timeoutMs); |
| handleAttemptLockout(deadline); |
| } |
| } |
| if (timeoutMs == 0) { |
| mMessageAreaController.setMessage(R.string.kg_wrong_pattern); |
| mLockPatternView.postDelayed(mCancelPatternRunnable, PATTERN_CLEAR_TIMEOUT_MS); |
| } |
| } |
| } |
| } |
| |
| protected KeyguardPatternViewController(KeyguardPatternView view, |
| KeyguardUpdateMonitor keyguardUpdateMonitor, |
| SecurityMode securityMode, |
| LockPatternUtils lockPatternUtils, |
| KeyguardSecurityCallback keyguardSecurityCallback, |
| LatencyTracker latencyTracker, |
| FalsingCollector falsingCollector, |
| EmergencyButtonController emergencyButtonController, |
| KeyguardMessageAreaController.Factory messageAreaControllerFactory, |
| DevicePostureController postureController) { |
| super(view, securityMode, keyguardSecurityCallback, emergencyButtonController); |
| mKeyguardUpdateMonitor = keyguardUpdateMonitor; |
| mLockPatternUtils = lockPatternUtils; |
| mLatencyTracker = latencyTracker; |
| mFalsingCollector = falsingCollector; |
| mEmergencyButtonController = emergencyButtonController; |
| mMessageAreaControllerFactory = messageAreaControllerFactory; |
| KeyguardMessageArea kma = KeyguardMessageArea.findSecurityMessageDisplay(mView); |
| mMessageAreaController = mMessageAreaControllerFactory.create(kma); |
| mLockPatternView = mView.findViewById(R.id.lockPatternView); |
| mPostureController = postureController; |
| } |
| |
| @Override |
| public void onInit() { |
| super.onInit(); |
| mMessageAreaController.init(); |
| } |
| |
| @Override |
| protected void onViewAttached() { |
| super.onViewAttached(); |
| mLockPatternView.setOnPatternListener(new UnlockPatternListener()); |
| mLockPatternView.setSaveEnabled(false); |
| mLockPatternView.setInStealthMode(!mLockPatternUtils.isVisiblePatternEnabled( |
| KeyguardUpdateMonitor.getCurrentUser())); |
| mLockPatternView.setOnTouchListener((v, event) -> { |
| if (event.getActionMasked() == MotionEvent.ACTION_DOWN) { |
| mFalsingCollector.avoidGesture(); |
| } |
| return false; |
| }); |
| mEmergencyButtonController.setEmergencyButtonCallback(mEmergencyButtonCallback); |
| |
| View cancelBtn = mView.findViewById(R.id.cancel_button); |
| if (cancelBtn != null) { |
| cancelBtn.setOnClickListener(view -> { |
| getKeyguardSecurityCallback().reset(); |
| getKeyguardSecurityCallback().onCancelClicked(); |
| }); |
| } |
| mPostureController.addCallback(mPostureCallback); |
| } |
| |
| @Override |
| protected void onViewDetached() { |
| super.onViewDetached(); |
| mLockPatternView.setOnPatternListener(null); |
| mLockPatternView.setOnTouchListener(null); |
| mEmergencyButtonController.setEmergencyButtonCallback(null); |
| View cancelBtn = mView.findViewById(R.id.cancel_button); |
| if (cancelBtn != null) { |
| cancelBtn.setOnClickListener(null); |
| } |
| mPostureController.removeCallback(mPostureCallback); |
| } |
| |
| @Override |
| public void reset() { |
| // reset lock pattern |
| mLockPatternView.setInStealthMode(!mLockPatternUtils.isVisiblePatternEnabled( |
| KeyguardUpdateMonitor.getCurrentUser())); |
| mLockPatternView.enableInput(); |
| mLockPatternView.setEnabled(true); |
| mLockPatternView.clearPattern(); |
| |
| // if the user is currently locked out, enforce it. |
| long deadline = mLockPatternUtils.getLockoutAttemptDeadline( |
| KeyguardUpdateMonitor.getCurrentUser()); |
| if (deadline != 0) { |
| handleAttemptLockout(deadline); |
| } else { |
| displayDefaultSecurityMessage(); |
| } |
| } |
| |
| @Override |
| public void reloadColors() { |
| super.reloadColors(); |
| mMessageAreaController.reloadColors(); |
| int textColor = Utils.getColorAttr(mLockPatternView.getContext(), |
| android.R.attr.textColorSecondary).getDefaultColor(); |
| int errorColor = Utils.getColorError(mLockPatternView.getContext()).getDefaultColor(); |
| mLockPatternView.setColors(textColor, textColor, errorColor); |
| } |
| |
| @Override |
| public void onPause() { |
| super.onPause(); |
| |
| if (mCountdownTimer != null) { |
| mCountdownTimer.cancel(); |
| mCountdownTimer = null; |
| } |
| |
| if (mPendingLockCheck != null) { |
| mPendingLockCheck.cancel(false); |
| mPendingLockCheck = null; |
| } |
| displayDefaultSecurityMessage(); |
| } |
| |
| @Override |
| public boolean needsInput() { |
| return false; |
| } |
| |
| @Override |
| public void showPromptReason(int reason) { |
| /// TODO: move all this logic into the MessageAreaController? |
| switch (reason) { |
| case PROMPT_REASON_RESTART: |
| mMessageAreaController.setMessage(R.string.kg_prompt_reason_restart_pattern); |
| break; |
| case PROMPT_REASON_TIMEOUT: |
| mMessageAreaController.setMessage(R.string.kg_prompt_reason_timeout_pattern); |
| break; |
| case PROMPT_REASON_DEVICE_ADMIN: |
| mMessageAreaController.setMessage(R.string.kg_prompt_reason_device_admin); |
| break; |
| case PROMPT_REASON_USER_REQUEST: |
| mMessageAreaController.setMessage(R.string.kg_prompt_reason_user_request); |
| break; |
| case PROMPT_REASON_PREPARE_FOR_UPDATE: |
| mMessageAreaController.setMessage(R.string.kg_prompt_reason_timeout_pattern); |
| break; |
| case PROMPT_REASON_NON_STRONG_BIOMETRIC_TIMEOUT: |
| mMessageAreaController.setMessage(R.string.kg_prompt_reason_timeout_pattern); |
| break; |
| case PROMPT_REASON_NONE: |
| break; |
| default: |
| mMessageAreaController.setMessage(R.string.kg_prompt_reason_timeout_pattern); |
| break; |
| } |
| } |
| |
| @Override |
| public void showMessage(CharSequence message, ColorStateList colorState) { |
| if (colorState != null) { |
| mMessageAreaController.setNextMessageColor(colorState); |
| } |
| mMessageAreaController.setMessage(message); |
| } |
| |
| @Override |
| public void startAppearAnimation() { |
| super.startAppearAnimation(); |
| } |
| |
| @Override |
| public boolean startDisappearAnimation(Runnable finishRunnable) { |
| return mView.startDisappearAnimation( |
| mKeyguardUpdateMonitor.needsSlowUnlockTransition(), finishRunnable); |
| } |
| |
| private void displayDefaultSecurityMessage() { |
| mMessageAreaController.setMessage(""); |
| } |
| |
| private void handleAttemptLockout(long elapsedRealtimeDeadline) { |
| mLockPatternView.clearPattern(); |
| mLockPatternView.setEnabled(false); |
| final long elapsedRealtime = SystemClock.elapsedRealtime(); |
| final long secondsInFuture = (long) Math.ceil( |
| (elapsedRealtimeDeadline - elapsedRealtime) / 1000.0); |
| mCountdownTimer = new CountDownTimer(secondsInFuture * 1000, 1000) { |
| |
| @Override |
| public void onTick(long millisUntilFinished) { |
| final int secondsRemaining = (int) Math.round(millisUntilFinished / 1000.0); |
| Map<String, Object> arguments = new HashMap<>(); |
| arguments.put("count", secondsRemaining); |
| |
| mMessageAreaController.setMessage(PluralsMessageFormatter.format( |
| mView.getResources(), |
| arguments, |
| R.string.kg_too_many_failed_attempts_countdown)); |
| } |
| |
| @Override |
| public void onFinish() { |
| mLockPatternView.setEnabled(true); |
| displayDefaultSecurityMessage(); |
| } |
| |
| }.start(); |
| } |
| } |