blob: 12bb47b81d7fc888ebb4ebdb8e62884dcd51cc21 [file] [log] [blame]
/*
* 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.app.admin.DevicePolicyResources.Strings.SystemUi.KEYGUARD_DIALOG_FAILED_ATTEMPTS_ALMOST_ERASING_PROFILE;
import static android.app.admin.DevicePolicyResources.Strings.SystemUi.KEYGUARD_DIALOG_FAILED_ATTEMPTS_ERASING_PROFILE;
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 com.android.systemui.plugins.FalsingManager.LOW_PENALTY;
import static com.android.systemui.statusbar.policy.UserSwitcherController.USER_SWITCH_DISABLED_ALPHA;
import static com.android.systemui.statusbar.policy.UserSwitcherController.USER_SWITCH_ENABLED_ALPHA;
import static java.lang.Integer.max;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.admin.DevicePolicyManager;
import android.content.Context;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BlendMode;
import android.graphics.Rect;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.LayerDrawable;
import android.os.UserManager;
import android.provider.Settings;
import android.util.AttributeSet;
import android.util.Log;
import android.util.MathUtils;
import android.util.TypedValue;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
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.AdapterView;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
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.util.UserIcons;
import com.android.internal.widget.LockPatternUtils;
import com.android.keyguard.KeyguardSecurityModel.SecurityMode;
import com.android.settingslib.Utils;
import com.android.systemui.Gefingerpoken;
import com.android.systemui.R;
import com.android.systemui.animation.Interpolators;
import com.android.systemui.plugins.FalsingManager;
import com.android.systemui.shared.system.SysUiStatsLog;
import com.android.systemui.statusbar.policy.UserSwitcherController;
import com.android.systemui.statusbar.policy.UserSwitcherController.BaseUserAdapter;
import com.android.systemui.statusbar.policy.UserSwitcherController.UserRecord;
import com.android.systemui.util.settings.GlobalSettings;
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;
@IntDef({MODE_DEFAULT, MODE_ONE_HANDED, MODE_USER_SWITCHER})
public @interface Mode {}
static final int MODE_DEFAULT = 0;
static final int MODE_ONE_HANDED = 1;
static final int MODE_USER_SWITCHER = 2;
// 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;
private static final String TAG = "KeyguardSecurityView";
// 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 GlobalSettings mGlobalSettings;
private FalsingManager mFalsingManager;
private UserSwitcherController mUserSwitcherController;
private AlertDialog mAlertDialog;
private boolean mSwipeUpToRetry;
private final ViewConfiguration mViewConfiguration;
private final SpringAnimation mSpringAnimation;
private final VelocityTracker mVelocityTracker = VelocityTracker.obtain();
private final List<Gefingerpoken> 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 ViewMode mViewMode = new DefaultViewMode();
private @Mode int mCurrentMode = MODE_DEFAULT;
private int mWidth = -1;
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<WindowInsetsAnimation> 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;
}
float alpha = mDisappearAnimRunning
? 1 - interpolatedFraction
: Math.max(interpolatedFraction, getAlpha());
updateChildren(translationY, alpha);
return windowInsets;
}
@Override
public void onEnd(WindowInsetsAnimation animation) {
if (!mDisappearAnimRunning) {
endJankInstrument(InteractionJankMonitor.CUJ_LOCKSCREEN_PASSWORD_APPEAR);
updateChildren(0 /* translationY */, 1f /* alpha */);
} else {
endJankInstrument(InteractionJankMonitor.CUJ_LOCKSCREEN_PASSWORD_DISAPPEAR);
}
}
private void updateChildren(int translationY, float alpha) {
for (int i = 0; i < KeyguardSecurityContainer.this.getChildCount(); ++i) {
View child = KeyguardSecurityContainer.this.getChildAt(i);
child.setTranslationY(translationY);
child.setAlpha(alpha);
}
}
};
// Used to notify the container when something interesting happens.
public interface SecurityCallback {
/**
* Potentially dismiss the current security screen, after validating that all device
* security has been unlocked. Otherwise show the next screen.
*/
boolean dismiss(boolean authenticated, int targetUserId, boolean bypassSecondaryLockScreen,
SecurityMode expectedSecurityMode);
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);
}
void initMode(@Mode int mode, GlobalSettings globalSettings, FalsingManager falsingManager,
UserSwitcherController userSwitcherController) {
if (mCurrentMode == mode) return;
Log.i(TAG, "Switching mode from " + modeToString(mCurrentMode) + " to "
+ modeToString(mode));
mCurrentMode = mode;
mViewMode.onDestroy();
switch (mode) {
case MODE_ONE_HANDED:
mViewMode = new OneHandedViewMode();
break;
case MODE_USER_SWITCHER:
mViewMode = new UserSwitcherViewMode();
break;
default:
mViewMode = new DefaultViewMode();
}
mGlobalSettings = globalSettings;
mFalsingManager = falsingManager;
mUserSwitcherController = userSwitcherController;
setupViewMode();
}
private String modeToString(@Mode int mode) {
switch (mode) {
case MODE_DEFAULT:
return "Default";
case MODE_ONE_HANDED:
return "OneHanded";
case MODE_USER_SWITCHER:
return "UserSwitcher";
default:
throw new IllegalArgumentException("mode: " + mode + " not supported");
}
}
private void setupViewMode() {
if (mSecurityViewFlipper == null || mGlobalSettings == null
|| mFalsingManager == null || mUserSwitcherController == null) {
return;
}
mViewMode.init(this, mGlobalSettings, mSecurityViewFlipper, mFalsingManager,
mUserSwitcherController);
}
@Mode int getMode() {
return mCurrentMode;
}
/**
* The position of the container can be adjusted based upon a touch at location x. This has
* been used in one-handed mode to make sure the bouncer appears on the side of the display
* that the user last interacted with.
*/
void updatePositionByTouchX(float x) {
mViewMode.updatePositionByTouchX(x);
}
/** Returns whether the inner SecurityViewFlipper is left-aligned when in one-handed mode. */
public boolean isOneHandedModeLeftAligned() {
return mCurrentMode == MODE_ONE_HANDED
&& ((OneHandedViewMode) mViewMode).isLeftAligned();
}
public void onPause() {
if (mAlertDialog != null) {
mAlertDialog.dismiss();
mAlertDialog = null;
}
mSecurityViewFlipper.setWindowInsetsAnimationCallback(null);
mViewMode.reset();
}
@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) {
mViewMode.handleTap(event);
}
}
}
return true;
}
void addMotionEventListener(Gefingerpoken listener) {
mMotionEventListeners.add(listener);
}
void removeMotionEventListener(Gefingerpoken listener) {
mMotionEventListeners.remove(listener);
}
void setSwipeListener(SwipeListener swipeListener) {
mSwipeListener = swipeListener;
}
private void startSpringAnimation(float startVelocity) {
mSpringAnimation
.setStartVelocity(startVelocity)
.animateToFinalPosition(0);
}
/**
* Runs after a succsssful authentication only
*/
public void startDisappearAnimation(SecurityMode securitySelection) {
mDisappearAnimRunning = true;
mViewMode.startDisappearAnimation(securitySelection);
}
/**
* This will run when the bouncer shows in all cases except when the user drags the bouncer up.
*/
public void startAppearAnimation(SecurityMode securityMode) {
mViewMode.startAppearAnimation(securityMode);
}
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);
int paddingBottom = max(inset, getContext().getResources()
.getDimensionPixelSize(R.dimen.keyguard_security_view_bottom_margin));
setPadding(getPaddingLeft(), getPaddingTop(), getPaddingRight(), paddingBottom);
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;
for (int i = 0; i < getChildCount(); i++) {
final View view = getChildAt(i);
if (view.getVisibility() != GONE) {
int updatedWidthMeasureSpec = mViewMode.getChildWidthMeasureSpec(widthMeasureSpec);
final LayoutParams lp = (LayoutParams) view.getLayoutParams();
// When using EXACTLY spec, measure will use the layout width if > 0. Set before
// measuring the child
lp.width = MeasureSpec.getSize(updatedWidthMeasureSpec);
measureChildWithMargins(view, updatedWidthMeasureSpec, 0, heightMeasureSpec, 0);
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);
int width = right - left;
if (changed && mWidth != width) {
mWidth = width;
mViewMode.updateSecurityViewLocation();
}
}
@Override
protected void onConfigurationChanged(Configuration config) {
super.onConfigurationChanged(config);
mViewMode.updateSecurityViewLocation();
}
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.getSystemService(DevicePolicyManager.class).getResources()
.getString(KEYGUARD_DIALOG_FAILED_ATTEMPTS_ALMOST_ERASING_PROFILE,
() -> mContext.getString(
R.string.kg_failed_attempts_almost_at_erase_profile,
attempts, remaining),
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.getSystemService(DevicePolicyManager.class).getResources()
.getString(KEYGUARD_DIALOG_FAILED_ATTEMPTS_ERASING_PROFILE,
() -> mContext.getString(
R.string.kg_failed_attempts_now_erasing_profile, attempts),
attempts);
break;
}
showDialog(null, message);
}
public void reset() {
mViewMode.reset();
mDisappearAnimRunning = false;
}
void reloadColors() {
mViewMode.reloadColors();
}
/**
* Enscapsulates the differences between bouncer modes for the container.
*/
interface ViewMode {
default void init(@NonNull ViewGroup v, @NonNull GlobalSettings globalSettings,
@NonNull KeyguardSecurityViewFlipper viewFlipper,
@NonNull FalsingManager falsingManager,
@NonNull UserSwitcherController userSwitcherController) {};
/** Reinitialize the location */
default void updateSecurityViewLocation() {};
/** Alter the ViewFlipper position, based upon a touch outside of it */
default void updatePositionByTouchX(float x) {};
/** A tap on the container, outside of the ViewFlipper */
default void handleTap(MotionEvent event) {};
/** Called when the view needs to reset or hides */
default void reset() {};
/** Refresh colors */
default void reloadColors() {};
/** On a successful auth, optionally handle how the view disappears */
default void startDisappearAnimation(SecurityMode securityMode) {};
/** On notif tap, this animation will run */
default void startAppearAnimation(SecurityMode securityMode) {};
/** Override to alter the width measure spec to perhaps limit the ViewFlipper size */
default int getChildWidthMeasureSpec(int parentWidthMeasureSpec) {
return parentWidthMeasureSpec;
}
/** Called when we are setting a new ViewMode */
default void onDestroy() {};
}
/**
* Default bouncer is centered within the space
*/
static class DefaultViewMode implements ViewMode {
private ViewGroup mView;
private KeyguardSecurityViewFlipper mViewFlipper;
@Override
public void init(@NonNull ViewGroup v, @NonNull GlobalSettings globalSettings,
@NonNull KeyguardSecurityViewFlipper viewFlipper,
@NonNull FalsingManager falsingManager,
@NonNull UserSwitcherController userSwitcherController) {
mView = v;
mViewFlipper = viewFlipper;
// Reset ViewGroup to default positions
updateSecurityViewGroup();
}
private void updateSecurityViewGroup() {
FrameLayout.LayoutParams lp =
(FrameLayout.LayoutParams) mViewFlipper.getLayoutParams();
lp.gravity = Gravity.CENTER_HORIZONTAL;
mViewFlipper.setLayoutParams(lp);
mViewFlipper.setTranslationX(0);
}
}
/**
* User switcher mode will display both the current user icon as well as
* a user switcher, in both portrait and landscape modes.
*/
static class UserSwitcherViewMode implements ViewMode {
private ViewGroup mView;
private ViewGroup mUserSwitcherViewGroup;
private KeyguardSecurityViewFlipper mViewFlipper;
private TextView mUserSwitcher;
private FalsingManager mFalsingManager;
private UserSwitcherController mUserSwitcherController;
private KeyguardUserSwitcherPopupMenu mPopup;
private Resources mResources;
private UserSwitcherController.UserSwitchCallback mUserSwitchCallback =
this::setupUserSwitcher;
@Override
public void init(@NonNull ViewGroup v, @NonNull GlobalSettings globalSettings,
@NonNull KeyguardSecurityViewFlipper viewFlipper,
@NonNull FalsingManager falsingManager,
@NonNull UserSwitcherController userSwitcherController) {
mView = v;
mViewFlipper = viewFlipper;
mFalsingManager = falsingManager;
mUserSwitcherController = userSwitcherController;
mResources = v.getContext().getResources();
if (mUserSwitcherViewGroup == null) {
LayoutInflater.from(v.getContext()).inflate(
R.layout.keyguard_bouncer_user_switcher,
mView,
true);
mUserSwitcherViewGroup = mView.findViewById(R.id.keyguard_bouncer_user_switcher);
}
updateSecurityViewLocation();
mUserSwitcher = mView.findViewById(R.id.user_switcher_header);
setupUserSwitcher();
mUserSwitcherController.addUserSwitchCallback(mUserSwitchCallback);
}
@Override
public void reset() {
if (mPopup != null) {
mPopup.dismiss();
mPopup = null;
}
}
@Override
public void reloadColors() {
TextView header = (TextView) mView.findViewById(R.id.user_switcher_header);
if (header != null) {
header.setTextColor(Utils.getColorAttrDefaultColor(mView.getContext(),
android.R.attr.textColorPrimary));
header.setBackground(mView.getContext().getDrawable(
R.drawable.bouncer_user_switcher_header_bg));
}
}
@Override
public void onDestroy() {
mUserSwitcherController.removeUserSwitchCallback(mUserSwitchCallback);
}
private Drawable findUserIcon(int userId) {
Bitmap userIcon = UserManager.get(mView.getContext()).getUserIcon(userId);
if (userIcon != null) {
return new BitmapDrawable(userIcon);
}
return UserIcons.getDefaultUserIcon(mResources, userId, false);
}
@Override
public void startAppearAnimation(SecurityMode securityMode) {
// IME insets animations handle alpha and translation
if (securityMode == SecurityMode.Password) {
return;
}
mUserSwitcherViewGroup.setAlpha(0f);
ObjectAnimator alphaAnim = ObjectAnimator.ofFloat(mUserSwitcherViewGroup, View.ALPHA,
1f);
alphaAnim.setInterpolator(Interpolators.ALPHA_IN);
alphaAnim.setDuration(500);
alphaAnim.start();
}
@Override
public void startDisappearAnimation(SecurityMode securityMode) {
// IME insets animations handle alpha and translation
if (securityMode == SecurityMode.Password) {
return;
}
int yTranslation = mResources.getDimensionPixelSize(R.dimen.disappear_y_translation);
AnimatorSet anims = new AnimatorSet();
ObjectAnimator yAnim = ObjectAnimator.ofFloat(mView, View.TRANSLATION_Y, yTranslation);
ObjectAnimator alphaAnim = ObjectAnimator.ofFloat(mView, View.ALPHA, 0f);
anims.setInterpolator(Interpolators.STANDARD_ACCELERATE);
anims.playTogether(alphaAnim, yAnim);
anims.start();
}
private void setupUserSwitcher() {
final UserRecord currentUser = mUserSwitcherController.getCurrentUserRecord();
if (currentUser == null) {
Log.e(TAG, "Current user in user switcher is null.");
return;
}
Drawable userIcon = findUserIcon(currentUser.info.id);
((ImageView) mView.findViewById(R.id.user_icon)).setImageDrawable(userIcon);
mUserSwitcher.setText(mUserSwitcherController.getCurrentUserName());
ViewGroup anchor = mView.findViewById(R.id.user_switcher_anchor);
BaseUserAdapter adapter = new BaseUserAdapter(mUserSwitcherController) {
@Override
public View getView(int position, View convertView, ViewGroup parent) {
UserRecord item = getItem(position);
FrameLayout view = (FrameLayout) convertView;
if (view == null) {
view = (FrameLayout) LayoutInflater.from(parent.getContext()).inflate(
R.layout.keyguard_bouncer_user_switcher_item,
parent,
false);
}
TextView textView = (TextView) view.getChildAt(0);
textView.setText(getName(parent.getContext(), item));
Drawable icon = null;
if (item.picture != null) {
icon = new BitmapDrawable(item.picture);
} else {
icon = getDrawable(item, view.getContext());
}
int iconSize = view.getResources().getDimensionPixelSize(
R.dimen.bouncer_user_switcher_item_icon_size);
int iconPadding = view.getResources().getDimensionPixelSize(
R.dimen.bouncer_user_switcher_item_icon_padding);
icon.setBounds(0, 0, iconSize, iconSize);
textView.setCompoundDrawablePadding(iconPadding);
textView.setCompoundDrawablesRelative(icon, null, null, null);
if (item == currentUser) {
textView.setBackground(view.getContext().getDrawable(
R.drawable.bouncer_user_switcher_item_selected_bg));
} else {
textView.setBackground(null);
}
textView.setSelected(item == currentUser);
view.setEnabled(item.isSwitchToEnabled);
view.setAlpha(view.isEnabled() ? USER_SWITCH_ENABLED_ALPHA :
USER_SWITCH_DISABLED_ALPHA);
return view;
}
private Drawable getDrawable(UserRecord item, Context context) {
Drawable drawable;
if (item.isCurrent && item.isGuest) {
drawable = context.getDrawable(R.drawable.ic_avatar_guest_user);
} else {
drawable = getIconDrawable(context, item);
}
int iconColor;
if (item.isSwitchToEnabled) {
iconColor = Utils.getColorAttrDefaultColor(context,
com.android.internal.R.attr.colorAccentPrimaryVariant);
} else {
iconColor = context.getResources().getColor(
R.color.kg_user_switcher_restricted_avatar_icon_color,
context.getTheme());
}
drawable.setTint(iconColor);
Drawable bg = context.getDrawable(R.drawable.user_avatar_bg);
bg.setTintBlendMode(BlendMode.DST);
bg.setTint(Utils.getColorAttrDefaultColor(context,
com.android.internal.R.attr.colorSurfaceVariant));
drawable = new LayerDrawable(new Drawable[]{bg, drawable});
return drawable;
}
};
if (adapter.getCount() < 2) {
// The drop down arrow is at index 1
((LayerDrawable) mUserSwitcher.getBackground()).getDrawable(1).setAlpha(0);
anchor.setClickable(false);
return;
} else {
((LayerDrawable) mUserSwitcher.getBackground()).getDrawable(1).setAlpha(255);
}
anchor.setOnClickListener((v) -> {
if (mFalsingManager.isFalseTap(LOW_PENALTY)) return;
mPopup = new KeyguardUserSwitcherPopupMenu(v.getContext(), mFalsingManager);
mPopup.setAnchorView(anchor);
mPopup.setAdapter(adapter);
mPopup.setOnItemClickListener(new AdapterView.OnItemClickListener() {
public void onItemClick(AdapterView parent, View view, int pos, long id) {
if (mFalsingManager.isFalseTap(LOW_PENALTY)) return;
if (!view.isEnabled()) return;
// Subtract one for the header
UserRecord user = adapter.getItem(pos - 1);
if (!user.isCurrent) {
adapter.onUserListItemClicked(user);
}
mPopup.dismiss();
mPopup = null;
}
});
mPopup.show();
});
}
/**
* Each view will get half the width. Yes, it would be easier to use something other than
* FrameLayout but it was too disruptive to downstream projects to change.
*/
@Override
public int getChildWidthMeasureSpec(int parentWidthMeasureSpec) {
return MeasureSpec.makeMeasureSpec(
MeasureSpec.getSize(parentWidthMeasureSpec) / 2,
MeasureSpec.EXACTLY);
}
@Override
public void updateSecurityViewLocation() {
int yTrans = mResources.getDimensionPixelSize(R.dimen.bouncer_user_switcher_y_trans);
if (mResources.getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT) {
updateViewGravity(mViewFlipper, Gravity.CENTER_HORIZONTAL);
updateViewGravity(mUserSwitcherViewGroup, Gravity.CENTER_HORIZONTAL);
mUserSwitcherViewGroup.setTranslationY(yTrans);
mViewFlipper.setTranslationY(-yTrans);
} else {
updateViewGravity(mViewFlipper, Gravity.RIGHT | Gravity.BOTTOM);
updateViewGravity(mUserSwitcherViewGroup, Gravity.LEFT | Gravity.CENTER_VERTICAL);
// Attempt to reposition a bit higher to make up for this frame being a bit lower
// on the device
mUserSwitcherViewGroup.setTranslationY(-yTrans);
mViewFlipper.setTranslationY(0);
}
}
private void updateViewGravity(View v, int gravity) {
FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) v.getLayoutParams();
lp.gravity = gravity;
v.setLayoutParams(lp);
}
}
/**
* Logic to enabled one-handed bouncer mode. Supports animating the bouncer
* between alternate sides of the display.
*/
static class OneHandedViewMode implements ViewMode {
@Nullable private ValueAnimator mRunningOneHandedAnimator;
private ViewGroup mView;
private KeyguardSecurityViewFlipper mViewFlipper;
private GlobalSettings mGlobalSettings;
@Override
public void init(@NonNull ViewGroup v, @NonNull GlobalSettings globalSettings,
@NonNull KeyguardSecurityViewFlipper viewFlipper,
@NonNull FalsingManager falsingManager,
@NonNull UserSwitcherController userSwitcherController) {
mView = v;
mViewFlipper = viewFlipper;
mGlobalSettings = globalSettings;
updateSecurityViewGravity();
updateSecurityViewLocation(isLeftAligned(), /* animate= */false);
}
/**
* One-handed mode contains the child to half of the available space.
*/
@Override
public int getChildWidthMeasureSpec(int parentWidthMeasureSpec) {
return MeasureSpec.makeMeasureSpec(
MeasureSpec.getSize(parentWidthMeasureSpec) / 2,
MeasureSpec.EXACTLY);
}
private void updateSecurityViewGravity() {
FrameLayout.LayoutParams lp =
(FrameLayout.LayoutParams) mViewFlipper.getLayoutParams();
lp.gravity = Gravity.LEFT | Gravity.BOTTOM;
mViewFlipper.setLayoutParams(lp);
}
/**
* Moves the bouncer to align with a tap (most likely in the shade), so the bouncer
* appears on the same side as a touch.
*/
@Override
public void updatePositionByTouchX(float x) {
boolean isTouchOnLeft = x <= mView.getWidth() / 2f;
updateSideSetting(isTouchOnLeft);
updateSecurityViewLocation(isTouchOnLeft, /* animate= */false);
}
boolean isLeftAligned() {
return mGlobalSettings.getInt(Settings.Global.ONE_HANDED_KEYGUARD_SIDE,
Settings.Global.ONE_HANDED_KEYGUARD_SIDE_LEFT)
== Settings.Global.ONE_HANDED_KEYGUARD_SIDE_LEFT;
}
private void updateSideSetting(boolean leftAligned) {
mGlobalSettings.putInt(
Settings.Global.ONE_HANDED_KEYGUARD_SIDE,
leftAligned ? Settings.Global.ONE_HANDED_KEYGUARD_SIDE_LEFT
: Settings.Global.ONE_HANDED_KEYGUARD_SIDE_RIGHT);
}
/**
* Determine if a tap on this view is on the other side. If so, will animate positions
* and record the preference to always show on this side.
*/
@Override
public void handleTap(MotionEvent event) {
float x = event.getX();
boolean currentlyLeftAligned = isLeftAligned();
// Did the tap hit the "other" side of the bouncer?
if ((currentlyLeftAligned && (x > mView.getWidth() / 2f))
|| (!currentlyLeftAligned && (x < mView.getWidth() / 2f))) {
boolean willBeLeftAligned = !currentlyLeftAligned;
updateSideSetting(willBeLeftAligned);
int keyguardState = willBeLeftAligned
? 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(willBeLeftAligned, true /* animate */);
}
}
@Override
public void updateSecurityViewLocation() {
updateSecurityViewLocation(isLeftAligned(), /* animate= */false);
}
/**
* 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 leftAlign, boolean animate) {
if (mRunningOneHandedAnimator != null) {
mRunningOneHandedAnimator.cancel();
mRunningOneHandedAnimator = null;
}
int targetTranslation = leftAlign
? 0 : (int) (mView.getMeasuredWidth() - mViewFlipper.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/208250221): Make this animation properly abortable.
Interpolator positionInterpolator = AnimationUtils.loadInterpolator(
mView.getContext(), 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) mViewFlipper.getTranslationX();
int totalTranslation = (int) mView.getResources().getDimension(
R.dimen.one_handed_bouncer_move_animation_translation);
final boolean shouldRestoreLayerType = mViewFlipper.hasOverlappingRendering()
&& mViewFlipper.getLayerType() != View.LAYER_TYPE_HARDWARE;
if (shouldRestoreLayerType) {
mViewFlipper.setLayerType(View.LAYER_TYPE_HARDWARE, /* paint= */null);
}
float initialAlpha = mViewFlipper.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 (leftAlign) {
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%.
mViewFlipper.setAlpha(opacity * initialAlpha);
// Animate away from the source.
mViewFlipper.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);
mViewFlipper.setAlpha(opacity);
// Fading back in, animate towards the destination.
mViewFlipper.setTranslationX(targetTranslation - translationRemaining);
}
if (animation.getAnimatedFraction() == 1.0f && shouldRestoreLayerType) {
mViewFlipper.setLayerType(View.LAYER_TYPE_NONE, /* paint= */null);
}
});
mRunningOneHandedAnimator.start();
} else {
mViewFlipper.setTranslationX(targetTranslation);
}
}
}
}