/* * Copyright (C) 2014 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 android.app.Activity; import android.app.AlertDialog; import android.app.admin.DevicePolicyManager; import android.content.Context; import android.content.res.ColorStateList; import android.graphics.Rect; import android.metrics.LogMaker; import android.os.UserHandle; import android.util.AttributeSet; import android.util.Log; import android.util.Slog; import android.util.StatsLog; import android.util.TypedValue; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.VelocityTracker; import android.view.View; import android.view.ViewConfiguration; import android.view.WindowManager; import android.widget.FrameLayout; import androidx.annotation.VisibleForTesting; import androidx.dynamicanimation.animation.DynamicAnimation; import androidx.dynamicanimation.animation.SpringAnimation; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.internal.widget.LockPatternUtils; import com.android.keyguard.KeyguardSecurityModel.SecurityMode; import com.android.systemui.Dependency; import com.android.systemui.SystemUIFactory; import com.android.systemui.util.InjectionInflationController; public class KeyguardSecurityContainer extends FrameLayout implements KeyguardSecurityView { private static final boolean DEBUG = KeyguardConstants.DEBUG; private static final String TAG = "KeyguardSecurityView"; private static final int USER_TYPE_PRIMARY = 1; private static final int USER_TYPE_WORK_PROFILE = 2; private static final int USER_TYPE_SECONDARY_USER = 3; // Bouncer is dismissed due to no security. private static final int BOUNCER_DISMISS_NONE_SECURITY = 0; // Bouncer is dismissed due to pin, password or pattern entered. private static final int BOUNCER_DISMISS_PASSWORD = 1; // Bouncer is dismissed due to biometric (face, fingerprint or iris) authenticated. private static final int BOUNCER_DISMISS_BIOMETRIC = 2; // Bouncer is dismissed due to extended access granted. private static final int BOUNCER_DISMISS_EXTENDED_ACCESS = 3; // Bouncer is dismissed due to sim card unlock code entered. private static final int BOUNCER_DISMISS_SIM = 4; // Make the view move slower than the finger, as if the spring were applying force. private static final float TOUCH_Y_MULTIPLIER = 0.25f; // How much you need to drag the bouncer to trigger an auth retry (in dps.) private static final float MIN_DRAG_SIZE = 10; // How much to scale the default slop by, to avoid accidental drags. private static final float SLOP_SCALE = 2f; private KeyguardSecurityModel mSecurityModel; private LockPatternUtils mLockPatternUtils; private KeyguardSecurityViewFlipper mSecurityViewFlipper; private boolean mIsVerifyUnlockOnly; private SecurityMode mCurrentSecuritySelection = SecurityMode.Invalid; private KeyguardSecurityView mCurrentSecurityView; private SecurityCallback mSecurityCallback; private AlertDialog mAlertDialog; private InjectionInflationController mInjectionInflationController; private boolean mSwipeUpToRetry; private final ViewConfiguration mViewConfiguration; private final SpringAnimation mSpringAnimation; private final VelocityTracker mVelocityTracker = VelocityTracker.obtain(); private final KeyguardUpdateMonitor mUpdateMonitor; private final MetricsLogger mMetricsLogger = Dependency.get(MetricsLogger.class); private float mLastTouchY = -1; private int mActivePointerId = -1; private boolean mIsDragging; private float mStartTouchY = -1; // Used to notify the container when something interesting happens. public interface SecurityCallback { public boolean dismiss(boolean authenticated, int targetUserId); public void userActivity(); public void onSecurityModeChanged(SecurityMode securityMode, boolean needsInput); /** * @param strongAuth wheher the user has authenticated with strong authentication like * pattern, password or PIN but not by trust agents or fingerprint * @param targetUserId a user that needs to be the foreground user at the finish completion. */ public void finish(boolean strongAuth, int targetUserId); public void reset(); public void onCancelClicked(); } public KeyguardSecurityContainer(Context context, AttributeSet attrs) { this(context, attrs, 0); } public KeyguardSecurityContainer(Context context) { this(context, null, 0); } public KeyguardSecurityContainer(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); mSecurityModel = new KeyguardSecurityModel(context); mLockPatternUtils = new LockPatternUtils(context); mUpdateMonitor = KeyguardUpdateMonitor.getInstance(mContext); mSpringAnimation = new SpringAnimation(this, DynamicAnimation.Y); mInjectionInflationController = new InjectionInflationController( SystemUIFactory.getInstance().getRootComponent()); mViewConfiguration = ViewConfiguration.get(context); } public void setSecurityCallback(SecurityCallback callback) { mSecurityCallback = callback; } @Override public void onResume(int reason) { if (mCurrentSecuritySelection != SecurityMode.None) { getSecurityView(mCurrentSecuritySelection).onResume(reason); } updateBiometricRetry(); } @Override public void onPause() { if (mAlertDialog != null) { mAlertDialog.dismiss(); mAlertDialog = null; } if (mCurrentSecuritySelection != SecurityMode.None) { getSecurityView(mCurrentSecuritySelection).onPause(); } } @Override public boolean shouldDelayChildPressedState() { return true; } @Override public boolean onInterceptTouchEvent(MotionEvent event) { switch (event.getActionMasked()) { case MotionEvent.ACTION_DOWN: int pointerIndex = event.getActionIndex(); mStartTouchY = event.getY(pointerIndex); mActivePointerId = event.getPointerId(pointerIndex); mVelocityTracker.clear(); break; case MotionEvent.ACTION_MOVE: if (mIsDragging) { return true; } if (!mSwipeUpToRetry) { return false; } // Avoid dragging the pattern view if (mCurrentSecurityView.disallowInterceptTouch(event)) { return false; } int index = event.findPointerIndex(mActivePointerId); float touchSlop = mViewConfiguration.getScaledTouchSlop() * SLOP_SCALE; if (mCurrentSecurityView != null && index != -1 && mStartTouchY - event.getY(index) > touchSlop) { mIsDragging = true; return true; } break; case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: mIsDragging = false; break; } return false; } @Override public boolean onTouchEvent(MotionEvent event) { final int action = event.getActionMasked(); switch (action) { case MotionEvent.ACTION_MOVE: mVelocityTracker.addMovement(event); int pointerIndex = event.findPointerIndex(mActivePointerId); float y = event.getY(pointerIndex); if (mLastTouchY != -1) { float dy = y - mLastTouchY; setTranslationY(getTranslationY() + dy * TOUCH_Y_MULTIPLIER); } mLastTouchY = y; break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: mActivePointerId = -1; mLastTouchY = -1; mIsDragging = false; startSpringAnimation(mVelocityTracker.getYVelocity()); break; case MotionEvent.ACTION_POINTER_UP: int index = event.getActionIndex(); int pointerId = event.getPointerId(index); if (pointerId == mActivePointerId) { // This was our active pointer going up. Choose a new // active pointer and adjust accordingly. final int newPointerIndex = index == 0 ? 1 : 0; mLastTouchY = event.getY(newPointerIndex); mActivePointerId = event.getPointerId(newPointerIndex); } break; } if (action == MotionEvent.ACTION_UP) { if (-getTranslationY() > TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, MIN_DRAG_SIZE, getResources().getDisplayMetrics())) { mUpdateMonitor.requestFaceAuth(); } } return true; } private void startSpringAnimation(float startVelocity) { mSpringAnimation .setStartVelocity(startVelocity) .animateToFinalPosition(0); } public void startAppearAnimation() { if (mCurrentSecuritySelection != SecurityMode.None) { getSecurityView(mCurrentSecuritySelection).startAppearAnimation(); } } public boolean startDisappearAnimation(Runnable onFinishRunnable) { if (mCurrentSecuritySelection != SecurityMode.None) { return getSecurityView(mCurrentSecuritySelection).startDisappearAnimation( onFinishRunnable); } return false; } /** * Enables/disables swipe up to retry on the bouncer. */ private void updateBiometricRetry() { SecurityMode securityMode = getSecurityMode(); int userId = KeyguardUpdateMonitor.getCurrentUser(); mSwipeUpToRetry = mUpdateMonitor.isUnlockWithFacePossible(userId) && securityMode != SecurityMode.SimPin && securityMode != SecurityMode.SimPuk && securityMode != SecurityMode.None; } public CharSequence getTitle() { return mSecurityViewFlipper.getTitle(); } private KeyguardSecurityView getSecurityView(SecurityMode securityMode) { final int securityViewIdForMode = getSecurityViewIdForMode(securityMode); KeyguardSecurityView view = null; final int children = mSecurityViewFlipper.getChildCount(); for (int child = 0; child < children; child++) { if (mSecurityViewFlipper.getChildAt(child).getId() == securityViewIdForMode) { view = ((KeyguardSecurityView)mSecurityViewFlipper.getChildAt(child)); break; } } int layoutId = getLayoutIdFor(securityMode); if (view == null && layoutId != 0) { final LayoutInflater inflater = LayoutInflater.from(mContext); if (DEBUG) Log.v(TAG, "inflating id = " + layoutId); View v = mInjectionInflationController.injectable(inflater) .inflate(layoutId, mSecurityViewFlipper, false); mSecurityViewFlipper.addView(v); updateSecurityView(v); view = (KeyguardSecurityView)v; view.reset(); } return view; } private void updateSecurityView(View view) { if (view instanceof KeyguardSecurityView) { KeyguardSecurityView ksv = (KeyguardSecurityView) view; ksv.setKeyguardCallback(mCallback); ksv.setLockPatternUtils(mLockPatternUtils); } else { Log.w(TAG, "View " + view + " is not a KeyguardSecurityView"); } } protected void onFinishInflate() { mSecurityViewFlipper = findViewById(R.id.view_flipper); mSecurityViewFlipper.setLockPatternUtils(mLockPatternUtils); } public void setLockPatternUtils(LockPatternUtils utils) { mLockPatternUtils = utils; mSecurityModel.setLockPatternUtils(utils); mSecurityViewFlipper.setLockPatternUtils(mLockPatternUtils); } @Override protected boolean fitSystemWindows(Rect insets) { // Consume bottom insets because we're setting the padding locally (for IME and navbar.) setPadding(getPaddingLeft(), getPaddingTop(), getPaddingRight(), insets.bottom); insets.bottom = 0; return false; } private void showDialog(String title, String message) { if (mAlertDialog != null) { mAlertDialog.dismiss(); } mAlertDialog = new AlertDialog.Builder(mContext) .setTitle(title) .setMessage(message) .setCancelable(false) .setNeutralButton(R.string.ok, null) .create(); if (!(mContext instanceof Activity)) { mAlertDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG); } mAlertDialog.show(); } private void showTimeoutDialog(int userId, int timeoutMs) { int timeoutInSeconds = (int) timeoutMs / 1000; int messageId = 0; switch (mSecurityModel.getSecurityMode(userId)) { case Pattern: messageId = R.string.kg_too_many_failed_pattern_attempts_dialog_message; break; case PIN: messageId = R.string.kg_too_many_failed_pin_attempts_dialog_message; break; case Password: messageId = R.string.kg_too_many_failed_password_attempts_dialog_message; break; // These don't have timeout dialogs. case Invalid: case None: case SimPin: case SimPuk: break; } if (messageId != 0) { final String message = mContext.getString(messageId, mLockPatternUtils.getCurrentFailedPasswordAttempts(userId), timeoutInSeconds); showDialog(null, message); } } private void showAlmostAtWipeDialog(int attempts, int remaining, int userType) { String message = null; switch (userType) { case USER_TYPE_PRIMARY: message = mContext.getString(R.string.kg_failed_attempts_almost_at_wipe, attempts, remaining); break; case USER_TYPE_SECONDARY_USER: message = mContext.getString(R.string.kg_failed_attempts_almost_at_erase_user, attempts, remaining); break; case USER_TYPE_WORK_PROFILE: message = mContext.getString(R.string.kg_failed_attempts_almost_at_erase_profile, attempts, remaining); break; } showDialog(null, message); } private void showWipeDialog(int attempts, int userType) { String message = null; switch (userType) { case USER_TYPE_PRIMARY: message = mContext.getString(R.string.kg_failed_attempts_now_wiping, attempts); break; case USER_TYPE_SECONDARY_USER: message = mContext.getString(R.string.kg_failed_attempts_now_erasing_user, attempts); break; case USER_TYPE_WORK_PROFILE: message = mContext.getString(R.string.kg_failed_attempts_now_erasing_profile, attempts); break; } showDialog(null, message); } private void reportFailedUnlockAttempt(int userId, int timeoutMs) { // +1 for this time final int failedAttempts = mLockPatternUtils.getCurrentFailedPasswordAttempts(userId) + 1; if (DEBUG) Log.d(TAG, "reportFailedPatternAttempt: #" + failedAttempts); final DevicePolicyManager dpm = mLockPatternUtils.getDevicePolicyManager(); final int failedAttemptsBeforeWipe = dpm.getMaximumFailedPasswordsForWipe(null, userId); final int remainingBeforeWipe = failedAttemptsBeforeWipe > 0 ? (failedAttemptsBeforeWipe - failedAttempts) : Integer.MAX_VALUE; // because DPM returns 0 if no restriction if (remainingBeforeWipe < LockPatternUtils.FAILED_ATTEMPTS_BEFORE_WIPE_GRACE) { // The user has installed a DevicePolicyManager that requests a user/profile to be wiped // N attempts. Once we get below the grace period, we post this dialog every time as a // clear warning until the deletion fires. // Check which profile has the strictest policy for failed password attempts final int expiringUser = dpm.getProfileWithMinimumFailedPasswordsForWipe(userId); int userType = USER_TYPE_PRIMARY; if (expiringUser == userId) { // TODO: http://b/23522538 if (expiringUser != UserHandle.USER_SYSTEM) { userType = USER_TYPE_SECONDARY_USER; } } else if (expiringUser != UserHandle.USER_NULL) { userType = USER_TYPE_WORK_PROFILE; } // If USER_NULL, which shouldn't happen, leave it as USER_TYPE_PRIMARY if (remainingBeforeWipe > 0) { showAlmostAtWipeDialog(failedAttempts, remainingBeforeWipe, userType); } else { // Too many attempts. The device will be wiped shortly. Slog.i(TAG, "Too many unlock attempts; user " + expiringUser + " will be wiped!"); showWipeDialog(failedAttempts, userType); } } mLockPatternUtils.reportFailedPasswordAttempt(userId); if (timeoutMs > 0) { mLockPatternUtils.reportPasswordLockout(timeoutMs, userId); showTimeoutDialog(userId, timeoutMs); } } /** * Shows the primary security screen for the user. This will be either the multi-selector * or the user's security method. * @param turningOff true if the device is being turned off */ void showPrimarySecurityScreen(boolean turningOff) { SecurityMode securityMode = mSecurityModel.getSecurityMode( KeyguardUpdateMonitor.getCurrentUser()); if (DEBUG) Log.v(TAG, "showPrimarySecurityScreen(turningOff=" + turningOff + ")"); showSecurityScreen(securityMode); } /** * Shows the next security screen if there is one. * @param authenticated true if the user entered the correct authentication * @param targetUserId a user that needs to be the foreground user at the finish (if called) * completion. * @return true if keyguard is done */ boolean showNextSecurityScreenOrFinish(boolean authenticated, int targetUserId) { if (DEBUG) Log.d(TAG, "showNextSecurityScreenOrFinish(" + authenticated + ")"); boolean finish = false; boolean strongAuth = false; int eventSubtype = -1; if (mUpdateMonitor.getUserHasTrust(targetUserId)) { finish = true; eventSubtype = BOUNCER_DISMISS_EXTENDED_ACCESS; } else if (mUpdateMonitor.getUserUnlockedWithBiometric(targetUserId)) { finish = true; eventSubtype = BOUNCER_DISMISS_BIOMETRIC; } else if (SecurityMode.None == mCurrentSecuritySelection) { SecurityMode securityMode = mSecurityModel.getSecurityMode(targetUserId); if (SecurityMode.None == securityMode) { finish = true; // no security required eventSubtype = BOUNCER_DISMISS_NONE_SECURITY; } else { showSecurityScreen(securityMode); // switch to the alternate security view } } else if (authenticated) { switch (mCurrentSecuritySelection) { case Pattern: case Password: case PIN: strongAuth = true; finish = true; eventSubtype = BOUNCER_DISMISS_PASSWORD; break; case SimPin: case SimPuk: // Shortcut for SIM PIN/PUK to go to directly to user's security screen or home SecurityMode securityMode = mSecurityModel.getSecurityMode(targetUserId); if (securityMode == SecurityMode.None || mLockPatternUtils.isLockScreenDisabled( KeyguardUpdateMonitor.getCurrentUser())) { finish = true; eventSubtype = BOUNCER_DISMISS_SIM; } else { showSecurityScreen(securityMode); } break; default: Log.v(TAG, "Bad security screen " + mCurrentSecuritySelection + ", fail safe"); showPrimarySecurityScreen(false); break; } } if (eventSubtype != -1) { mMetricsLogger.write(new LogMaker(MetricsEvent.BOUNCER) .setType(MetricsEvent.TYPE_DISMISS).setSubtype(eventSubtype)); } if (finish) { mSecurityCallback.finish(strongAuth, targetUserId); } return finish; } /** * Switches to the given security view unless it's already being shown, in which case * this is a no-op. * * @param securityMode */ private void showSecurityScreen(SecurityMode securityMode) { if (DEBUG) Log.d(TAG, "showSecurityScreen(" + securityMode + ")"); if (securityMode == mCurrentSecuritySelection) return; KeyguardSecurityView oldView = getSecurityView(mCurrentSecuritySelection); KeyguardSecurityView newView = getSecurityView(securityMode); // Emulate Activity life cycle if (oldView != null) { oldView.onPause(); oldView.setKeyguardCallback(mNullCallback); // ignore requests from old view } if (securityMode != SecurityMode.None) { newView.onResume(KeyguardSecurityView.VIEW_REVEALED); newView.setKeyguardCallback(mCallback); } // Find and show this child. final int childCount = mSecurityViewFlipper.getChildCount(); final int securityViewIdForMode = getSecurityViewIdForMode(securityMode); for (int i = 0; i < childCount; i++) { if (mSecurityViewFlipper.getChildAt(i).getId() == securityViewIdForMode) { mSecurityViewFlipper.setDisplayedChild(i); break; } } mCurrentSecuritySelection = securityMode; mCurrentSecurityView = newView; mSecurityCallback.onSecurityModeChanged(securityMode, securityMode != SecurityMode.None && newView.needsInput()); } private KeyguardSecurityViewFlipper getFlipper() { for (int i = 0; i < getChildCount(); i++) { View child = getChildAt(i); if (child instanceof KeyguardSecurityViewFlipper) { return (KeyguardSecurityViewFlipper) child; } } return null; } private KeyguardSecurityCallback mCallback = new KeyguardSecurityCallback() { public void userActivity() { if (mSecurityCallback != null) { mSecurityCallback.userActivity(); } } public void dismiss(boolean authenticated, int targetId) { mSecurityCallback.dismiss(authenticated, targetId); } public boolean isVerifyUnlockOnly() { return mIsVerifyUnlockOnly; } public void reportUnlockAttempt(int userId, boolean success, int timeoutMs) { if (success) { StatsLog.write(StatsLog.KEYGUARD_BOUNCER_PASSWORD_ENTERED, StatsLog.KEYGUARD_BOUNCER_PASSWORD_ENTERED__RESULT__SUCCESS); mLockPatternUtils.reportSuccessfulPasswordAttempt(userId); } else { StatsLog.write(StatsLog.KEYGUARD_BOUNCER_PASSWORD_ENTERED, StatsLog.KEYGUARD_BOUNCER_PASSWORD_ENTERED__RESULT__FAILURE); KeyguardSecurityContainer.this.reportFailedUnlockAttempt(userId, timeoutMs); } mMetricsLogger.write(new LogMaker(MetricsEvent.BOUNCER) .setType(success ? MetricsEvent.TYPE_SUCCESS : MetricsEvent.TYPE_FAILURE)); } public void reset() { mSecurityCallback.reset(); } public void onCancelClicked() { mSecurityCallback.onCancelClicked(); } }; // The following is used to ignore callbacks from SecurityViews that are no longer current // (e.g. face unlock). This avoids unwanted asynchronous events from messing with the // state for the current security method. private KeyguardSecurityCallback mNullCallback = new KeyguardSecurityCallback() { @Override public void userActivity() { } @Override public void reportUnlockAttempt(int userId, boolean success, int timeoutMs) { } @Override public boolean isVerifyUnlockOnly() { return false; } @Override public void dismiss(boolean securityVerified, int targetUserId) { } @Override public void reset() {} }; private int getSecurityViewIdForMode(SecurityMode securityMode) { switch (securityMode) { case Pattern: return R.id.keyguard_pattern_view; case PIN: return R.id.keyguard_pin_view; case Password: return R.id.keyguard_password_view; case SimPin: return R.id.keyguard_sim_pin_view; case SimPuk: return R.id.keyguard_sim_puk_view; } return 0; } @VisibleForTesting public int getLayoutIdFor(SecurityMode securityMode) { switch (securityMode) { case Pattern: return R.layout.keyguard_pattern_view; case PIN: return R.layout.keyguard_pin_view; case Password: return R.layout.keyguard_password_view; case SimPin: return R.layout.keyguard_sim_pin_view; case SimPuk: return R.layout.keyguard_sim_puk_view; default: return 0; } } public SecurityMode getSecurityMode() { return mSecurityModel.getSecurityMode(KeyguardUpdateMonitor.getCurrentUser()); } public SecurityMode getCurrentSecurityMode() { return mCurrentSecuritySelection; } public KeyguardSecurityView getCurrentSecurityView() { return mCurrentSecurityView; } public void verifyUnlock() { mIsVerifyUnlockOnly = true; showSecurityScreen(getSecurityMode()); } public SecurityMode getCurrentSecuritySelection() { return mCurrentSecuritySelection; } public void dismiss(boolean authenticated, int targetUserId) { mCallback.dismiss(authenticated, targetUserId); } public boolean needsInput() { return mSecurityViewFlipper.needsInput(); } @Override public void setKeyguardCallback(KeyguardSecurityCallback callback) { mSecurityViewFlipper.setKeyguardCallback(callback); } @Override public void reset() { mSecurityViewFlipper.reset(); } @Override public KeyguardSecurityCallback getCallback() { return mSecurityViewFlipper.getCallback(); } @Override public void showPromptReason(int reason) { if (mCurrentSecuritySelection != SecurityMode.None) { if (reason != PROMPT_REASON_NONE) { Log.i(TAG, "Strong auth required, reason: " + reason); } getSecurityView(mCurrentSecuritySelection).showPromptReason(reason); } } public void showMessage(CharSequence message, ColorStateList colorState) { if (mCurrentSecuritySelection != SecurityMode.None) { getSecurityView(mCurrentSecuritySelection).showMessage(message, colorState); } } @Override public void showUsabilityHint() { mSecurityViewFlipper.showUsabilityHint(); } }