diff --git a/packages/SystemUI/res-keyguard/values/arrays.xml b/packages/SystemUI/res-keyguard/values/arrays.xml index a8b3c1bf97971..26bc865bcaf70 100644 --- a/packages/SystemUI/res-keyguard/values/arrays.xml +++ b/packages/SystemUI/res-keyguard/values/arrays.xml @@ -32,4 +32,13 @@ TUV WXYZ + + + @drawable/pin_dot_shape_1_avd + @drawable/pin_dot_shape_2_avd + @drawable/pin_dot_shape_3_avd + @drawable/pin_dot_shape_4_avd + @drawable/pin_dot_shape_5_avd + @drawable/pin_dot_shape_6_avd + diff --git a/packages/SystemUI/res-keyguard/values/dimens.xml b/packages/SystemUI/res-keyguard/values/dimens.xml index 6cc5b9d7b7e88..b9c7be204d9d6 100644 --- a/packages/SystemUI/res-keyguard/values/dimens.xml +++ b/packages/SystemUI/res-keyguard/values/dimens.xml @@ -69,6 +69,10 @@ 9dp + + + 34dp + 40 @@ -139,4 +143,9 @@ 80dp + + + 34dp + 0dp + diff --git a/packages/SystemUI/res/drawable/pin_dot_avd.xml b/packages/SystemUI/res/drawable/pin_dot_avd.xml new file mode 100644 index 0000000000000..e0cd1fb2e1297 --- /dev/null +++ b/packages/SystemUI/res/drawable/pin_dot_avd.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/SystemUI/res/drawable/pin_dot_delete_avd.xml b/packages/SystemUI/res/drawable/pin_dot_delete_avd.xml new file mode 100644 index 0000000000000..33f995cfb7b69 --- /dev/null +++ b/packages/SystemUI/res/drawable/pin_dot_delete_avd.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/SystemUI/res/drawable/pin_dot_shape_1_avd.xml b/packages/SystemUI/res/drawable/pin_dot_shape_1_avd.xml new file mode 100644 index 0000000000000..da936a28e34fb --- /dev/null +++ b/packages/SystemUI/res/drawable/pin_dot_shape_1_avd.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/SystemUI/res/drawable/pin_dot_shape_2_avd.xml b/packages/SystemUI/res/drawable/pin_dot_shape_2_avd.xml new file mode 100644 index 0000000000000..d910990ae248d --- /dev/null +++ b/packages/SystemUI/res/drawable/pin_dot_shape_2_avd.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/SystemUI/res/drawable/pin_dot_shape_3_avd.xml b/packages/SystemUI/res/drawable/pin_dot_shape_3_avd.xml new file mode 100644 index 0000000000000..cf08899979701 --- /dev/null +++ b/packages/SystemUI/res/drawable/pin_dot_shape_3_avd.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/SystemUI/res/drawable/pin_dot_shape_4_avd.xml b/packages/SystemUI/res/drawable/pin_dot_shape_4_avd.xml new file mode 100644 index 0000000000000..76ee65b0060c4 --- /dev/null +++ b/packages/SystemUI/res/drawable/pin_dot_shape_4_avd.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/SystemUI/res/drawable/pin_dot_shape_5_avd.xml b/packages/SystemUI/res/drawable/pin_dot_shape_5_avd.xml new file mode 100644 index 0000000000000..de2a7db4ab0a9 --- /dev/null +++ b/packages/SystemUI/res/drawable/pin_dot_shape_5_avd.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/SystemUI/res/drawable/pin_dot_shape_6_avd.xml b/packages/SystemUI/res/drawable/pin_dot_shape_6_avd.xml new file mode 100644 index 0000000000000..e7ce4267fc323 --- /dev/null +++ b/packages/SystemUI/res/drawable/pin_dot_shape_6_avd.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/SystemUI/res/layout/keyguard_pin_shape_hinting_view.xml b/packages/SystemUI/res/layout/keyguard_pin_shape_hinting_view.xml new file mode 100644 index 0000000000000..aab987075bbd5 --- /dev/null +++ b/packages/SystemUI/res/layout/keyguard_pin_shape_hinting_view.xml @@ -0,0 +1,23 @@ + + + + diff --git a/packages/SystemUI/res/layout/keyguard_pin_shape_non_hinting_view.xml b/packages/SystemUI/res/layout/keyguard_pin_shape_non_hinting_view.xml new file mode 100644 index 0000000000000..cba1db0487d63 --- /dev/null +++ b/packages/SystemUI/res/layout/keyguard_pin_shape_non_hinting_view.xml @@ -0,0 +1,23 @@ + + + + \ No newline at end of file diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java index b143c5b90373f..48844db6f65fb 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java @@ -30,6 +30,7 @@ import com.android.keyguard.KeyguardSecurityModel.SecurityMode; import com.android.systemui.R; import com.android.systemui.classifier.FalsingCollector; import com.android.systemui.dagger.qualifiers.Main; +import com.android.systemui.flags.FeatureFlags; import com.android.systemui.statusbar.policy.DevicePostureController; import com.android.systemui.util.ViewController; import com.android.systemui.util.concurrency.DelayableExecutor; @@ -184,6 +185,7 @@ public abstract class KeyguardInputViewController private final FalsingCollector mFalsingCollector; private final DevicePostureController mDevicePostureController; private final KeyguardViewController mKeyguardViewController; + private final FeatureFlags mFeatureFlags; @Inject public Factory(KeyguardUpdateMonitor keyguardUpdateMonitor, @@ -195,7 +197,8 @@ public abstract class KeyguardInputViewController TelephonyManager telephonyManager, FalsingCollector falsingCollector, EmergencyButtonController.Factory emergencyButtonControllerFactory, DevicePostureController devicePostureController, - KeyguardViewController keyguardViewController) { + KeyguardViewController keyguardViewController, + FeatureFlags featureFlags) { mKeyguardUpdateMonitor = keyguardUpdateMonitor; mLockPatternUtils = lockPatternUtils; mLatencyTracker = latencyTracker; @@ -209,6 +212,7 @@ public abstract class KeyguardInputViewController mFalsingCollector = falsingCollector; mDevicePostureController = devicePostureController; mKeyguardViewController = keyguardViewController; + mFeatureFlags = featureFlags; } /** Create a new {@link KeyguardInputViewController}. */ @@ -236,7 +240,7 @@ public abstract class KeyguardInputViewController mKeyguardUpdateMonitor, securityMode, mLockPatternUtils, keyguardSecurityCallback, mMessageAreaControllerFactory, mLatencyTracker, mLiftToActivateListener, emergencyButtonController, mFalsingCollector, - mDevicePostureController); + mDevicePostureController, mFeatureFlags); } else if (keyguardInputView instanceof KeyguardSimPinView) { return new KeyguardSimPinViewController((KeyguardSimPinView) keyguardInputView, mKeyguardUpdateMonitor, securityMode, mLockPatternUtils, diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java index 35b2db27d8794..fd47e39534a7a 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java @@ -16,6 +16,8 @@ package com.android.keyguard; +import static com.android.systemui.keyguard.shared.constants.KeyguardBouncerConstants.DEFAULT_PIN_LENGTH; + import android.view.View; import com.android.internal.util.LatencyTracker; @@ -23,6 +25,8 @@ import com.android.internal.widget.LockPatternUtils; import com.android.keyguard.KeyguardSecurityModel.SecurityMode; import com.android.systemui.R; import com.android.systemui.classifier.FalsingCollector; +import com.android.systemui.flags.FeatureFlags; +import com.android.systemui.flags.Flags; import com.android.systemui.statusbar.policy.DevicePostureController; public class KeyguardPinViewController @@ -31,6 +35,14 @@ public class KeyguardPinViewController private final DevicePostureController mPostureController; private final DevicePostureController.Callback mPostureCallback = posture -> mView.onDevicePostureChanged(posture); + private LockPatternUtils mLockPatternUtils; + private final FeatureFlags mFeatureFlags; + private static final int DEFAULT_PIN_LENGTH = 6; + private NumPadButton mBackspaceKey; + private View mOkButton = mView.findViewById(R.id.key_enter); + + private int mUserId; + private long mPinLength; protected KeyguardPinViewController(KeyguardPINView view, KeyguardUpdateMonitor keyguardUpdateMonitor, @@ -40,12 +52,16 @@ public class KeyguardPinViewController LatencyTracker latencyTracker, LiftToActivateListener liftToActivateListener, EmergencyButtonController emergencyButtonController, FalsingCollector falsingCollector, - DevicePostureController postureController) { + DevicePostureController postureController, + FeatureFlags featureFlags) { super(view, keyguardUpdateMonitor, securityMode, lockPatternUtils, keyguardSecurityCallback, messageAreaControllerFactory, latencyTracker, liftToActivateListener, emergencyButtonController, falsingCollector); mKeyguardUpdateMonitor = keyguardUpdateMonitor; mPostureController = postureController; + mLockPatternUtils = lockPatternUtils; + mFeatureFlags = featureFlags; + mBackspaceKey = view.findViewById(R.id.delete_button); } @Override @@ -59,19 +75,76 @@ public class KeyguardPinViewController getKeyguardSecurityCallback().onCancelClicked(); }); } - + mPasswordEntry.setUserActivityListener(this::onUserInput); mPostureController.addCallback(mPostureCallback); } + protected void onUserInput() { + super.onUserInput(); + if (isAutoConfirmation()) { + updateBackSpaceVisibility(); + if (mPasswordEntry.getText().length() == mPinLength) { + verifyPasswordAndUnlock(); + } + } + } + @Override protected void onViewDetached() { super.onViewDetached(); mPostureController.removeCallback(mPostureCallback); } + @Override + public void startAppearAnimation() { + if (mFeatureFlags.isEnabled(Flags.AUTO_PIN_CONFIRMATION)) { + mUserId = KeyguardUpdateMonitor.getCurrentUser(); + mPinLength = mLockPatternUtils.getPinLength(mUserId); + mBackspaceKey.setTransparentMode(/* isTransparentMode= */ isAutoConfirmation()); + mOkButton.setVisibility(isAutoConfirmation() ? View.INVISIBLE : View.VISIBLE); + updateBackSpaceVisibility(); + mPasswordEntry.setUsePinShapes(true); + mPasswordEntry.setIsPinHinting(isAutoConfirmation() && isPinHinting()); + } + super.startAppearAnimation(); + } + @Override public boolean startDisappearAnimation(Runnable finishRunnable) { return mView.startDisappearAnimation( mKeyguardUpdateMonitor.needsSlowUnlockTransition(), finishRunnable); } + + // + + /** + * Updates the visibility and the enabled state of the backspace. + * Visibility changes are only for auto confirmation configuration. + */ + private void updateBackSpaceVisibility() { + if (!isAutoConfirmation()) { + return; + } + + if (mPasswordEntry.getText().length() > 0) { + mBackspaceKey.setVisibility(View.VISIBLE); + } else { + mBackspaceKey.setVisibility(View.INVISIBLE); + } + } + + /** + * Responsible for identifying if PIN hinting is to be enabled or not + */ + private boolean isPinHinting() { + return mLockPatternUtils.getPinLength(mUserId) == DEFAULT_PIN_LENGTH; + } + + /** + * Responsible for identifying if auto confirm is enabled or not in Settings + */ + private boolean isAutoConfirmation() { + //Checks if user has enabled the auto confirm in Settings + return mLockPatternUtils.isAutoPinConfirmEnabled(mUserId); + } } diff --git a/packages/SystemUI/src/com/android/keyguard/NumPadAnimator.java b/packages/SystemUI/src/com/android/keyguard/NumPadAnimator.java index 41111e3d3c6cf..5135eed519a9e 100644 --- a/packages/SystemUI/src/com/android/keyguard/NumPadAnimator.java +++ b/packages/SystemUI/src/com/android/keyguard/NumPadAnimator.java @@ -95,7 +95,6 @@ class NumPadAnimator { mHeight = height; mStartRadius = height / 2f; mEndRadius = height / 4f; - mBackground.setCornerRadius(mStartRadius); mExpandAnimator.setFloatValues(mStartRadius, mEndRadius); mContractAnimator.setFloatValues(mEndRadius, mStartRadius); } diff --git a/packages/SystemUI/src/com/android/keyguard/NumPadButton.java b/packages/SystemUI/src/com/android/keyguard/NumPadButton.java index 37060987cb21b..11c329e9811d3 100644 --- a/packages/SystemUI/src/com/android/keyguard/NumPadButton.java +++ b/packages/SystemUI/src/com/android/keyguard/NumPadButton.java @@ -27,6 +27,8 @@ import android.view.MotionEvent; import androidx.annotation.Nullable; +import com.android.systemui.R; + /** * Similar to the {@link NumPadKey}, but displays an image. */ @@ -35,18 +37,13 @@ public class NumPadButton extends AlphaOptimizedImageButton implements NumPadAni @Nullable private NumPadAnimator mAnimator; private int mOrientation; + private int mStyleAttr; + private boolean mIsTransparentMode; public NumPadButton(Context context, AttributeSet attrs) { super(context, attrs); - - Drawable background = getBackground(); - if (background instanceof GradientDrawable) { - mAnimator = new NumPadAnimator(context, background.mutate(), - attrs.getStyleAttribute(), getDrawable()); - } else { - mAnimator = null; - } - + mStyleAttr = attrs.getStyleAttribute(); + setupAnimator(); } @Override @@ -98,7 +95,9 @@ public class NumPadButton extends AlphaOptimizedImageButton implements NumPadAni public void reloadColors() { if (mAnimator != null) mAnimator.reloadColors(getContext()); - int[] customAttrs = {android.R.attr.textColorPrimaryInverse}; + int textColorResId = mIsTransparentMode ? android.R.attr.textColorPrimary + : android.R.attr.textColorPrimaryInverse; + int[] customAttrs = {textColorResId}; TypedArray a = getContext().obtainStyledAttributes(customAttrs); int imageColor = a.getColor(0, 0); a.recycle(); @@ -111,4 +110,34 @@ public class NumPadButton extends AlphaOptimizedImageButton implements NumPadAni mAnimator.setProgress(progress); } } + + /** + * Set whether button is transparent mode. + * + * @param isTransparentMode + */ + public void setTransparentMode(boolean isTransparentMode) { + mIsTransparentMode = isTransparentMode; + if (isTransparentMode) { + setBackgroundColor(android.R.color.transparent); + } else { + setBackground(getContext().getDrawable(R.drawable.num_pad_key_background)); + } + setupAnimator(); + reloadColors(); + requestLayout(); + } + + /** + * Set up the animator for the NumPadButton. + */ + private void setupAnimator() { + Drawable background = getBackground(); + if (background instanceof GradientDrawable) { + mAnimator = new NumPadAnimator(getContext(), background.mutate(), + mStyleAttr, getDrawable()); + } else { + mAnimator = null; + } + } } diff --git a/packages/SystemUI/src/com/android/keyguard/PasswordTextView.java b/packages/SystemUI/src/com/android/keyguard/PasswordTextView.java index 35cae099e44e9..8554e11a2f3cf 100644 --- a/packages/SystemUI/src/com/android/keyguard/PasswordTextView.java +++ b/packages/SystemUI/src/com/android/keyguard/PasswordTextView.java @@ -35,13 +35,14 @@ import android.text.InputType; import android.text.TextUtils; import android.util.AttributeSet; import android.view.Gravity; -import android.view.View; +import android.view.LayoutInflater; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityManager; import android.view.accessibility.AccessibilityNodeInfo; import android.view.animation.AnimationUtils; import android.view.animation.Interpolator; import android.widget.EditText; +import android.widget.FrameLayout; import com.android.settingslib.Utils; import com.android.systemui.R; @@ -52,12 +53,12 @@ import java.util.ArrayList; * A View similar to a textView which contains password text and can animate when the text is * changed */ -public class PasswordTextView extends View { +public class PasswordTextView extends FrameLayout { private static final float DOT_OVERSHOOT_FACTOR = 1.5f; private static final long DOT_APPEAR_DURATION_OVERSHOOT = 320; - private static final long APPEAR_DURATION = 160; - private static final long DISAPPEAR_DURATION = 160; + public static final long APPEAR_DURATION = 160; + public static final long DISAPPEAR_DURATION = 160; private static final long RESET_DELAY_PER_ELEMENT = 40; private static final long RESET_MAX_DELAY = 200; @@ -95,11 +96,14 @@ public class PasswordTextView extends View { private PowerManager mPM; private int mCharPadding; private final Paint mDrawPaint = new Paint(); + private int mDrawColor; private Interpolator mAppearInterpolator; private Interpolator mDisappearInterpolator; private Interpolator mFastOutSlowInInterpolator; private boolean mShowPassword; private UserActivityListener mUserActivityListener; + private PinShapeInput mPinShapeInput; + private boolean mUsePinShapes = false; public interface UserActivityListener { void onUserActivity(); @@ -141,8 +145,10 @@ public class PasswordTextView extends View { mCharPadding = a.getDimensionPixelSize(R.styleable.PasswordTextView_charPadding, getContext().getResources().getDimensionPixelSize( R.dimen.password_char_padding)); - mDrawPaint.setColor(a.getColor(R.styleable.PasswordTextView_android_textColor, - Color.WHITE)); + mDrawColor = a.getColor(R.styleable.PasswordTextView_android_textColor, + Color.WHITE); + mDrawPaint.setColor(mDrawColor); + } finally { a.recycle(); } @@ -161,6 +167,7 @@ public class PasswordTextView extends View { mFastOutSlowInInterpolator = AnimationUtils.loadInterpolator(mContext, android.R.interpolator.fast_out_slow_in); mPM = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE); + setWillNotDraw(false); } @Override @@ -171,6 +178,12 @@ public class PasswordTextView extends View { @Override protected void onDraw(Canvas canvas) { + // Do not use legacy draw animations for pin shapes. + if (mUsePinShapes) { + super.onDraw(canvas); + return; + } + float totalDrawingWidth = getDrawingWidth(); float currentDrawPosition; if ((mGravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.LEFT) { @@ -205,9 +218,12 @@ public class PasswordTextView extends View { * Reload colors from resources. **/ public void reloadColors() { - int textColor = Utils.getColorAttr(getContext(), android.R.attr.textColorPrimary) - .getDefaultColor(); - mDrawPaint.setColor(textColor); + mDrawColor = Utils.getColorAttr(getContext(), + android.R.attr.textColorPrimary).getDefaultColor(); + mDrawPaint.setColor(mDrawColor); + if (mPinShapeInput != null) { + mPinShapeInput.setDrawColor(mDrawColor); + } } @Override @@ -252,6 +268,9 @@ public class PasswordTextView extends View { charState = mTextChars.get(newLength - 1); charState.whichChar = c; } + if (mPinShapeInput != null) { + mPinShapeInput.append(); + } charState.startAppearAnimation(); // ensure that the previous element is being swapped @@ -265,8 +284,8 @@ public class PasswordTextView extends View { sendAccessibilityEventTypeViewTextChanged(textbefore, textbefore.length(), 0, 1); } - public void setUserActivityListener(UserActivityListener userActivitiListener) { - mUserActivityListener = userActivitiListener; + public void setUserActivityListener(UserActivityListener userActivityListener) { + mUserActivityListener = userActivityListener; } private void userActivity() { @@ -284,6 +303,9 @@ public class PasswordTextView extends View { CharState charState = mTextChars.get(length - 1); charState.startRemoveAnimation(0, 0); sendAccessibilityEventTypeViewTextChanged(textbefore, textbefore.length() - 1, 1, 0); + if (mPinShapeInput != null) { + mPinShapeInput.delete(); + } } userActivity(); } @@ -339,6 +361,11 @@ public class PasswordTextView extends View { } if (!animated) { mTextChars.clear(); + } else { + userActivity(); + } + if (mPinShapeInput != null) { + mPinShapeInput.reset(); } if (announce) { sendAccessibilityEventTypeViewTextChanged(textbefore, 0, textbefore.length(), 0); @@ -385,6 +412,35 @@ public class PasswordTextView extends View { info.setInputType(InputType.TYPE_NUMBER_VARIATION_PASSWORD); } + /** + * Sets whether to use pin shapes. + */ + public void setUsePinShapes(boolean usePinShapes) { + mUsePinShapes = usePinShapes; + } + + /** + * Determines whether AutoConfirmation feature is on. + * + * @param usePinShapes + * @param isPinHinting + */ + public void setIsPinHinting(boolean isPinHinting) { + if (mPinShapeInput != null) { + removeView(mPinShapeInput.getView()); + mPinShapeInput = null; + } + + if (isPinHinting) { + mPinShapeInput = (PinShapeInput) LayoutInflater.from(mContext).inflate( + R.layout.keyguard_pin_shape_hinting_view, null); + } else { + mPinShapeInput = (PinShapeInput) LayoutInflater.from(mContext).inflate( + R.layout.keyguard_pin_shape_non_hinting_view, null); + } + addView(mPinShapeInput.getView()); + } + private class CharState { char whichChar; ValueAnimator textAnimator; diff --git a/packages/SystemUI/src/com/android/keyguard/PinShapeAdapter.kt b/packages/SystemUI/src/com/android/keyguard/PinShapeAdapter.kt new file mode 100644 index 0000000000000..4496dc311e92b --- /dev/null +++ b/packages/SystemUI/src/com/android/keyguard/PinShapeAdapter.kt @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2023 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.content.Context +import com.android.systemui.R +import kotlin.random.Random + +class PinShapeAdapter { + var shapes: MutableList = ArrayList() + val random = Random(System.currentTimeMillis()) + + constructor(context: Context) { + val availableShapes = context.resources.obtainTypedArray(R.array.bouncer_pin_shapes) + + for (i in 0 until availableShapes.length()) { + val shape = availableShapes.getResourceId(i, 0) + shapes.add(shape) + } + + shapes.shuffle() + availableShapes.recycle() + } + + fun getShape(pos: Int): Int { + return shapes[pos.mod(shapes.size)] + } +} diff --git a/packages/SystemUI/src/com/android/keyguard/PinShapeHintingView.java b/packages/SystemUI/src/com/android/keyguard/PinShapeHintingView.java new file mode 100644 index 0000000000000..cf9d0537defeb --- /dev/null +++ b/packages/SystemUI/src/com/android/keyguard/PinShapeHintingView.java @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2023 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.content.Context; +import android.graphics.drawable.AnimatedVectorDrawable; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.view.View; +import android.widget.ImageView; +import android.widget.LinearLayout; + +import androidx.core.graphics.drawable.DrawableCompat; + +import com.android.settingslib.Utils; +import com.android.systemui.R; + +/** + * This class contains implementation for methods that will be used when user has set a + * six digit pin on their device + */ +public class PinShapeHintingView extends LinearLayout implements PinShapeInput { + + private int mPinLength; + private int mDotDiameter; + private int mDotSpacing; + private int mColor = Utils.getColorAttr(getContext(), android.R.attr.textColorPrimary) + .getDefaultColor(); + private int mPosition = 0; + private static final int DEFAULT_PIN_LENGTH = 6; + private PinShapeAdapter mPinShapeAdapter; + + public PinShapeHintingView(Context context, AttributeSet attrs) { + super(context, attrs); + mPinShapeAdapter = new PinShapeAdapter(context); + mPinLength = DEFAULT_PIN_LENGTH; + mDotDiameter = context.getResources().getDimensionPixelSize(R.dimen.default_dot_diameter); + mDotSpacing = context.getResources().getDimensionPixelSize(R.dimen.default_dot_spacing); + + for (int i = 0; i < mPinLength; i++) { + ImageView pinDot = new ImageView(context, attrs); + LayoutParams layoutParams = new LayoutParams(mDotDiameter, mDotDiameter); + pinDot.setLayoutParams(layoutParams); + pinDot.setImageResource(R.drawable.pin_dot_avd); + if (pinDot.getDrawable() != null) { + Drawable drawable = DrawableCompat.wrap(pinDot.getDrawable()); + DrawableCompat.setTint(drawable, mColor); + } + addView(pinDot); + } + } + + @Override + public void append() { + if (mPosition == DEFAULT_PIN_LENGTH) { + return; + } + setAnimatedDrawable(mPosition, mPinShapeAdapter.getShape(mPosition)); + mPosition++; + } + + @Override + public void delete() { + if (mPosition == 0) { + return; + } + mPosition--; + setAnimatedDrawable(mPosition, R.drawable.pin_dot_delete_avd); + } + + @Override + public void setDrawColor(int color) { + this.mColor = color; + } + + @Override + public void reset() { + int size = mPosition; + for (int i = 0; i < size; i++) { + delete(); + } + mPosition = 0; + } + + @Override + public View getView() { + return this; + } + + private void setAnimatedDrawable(int position, int drawableResId) { + ImageView pinDot = (ImageView) getChildAt(position); + pinDot.setImageResource(drawableResId); + if (pinDot.getDrawable() != null) { + Drawable drawable = DrawableCompat.wrap(pinDot.getDrawable()); + DrawableCompat.setTint(drawable, mColor); + } + if (pinDot.getDrawable() instanceof AnimatedVectorDrawable) { + ((AnimatedVectorDrawable) pinDot.getDrawable()).start(); + } + } +} diff --git a/packages/SystemUI/src/com/android/keyguard/PinShapeInput.java b/packages/SystemUI/src/com/android/keyguard/PinShapeInput.java new file mode 100644 index 0000000000000..52ae6bac6d744 --- /dev/null +++ b/packages/SystemUI/src/com/android/keyguard/PinShapeInput.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2023 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.view.View; + +/** + * A common interface for classes that provide functionality for the PIN type view + */ +public interface PinShapeInput { + + /** + * This is the method that is triggered when user types in a character + */ + void append(); + + /** + * This is the method that is triggered when user deletes a character + */ + void delete(); + + /** + * This is the method that is triggered for setting the color of the view + */ + void setDrawColor(int color); + + /** + * This is the method that is triggered for resetting the view + */ + void reset(); + + /** + * This is the method that is triggered for getting the view + */ + View getView(); +} diff --git a/packages/SystemUI/src/com/android/keyguard/PinShapeNonHintingView.java b/packages/SystemUI/src/com/android/keyguard/PinShapeNonHintingView.java new file mode 100644 index 0000000000000..6a6e81e9cb463 --- /dev/null +++ b/packages/SystemUI/src/com/android/keyguard/PinShapeNonHintingView.java @@ -0,0 +1,183 @@ +/* + * Copyright (C) 2023 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.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ValueAnimator; +import android.content.Context; +import android.graphics.Rect; +import android.graphics.drawable.AnimatedVectorDrawable; +import android.graphics.drawable.Drawable; +import android.transition.Transition; +import android.transition.TransitionManager; +import android.transition.TransitionValues; +import android.util.AttributeSet; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; +import android.view.animation.Animation; +import android.widget.ImageView; +import android.widget.LinearLayout; + +import androidx.core.graphics.drawable.DrawableCompat; + +import com.android.settingslib.Utils; +import com.android.systemui.R; +import com.android.systemui.animation.Interpolators; + +/** + * This class contains implementation for methods that will be used when user has set a + * non six digit pin on their device + */ +public class PinShapeNonHintingView extends LinearLayout implements PinShapeInput { + + private int mColor = Utils.getColorAttr(getContext(), + android.R.attr.textColorPrimary).getDefaultColor(); + private int mPosition = 0; + private final PinShapeAdapter mPinShapeAdapter; + private Animation mCurrentPlayingAnimation; + public PinShapeNonHintingView(Context context, AttributeSet attrs) { + super(context, attrs); + mPinShapeAdapter = new PinShapeAdapter(context); + } + + @Override + public void append() { + int size = getResources().getDimensionPixelSize(R.dimen.password_shape_size); + ImageView pinDot = new ImageView(getContext()); + pinDot.setLayoutParams(new LayoutParams(size, size)); + pinDot.setImageResource(mPinShapeAdapter.getShape(mPosition)); + if (pinDot.getDrawable() != null) { + Drawable wrappedDrawable = DrawableCompat.wrap(pinDot.getDrawable()); + DrawableCompat.setTint(wrappedDrawable, mColor); + } + if (pinDot.getDrawable() instanceof AnimatedVectorDrawable) { + ((AnimatedVectorDrawable) pinDot.getDrawable()).start(); + } + TransitionManager.beginDelayedTransition(this, new PinShapeViewTransition()); + addView(pinDot); + mPosition++; + } + + @Override + public void delete() { + if (mPosition == 0) { + Log.e(getClass().getName(), "Trying to delete a non-existent char"); + return; + } + mPosition--; + ImageView pinDot = (ImageView) getChildAt(mPosition); + ValueAnimator animator = ValueAnimator.ofFloat(1f, 0f); + animator.addUpdateListener(valueAnimator -> { + float value = (float) valueAnimator.getAnimatedValue(); + pinDot.setScaleX(value); + pinDot.setScaleY(value); + }); + animator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + super.onAnimationEnd(animation); + TransitionManager.beginDelayedTransition( + PinShapeNonHintingView.this, + new PinShapeViewTransition()); + removeView(pinDot); + mCurrentPlayingAnimation = null; + } + }); + animator.setDuration(PasswordTextView.DISAPPEAR_DURATION); + animator.start(); + } + + @Override + public void setDrawColor(int color) { + this.mColor = color; + } + + @Override + public void reset() { + final int position = mPosition; + for (int i = 0; i < position; i++) { + delete(); + } + } + + @Override + public View getView() { + return this; + } + + class PinShapeViewTransition extends Transition { + private static final String PROP_BOUNDS = "PinShapeViewTransition:bounds"; + + @Override + public void captureEndValues(TransitionValues transitionValues) { + if (transitionValues != null) { + captureValues(transitionValues); + } + } + + @Override + public void captureStartValues(TransitionValues transitionValues) { + if (transitionValues != null) { + captureValues(transitionValues); + } + } + + private void captureValues(TransitionValues values) { + Rect boundsRect = new Rect(); + boundsRect.left = values.view.getLeft(); + boundsRect.top = values.view.getTop(); + boundsRect.right = values.view.getRight(); + boundsRect.bottom = values.view.getBottom(); + values.values.put(PROP_BOUNDS, boundsRect); + } + + @Override + public String[] getTransitionProperties() { + return new String[] { PROP_BOUNDS }; + } + + @Override + public Animator createAnimator(ViewGroup sceneRoot, TransitionValues startValues, + TransitionValues endValues) { + if (sceneRoot == null || startValues == null || endValues == null) { + return null; + } + + Rect startRect = (Rect) startValues.values.get(PROP_BOUNDS); + Rect endRect = (Rect) endValues.values.get(PROP_BOUNDS); + View v = startValues.view; + ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f); + animator.setDuration(PasswordTextView.APPEAR_DURATION); + animator.setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN); + animator.addUpdateListener(valueAnimator -> { + float value = (float) valueAnimator.getAnimatedValue(); + int diff = startRect.left - endRect.left; + int currentTranslation = (int) ((diff) * value); + v.setLeftTopRightBottom( + startRect.left - currentTranslation, + startRect.top, + startRect.right - currentTranslation, + startRect.bottom + ); + }); + animator.start(); + return animator; + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/constants/KeyguardBouncerConstants.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/constants/KeyguardBouncerConstants.kt index 8222dd54f46d6..3b3ec39d0209f 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/shared/constants/KeyguardBouncerConstants.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/constants/KeyguardBouncerConstants.kt @@ -26,4 +26,10 @@ object KeyguardBouncerConstants { const val EXPANSION_HIDDEN = 1f const val EXPANSION_VISIBLE = 0f const val ALPHA_EXPANSION_THRESHOLD = 0.95f + + /** + * This value is used for denoting the PIN length at which we want to layout the view in which + * PIN hinting is enabled + */ + const val DEFAULT_PIN_LENGTH = 6 } diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt index cdb7bbb9f8233..a1af8e8fac9c0 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt @@ -27,11 +27,14 @@ import com.android.systemui.R import com.android.systemui.SysuiTestCase import com.android.systemui.classifier.FalsingCollector import com.android.systemui.classifier.FalsingCollectorFake +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags import com.android.systemui.statusbar.policy.DevicePostureController import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentMatchers.anyBoolean +import org.mockito.ArgumentMatchers.anyInt import org.mockito.ArgumentMatchers.anyString import org.mockito.Mock import org.mockito.Mockito @@ -71,6 +74,11 @@ class KeyguardPinViewControllerTest : SysuiTestCase() { private val falsingCollector: FalsingCollector = FalsingCollectorFake() @Mock lateinit var postureController: DevicePostureController + @Mock lateinit var featureFlags: FeatureFlags + @Mock lateinit var passwordTextView: PasswordTextView + @Mock lateinit var deleteButton: NumPadButton + @Mock lateinit var enterButton: View + lateinit var pinViewController: KeyguardPinViewController @Before @@ -82,7 +90,13 @@ class KeyguardPinViewControllerTest : SysuiTestCase() { keyguardMessageAreaControllerFactory.create(any(KeyguardMessageArea::class.java)) ) .thenReturn(keyguardMessageAreaController) + `when`(keyguardPinView.passwordTextViewId).thenReturn(R.id.pinEntry) + `when`(keyguardPinView.findViewById(R.id.pinEntry)) + .thenReturn(passwordTextView) `when`(keyguardPinView.resources).thenReturn(context.resources) + `when`(keyguardPinView.findViewById(R.id.delete_button)) + .thenReturn(deleteButton) + `when`(keyguardPinView.findViewById(R.id.key_enter)).thenReturn(enterButton) pinViewController = KeyguardPinViewController( keyguardPinView, @@ -95,7 +109,8 @@ class KeyguardPinViewControllerTest : SysuiTestCase() { liftToActivateListener, mEmergencyButtonController, falsingCollector, - postureController + postureController, + featureFlags ) } @@ -112,4 +127,18 @@ class KeyguardPinViewControllerTest : SysuiTestCase() { pinViewController.startAppearAnimation() verify(keyguardMessageAreaController, Mockito.never()).setMessage(anyString(), anyBoolean()) } + + @Test + fun startAppearAnimation_withAutoPinConfirmation() { + `when`(featureFlags.isEnabled(Flags.AUTO_PIN_CONFIRMATION)).thenReturn(true) + `when`(lockPatternUtils.getPinLength(anyInt())).thenReturn(6) + `when`(lockPatternUtils.isAutoPinConfirmEnabled(anyInt())).thenReturn(true) + `when`(passwordTextView.text).thenReturn("") + + pinViewController.startAppearAnimation() + verify(deleteButton).visibility = View.INVISIBLE + verify(enterButton).visibility = View.INVISIBLE + verify(passwordTextView).setUsePinShapes(true) + verify(passwordTextView).setIsPinHinting(true) + } } diff --git a/packages/SystemUI/tests/src/com/android/keyguard/PinShapeHintingViewTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/PinShapeHintingViewTest.kt new file mode 100644 index 0000000000000..42e12df431c55 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/keyguard/PinShapeHintingViewTest.kt @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2023 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.testing.AndroidTestingRunner +import android.testing.TestableLooper +import android.view.LayoutInflater +import androidx.test.filters.SmallTest +import com.android.systemui.R +import com.android.systemui.SysuiTestCase +import com.google.common.truth.Truth +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidTestingRunner::class) +@TestableLooper.RunWithLooper +class PinShapeHintingViewTest : SysuiTestCase() { + lateinit var underTest: PinShapeHintingView + + @Before + fun setup() { + underTest = + LayoutInflater.from(context).inflate(R.layout.keyguard_pin_shape_hinting_view, null) + as PinShapeHintingView + } + + @Test + fun testAppend() { + // Add more when animation part is complete + underTest.append() + Truth.assertThat(underTest.childCount).isEqualTo(6) + } + + @Test + fun testDelete() { + underTest.delete() + Truth.assertThat(underTest.childCount).isEqualTo(6) + } + + @Test + fun testReset() { + for (i in 0 until 3) { + underTest.append() + } + underTest.reset() + Truth.assertThat(underTest.childCount).isEqualTo(6) + } +} diff --git a/packages/SystemUI/tests/src/com/android/keyguard/PinShapeNonHintingViewTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/PinShapeNonHintingViewTest.kt new file mode 100644 index 0000000000000..c04fd39168de9 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/keyguard/PinShapeNonHintingViewTest.kt @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2023 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.testing.AndroidTestingRunner +import android.testing.TestableLooper +import android.view.LayoutInflater +import androidx.test.filters.SmallTest +import com.android.systemui.R +import com.android.systemui.SysuiTestCase +import com.google.common.truth.Truth +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidTestingRunner::class) +@TestableLooper.RunWithLooper +class PinShapeNonHintingViewTest : SysuiTestCase() { + lateinit var underTest: PinShapeNonHintingView + + @Before + fun setup() { + underTest = + LayoutInflater.from(context).inflate(R.layout.keyguard_pin_shape_non_hinting_view, null) + as PinShapeNonHintingView + } + + @Test + fun testAppend() { + // Add more when animation part is complete + underTest.append() + Truth.assertThat(underTest.childCount).isEqualTo(1) + } + + @Test + fun testDelete() { + for (i in 0 until 3) { + underTest.append() + } + underTest.delete() + + underTest.postDelayed( + { Truth.assertThat(underTest.childCount).isEqualTo(2) }, + PasswordTextView.DISAPPEAR_DURATION + 100L + ) + } + + @Test + fun testReset() { + for (i in 0 until 3) { + underTest.append() + } + underTest.reset() + underTest.postDelayed( + { Truth.assertThat(underTest.childCount).isEqualTo(0) }, + PasswordTextView.DISAPPEAR_DURATION + 100L + ) + } +}