/* * 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 static android.view.WindowInsets.Type.ime; import static android.view.WindowInsets.Type.systemBars; import static android.view.WindowInsetsAnimation.Callback.DISPATCH_MODE_STOP; import static java.lang.Integer.max; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; import android.app.Activity; import android.app.AlertDialog; import android.content.Context; import android.graphics.Rect; import android.provider.Settings; import android.util.AttributeSet; import android.util.MathUtils; import android.util.TypedValue; import android.view.Gravity; import android.view.MotionEvent; import android.view.VelocityTracker; import android.view.View; import android.view.ViewConfiguration; import android.view.WindowInsets; import android.view.WindowInsetsAnimation; import android.view.WindowManager; import android.view.animation.AnimationUtils; import android.view.animation.Interpolator; import android.widget.FrameLayout; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import androidx.dynamicanimation.animation.DynamicAnimation; import androidx.dynamicanimation.animation.SpringAnimation; import com.android.internal.jank.InteractionJankMonitor; import com.android.internal.logging.UiEvent; import com.android.internal.logging.UiEventLogger; import com.android.internal.widget.LockPatternUtils; import com.android.keyguard.KeyguardSecurityModel.SecurityMode; import com.android.systemui.Gefingerpoken; import com.android.systemui.R; import com.android.systemui.animation.Interpolators; import com.android.systemui.shared.system.SysUiStatsLog; import java.util.ArrayList; import java.util.List; public class KeyguardSecurityContainer extends FrameLayout { static final int USER_TYPE_PRIMARY = 1; static final int USER_TYPE_WORK_PROFILE = 2; static final int USER_TYPE_SECONDARY_USER = 3; // Bouncer is dismissed due to no security. static final int BOUNCER_DISMISS_NONE_SECURITY = 0; // Bouncer is dismissed due to pin, password or pattern entered. static final int BOUNCER_DISMISS_PASSWORD = 1; // Bouncer is dismissed due to biometric (face, fingerprint or iris) authenticated. static final int BOUNCER_DISMISS_BIOMETRIC = 2; // Bouncer is dismissed due to extended access granted. static final int BOUNCER_DISMISS_EXTENDED_ACCESS = 3; // Bouncer is dismissed due to sim card unlock code entered. 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 = 4f; private static final long IME_DISAPPEAR_DURATION_MS = 125; // The duration of the animation to switch bouncer sides. private static final long BOUNCER_HANDEDNESS_ANIMATION_DURATION_MS = 500; // How much of the switch sides animation should be dedicated to fading the bouncer out. The // remainder will fade it back in again. private static final float BOUNCER_HANDEDNESS_ANIMATION_FADE_OUT_PROPORTION = 0.2f; @VisibleForTesting KeyguardSecurityViewFlipper mSecurityViewFlipper; private AlertDialog mAlertDialog; private boolean mSwipeUpToRetry; private final ViewConfiguration mViewConfiguration; private final SpringAnimation mSpringAnimation; private final VelocityTracker mVelocityTracker = VelocityTracker.obtain(); private final List mMotionEventListeners = new ArrayList<>(); private float mLastTouchY = -1; private int mActivePointerId = -1; private boolean mIsDragging; private float mStartTouchY = -1; private boolean mDisappearAnimRunning; private SwipeListener mSwipeListener; private boolean mIsSecurityViewLeftAligned = true; private boolean mOneHandedMode = false; @Nullable private ValueAnimator mRunningOneHandedAnimator; private final WindowInsetsAnimation.Callback mWindowInsetsAnimationCallback = new WindowInsetsAnimation.Callback(DISPATCH_MODE_STOP) { private final Rect mInitialBounds = new Rect(); private final Rect mFinalBounds = new Rect(); @Override public void onPrepare(WindowInsetsAnimation animation) { mSecurityViewFlipper.getBoundsOnScreen(mInitialBounds); } @Override public WindowInsetsAnimation.Bounds onStart(WindowInsetsAnimation animation, WindowInsetsAnimation.Bounds bounds) { if (!mDisappearAnimRunning) { beginJankInstrument(InteractionJankMonitor.CUJ_LOCKSCREEN_PASSWORD_APPEAR); } else { beginJankInstrument( InteractionJankMonitor.CUJ_LOCKSCREEN_PASSWORD_DISAPPEAR); } mSecurityViewFlipper.getBoundsOnScreen(mFinalBounds); return bounds; } @Override public WindowInsets onProgress(WindowInsets windowInsets, List list) { float start = mDisappearAnimRunning ? -(mFinalBounds.bottom - mInitialBounds.bottom) : mInitialBounds.bottom - mFinalBounds.bottom; float end = mDisappearAnimRunning ? -((mFinalBounds.bottom - mInitialBounds.bottom) * 0.75f) : 0f; int translationY = 0; float interpolatedFraction = 1f; for (WindowInsetsAnimation animation : list) { if ((animation.getTypeMask() & WindowInsets.Type.ime()) == 0) { continue; } interpolatedFraction = animation.getInterpolatedFraction(); final int paddingBottom = (int) MathUtils.lerp( start, end, interpolatedFraction); translationY += paddingBottom; } mSecurityViewFlipper.animateForIme(translationY, interpolatedFraction, !mDisappearAnimRunning); return windowInsets; } @Override public void onEnd(WindowInsetsAnimation animation) { if (!mDisappearAnimRunning) { endJankInstrument(InteractionJankMonitor.CUJ_LOCKSCREEN_PASSWORD_APPEAR); mSecurityViewFlipper.animateForIme(0, /* interpolatedFraction */ 1f, true /* appearingAnim */); } else { endJankInstrument(InteractionJankMonitor.CUJ_LOCKSCREEN_PASSWORD_DISAPPEAR); } } }; // Used to notify the container when something interesting happens. public interface SecurityCallback { boolean dismiss(boolean authenticated, int targetUserId, boolean bypassSecondaryLockScreen); void userActivity(); 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. */ void finish(boolean strongAuth, int targetUserId); void reset(); void onCancelClicked(); } public interface SwipeListener { void onSwipeUp(); } @VisibleForTesting public enum BouncerUiEvent implements UiEventLogger.UiEventEnum { @UiEvent(doc = "Default UiEvent used for variable initialization.") UNKNOWN(0), @UiEvent(doc = "Bouncer is dismissed using extended security access.") BOUNCER_DISMISS_EXTENDED_ACCESS(413), @UiEvent(doc = "Bouncer is dismissed using biometric.") BOUNCER_DISMISS_BIOMETRIC(414), @UiEvent(doc = "Bouncer is dismissed without security access.") BOUNCER_DISMISS_NONE_SECURITY(415), @UiEvent(doc = "Bouncer is dismissed using password security.") BOUNCER_DISMISS_PASSWORD(416), @UiEvent(doc = "Bouncer is dismissed using sim security access.") BOUNCER_DISMISS_SIM(417), @UiEvent(doc = "Bouncer is successfully unlocked using password.") BOUNCER_PASSWORD_SUCCESS(418), @UiEvent(doc = "An attempt to unlock bouncer using password has failed.") BOUNCER_PASSWORD_FAILURE(419); private final int mId; BouncerUiEvent(int id) { mId = id; } @Override public int getId() { return mId; } } 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); mSpringAnimation = new SpringAnimation(this, DynamicAnimation.Y); mViewConfiguration = ViewConfiguration.get(context); } void onResume(SecurityMode securityMode, boolean faceAuthEnabled) { mSecurityViewFlipper.setWindowInsetsAnimationCallback(mWindowInsetsAnimationCallback); updateBiometricRetry(securityMode, faceAuthEnabled); } /** * Sets whether this security container is in one handed mode. If so, it will measure its * child SecurityViewFlipper in one half of the screen, and move it when tapping on the opposite * side of the screen. */ public void setOneHandedMode(boolean oneHandedMode) { mOneHandedMode = oneHandedMode; updateSecurityViewGravity(); updateSecurityViewLocation(false); } /** Returns whether this security container is in one-handed mode. */ public boolean isOneHandedMode() { return mOneHandedMode; } /** * When in one-handed mode, sets if the inner SecurityViewFlipper should be aligned to the * left-hand side of the screen or not, and whether to animate when moving between the two. */ public void setOneHandedModeLeftAligned(boolean leftAligned, boolean animate) { mIsSecurityViewLeftAligned = leftAligned; updateSecurityViewLocation(animate); } /** Returns whether the inner SecurityViewFlipper is left-aligned when in one-handed mode. */ public boolean isOneHandedModeLeftAligned() { return mIsSecurityViewLeftAligned; } private void updateSecurityViewGravity() { if (mSecurityViewFlipper == null) { return; } FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) mSecurityViewFlipper.getLayoutParams(); if (mOneHandedMode) { lp.gravity = Gravity.LEFT | Gravity.BOTTOM; } else { lp.gravity = Gravity.CENTER_HORIZONTAL; } mSecurityViewFlipper.setLayoutParams(lp); } /** * Moves the inner security view to the correct location (in one handed mode) with animation. * This is triggered when the user taps on the side of the screen that is not currently occupied * by the security view . */ private void updateSecurityViewLocation(boolean animate) { if (mSecurityViewFlipper == null) { return; } if (!mOneHandedMode) { mSecurityViewFlipper.setTranslationX(0); return; } if (mRunningOneHandedAnimator != null) { mRunningOneHandedAnimator.cancel(); mRunningOneHandedAnimator = null; } int targetTranslation = mIsSecurityViewLeftAligned ? 0 : (int) (getMeasuredWidth() - mSecurityViewFlipper.getWidth()); if (animate) { // This animation is a bit fun to implement. The bouncer needs to move, and fade in/out // at the same time. The issue is, the bouncer should only move a short amount (120dp or // so), but obviously needs to go from one side of the screen to the other. This needs a // pretty custom animation. // // This works as follows. It uses a ValueAnimation to simply drive the animation // progress. This animator is responsible for both the translation of the bouncer, and // the current fade. It will fade the bouncer out while also moving it along the 120dp // path. Once the bouncer is fully faded out though, it will "snap" the bouncer closer // to its destination, then fade it back in again. The effect is that the bouncer will // move from 0 -> X while fading out, then (destination - X) -> destination while fading // back in again. // TODO(b/195012405): Make this animation properly abortable. Interpolator positionInterpolator = AnimationUtils.loadInterpolator( mContext, android.R.interpolator.fast_out_extra_slow_in); Interpolator fadeOutInterpolator = Interpolators.FAST_OUT_LINEAR_IN; Interpolator fadeInInterpolator = Interpolators.LINEAR_OUT_SLOW_IN; mRunningOneHandedAnimator = ValueAnimator.ofFloat(0.0f, 1.0f); mRunningOneHandedAnimator.setDuration(BOUNCER_HANDEDNESS_ANIMATION_DURATION_MS); mRunningOneHandedAnimator.setInterpolator(Interpolators.LINEAR); int initialTranslation = (int) mSecurityViewFlipper.getTranslationX(); int totalTranslation = (int) getResources().getDimension( R.dimen.one_handed_bouncer_move_animation_translation); final boolean shouldRestoreLayerType = mSecurityViewFlipper.hasOverlappingRendering() && mSecurityViewFlipper.getLayerType() != View.LAYER_TYPE_HARDWARE; if (shouldRestoreLayerType) { mSecurityViewFlipper.setLayerType(View.LAYER_TYPE_HARDWARE, /* paint= */null); } float initialAlpha = mSecurityViewFlipper.getAlpha(); mRunningOneHandedAnimator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { mRunningOneHandedAnimator = null; } }); mRunningOneHandedAnimator.addUpdateListener(animation -> { float switchPoint = BOUNCER_HANDEDNESS_ANIMATION_FADE_OUT_PROPORTION; boolean isFadingOut = animation.getAnimatedFraction() < switchPoint; int currentTranslation = (int) (positionInterpolator.getInterpolation( animation.getAnimatedFraction()) * totalTranslation); int translationRemaining = totalTranslation - currentTranslation; // Flip the sign if we're going from right to left. if (mIsSecurityViewLeftAligned) { currentTranslation = -currentTranslation; translationRemaining = -translationRemaining; } if (isFadingOut) { // The bouncer fades out over the first X%. float fadeOutFraction = MathUtils.constrainedMap( /* rangeMin= */1.0f, /* rangeMax= */0.0f, /* valueMin= */0.0f, /* valueMax= */switchPoint, animation.getAnimatedFraction()); float opacity = fadeOutInterpolator.getInterpolation(fadeOutFraction); // When fading out, the alpha needs to start from the initial opacity of the // view flipper, otherwise we get a weird bit of jank as it ramps back to 100%. mSecurityViewFlipper.setAlpha(opacity * initialAlpha); // Animate away from the source. mSecurityViewFlipper.setTranslationX(initialTranslation + currentTranslation); } else { // And in again over the remaining (100-X)%. float fadeInFraction = MathUtils.constrainedMap( /* rangeMin= */0.0f, /* rangeMax= */1.0f, /* valueMin= */switchPoint, /* valueMax= */1.0f, animation.getAnimatedFraction()); float opacity = fadeInInterpolator.getInterpolation(fadeInFraction); mSecurityViewFlipper.setAlpha(opacity); // Fading back in, animate towards the destination. mSecurityViewFlipper.setTranslationX(targetTranslation - translationRemaining); } if (animation.getAnimatedFraction() == 1.0f && shouldRestoreLayerType) { mSecurityViewFlipper.setLayerType(View.LAYER_TYPE_NONE, /* paint= */null); } }); mRunningOneHandedAnimator.start(); } else { mSecurityViewFlipper.setTranslationX(targetTranslation); } } public void onPause() { if (mAlertDialog != null) { mAlertDialog.dismiss(); mAlertDialog = null; } mSecurityViewFlipper.setWindowInsetsAnimationCallback(null); } @Override public boolean shouldDelayChildPressedState() { return true; } @Override public boolean onInterceptTouchEvent(MotionEvent event) { boolean result = mMotionEventListeners.stream().anyMatch( listener -> listener.onInterceptTouchEvent(event)) || super.onInterceptTouchEvent(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 (mSecurityViewFlipper.getSecurityView().disallowInterceptTouch(event)) { return false; } int index = event.findPointerIndex(mActivePointerId); float touchSlop = mViewConfiguration.getScaledTouchSlop() * SLOP_SCALE; if (index != -1 && mStartTouchY - event.getY(index) > touchSlop) { mIsDragging = true; return true; } break; case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: mIsDragging = false; break; } return result; } @Override public boolean onTouchEvent(MotionEvent event) { final int action = event.getActionMasked(); boolean result = mMotionEventListeners.stream() .anyMatch(listener -> listener.onTouchEvent(event)) || super.onTouchEvent(event); 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())) { if (mSwipeListener != null) { mSwipeListener.onSwipeUp(); } } else { if (!mIsDragging) { handleTap(event); } } } return true; } void addMotionEventListener(Gefingerpoken listener) { mMotionEventListeners.add(listener); } void removeMotionEventListener(Gefingerpoken listener) { mMotionEventListeners.remove(listener); } private void handleTap(MotionEvent event) { // If we're using a fullscreen security mode, skip if (!mOneHandedMode) { return; } moveBouncerForXCoordinate(event.getX(), /* animate= */true); } private void moveBouncerForXCoordinate(float x, boolean animate) { // Did the tap hit the "other" side of the bouncer? if ((mIsSecurityViewLeftAligned && (x > getWidth() / 2f)) || (!mIsSecurityViewLeftAligned && (x < getWidth() / 2f))) { mIsSecurityViewLeftAligned = !mIsSecurityViewLeftAligned; Settings.Global.putInt( mContext.getContentResolver(), Settings.Global.ONE_HANDED_KEYGUARD_SIDE, mIsSecurityViewLeftAligned ? Settings.Global.ONE_HANDED_KEYGUARD_SIDE_LEFT : Settings.Global.ONE_HANDED_KEYGUARD_SIDE_RIGHT); int keyguardState = mIsSecurityViewLeftAligned ? SysUiStatsLog.KEYGUARD_BOUNCER_STATE_CHANGED__STATE__SWITCH_LEFT : SysUiStatsLog.KEYGUARD_BOUNCER_STATE_CHANGED__STATE__SWITCH_RIGHT; SysUiStatsLog.write(SysUiStatsLog.KEYGUARD_BOUNCER_STATE_CHANGED, keyguardState); updateSecurityViewLocation(animate); } } void setSwipeListener(SwipeListener swipeListener) { mSwipeListener = swipeListener; } private void startSpringAnimation(float startVelocity) { mSpringAnimation .setStartVelocity(startVelocity) .animateToFinalPosition(0); } public void startDisappearAnimation(SecurityMode securitySelection) { mDisappearAnimRunning = true; } private void beginJankInstrument(int cuj) { KeyguardInputView securityView = mSecurityViewFlipper.getSecurityView(); if (securityView == null) return; InteractionJankMonitor.getInstance().begin(securityView, cuj); } private void endJankInstrument(int cuj) { InteractionJankMonitor.getInstance().end(cuj); } private void cancelJankInstrument(int cuj) { InteractionJankMonitor.getInstance().cancel(cuj); } /** * Enables/disables swipe up to retry on the bouncer. */ private void updateBiometricRetry(SecurityMode securityMode, boolean faceAuthEnabled) { mSwipeUpToRetry = faceAuthEnabled && securityMode != SecurityMode.SimPin && securityMode != SecurityMode.SimPuk && securityMode != SecurityMode.None; } public CharSequence getTitle() { return mSecurityViewFlipper.getTitle(); } @Override public void onFinishInflate() { super.onFinishInflate(); mSecurityViewFlipper = findViewById(R.id.view_flipper); } @Override public WindowInsets onApplyWindowInsets(WindowInsets insets) { // Consume bottom insets because we're setting the padding locally (for IME and navbar.) int bottomInset = insets.getInsetsIgnoringVisibility(systemBars()).bottom; int imeInset = insets.getInsets(ime()).bottom; int inset = max(bottomInset, imeInset); setPadding(getPaddingLeft(), getPaddingTop(), getPaddingRight(), inset); return insets.inset(0, 0, 0, inset); } 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(); } void showTimeoutDialog(int userId, int timeoutMs, LockPatternUtils lockPatternUtils, SecurityMode securityMode) { int timeoutInSeconds = timeoutMs / 1000; int messageId = 0; switch (securityMode) { 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, lockPatternUtils.getCurrentFailedPasswordAttempts(userId), timeoutInSeconds); showDialog(null, message); } } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int maxHeight = 0; int maxWidth = 0; int childState = 0; int halfWidthMeasureSpec = MeasureSpec.makeMeasureSpec( MeasureSpec.getSize(widthMeasureSpec) / 2, MeasureSpec.getMode(widthMeasureSpec)); for (int i = 0; i < getChildCount(); i++) { final View view = getChildAt(i); if (view.getVisibility() != GONE) { if (mOneHandedMode && view == mSecurityViewFlipper) { measureChildWithMargins(view, halfWidthMeasureSpec, 0, heightMeasureSpec, 0); } else { measureChildWithMargins(view, widthMeasureSpec, 0, heightMeasureSpec, 0); } final LayoutParams lp = (LayoutParams) view.getLayoutParams(); maxWidth = Math.max(maxWidth, view.getMeasuredWidth() + lp.leftMargin + lp.rightMargin); maxHeight = Math.max(maxHeight, view.getMeasuredHeight() + lp.topMargin + lp.bottomMargin); childState = combineMeasuredStates(childState, view.getMeasuredState()); } } maxWidth += getPaddingLeft() + getPaddingRight(); maxHeight += getPaddingTop() + getPaddingBottom(); // Check against our minimum height and width maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight()); maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth()); setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState), resolveSizeAndState(maxHeight, heightMeasureSpec, childState << MEASURED_HEIGHT_STATE_SHIFT)); } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); // After a layout pass, we need to re-place the inner bouncer, as our bounds may have // changed. updateSecurityViewLocation(/* animate= */false); } 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); } 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); } public void reset() { mDisappearAnimRunning = false; } }