1 /* 2 * Copyright (C) 2012 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.keyguard; 18 19 import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; 20 import static android.content.res.Configuration.ORIENTATION_PORTRAIT; 21 22 import static com.android.internal.jank.InteractionJankMonitor.CUJ_LOCKSCREEN_PIN_APPEAR; 23 import static com.android.internal.jank.InteractionJankMonitor.CUJ_LOCKSCREEN_PIN_DISAPPEAR; 24 import static com.android.systemui.statusbar.policy.DevicePostureController.DEVICE_POSTURE_CLOSED; 25 import static com.android.systemui.statusbar.policy.DevicePostureController.DEVICE_POSTURE_HALF_OPENED; 26 import static com.android.systemui.statusbar.policy.DevicePostureController.DEVICE_POSTURE_UNKNOWN; 27 28 import android.animation.ValueAnimator; 29 import android.annotation.Nullable; 30 import android.content.Context; 31 import android.content.res.Configuration; 32 import android.util.AttributeSet; 33 import android.util.MathUtils; 34 import android.view.View; 35 import android.view.animation.AnimationUtils; 36 import android.view.animation.Interpolator; 37 38 import androidx.constraintlayout.motion.widget.MotionLayout; 39 import androidx.constraintlayout.widget.ConstraintLayout; 40 import androidx.constraintlayout.widget.ConstraintSet; 41 42 import com.android.app.animation.Interpolators; 43 import com.android.settingslib.animation.DisappearAnimationUtils; 44 import com.android.systemui.res.R; 45 import com.android.systemui.statusbar.policy.DevicePostureController.DevicePostureInt; 46 47 /** 48 * Displays a PIN pad for unlocking. 49 */ 50 public class KeyguardPINView extends KeyguardPinBasedInputView { 51 52 ValueAnimator mAppearAnimator = ValueAnimator.ofFloat(0f, 1f); 53 private final DisappearAnimationUtils mDisappearAnimationUtils; 54 private final DisappearAnimationUtils mDisappearAnimationUtilsLocked; 55 @Nullable private MotionLayout mContainerMotionLayout; 56 // TODO (b/293252410) - usage of mContainerConstraintLayout should be removed 57 // when the flag is enabled/removed 58 @Nullable private ConstraintLayout mContainerConstraintLayout; 59 private int mDisappearYTranslation; 60 private View[][] mViews; 61 private int mYTrans; 62 private int mYTransOffset; 63 private View mBouncerMessageArea; 64 private boolean mAlreadyUsingSplitBouncer = false; 65 private boolean mIsSmallLockScreenLandscapeEnabled = false; 66 @DevicePostureInt private int mLastDevicePosture = DEVICE_POSTURE_UNKNOWN; 67 public static final long ANIMATION_DURATION = 650; 68 KeyguardPINView(Context context)69 public KeyguardPINView(Context context) { 70 this(context, null); 71 } 72 KeyguardPINView(Context context, AttributeSet attrs)73 public KeyguardPINView(Context context, AttributeSet attrs) { 74 super(context, attrs); 75 mDisappearAnimationUtils = new DisappearAnimationUtils(context, 76 125, 0.6f /* translationScale */, 77 0.45f /* delayScale */, AnimationUtils.loadInterpolator( 78 mContext, android.R.interpolator.fast_out_linear_in)); 79 mDisappearAnimationUtilsLocked = new DisappearAnimationUtils(context, 80 (long) (125 * KeyguardPatternView.DISAPPEAR_MULTIPLIER_LOCKED), 81 0.6f /* translationScale */, 82 0.45f /* delayScale */, AnimationUtils.loadInterpolator( 83 mContext, android.R.interpolator.fast_out_linear_in)); 84 mDisappearYTranslation = getResources().getDimensionPixelSize( 85 R.dimen.disappear_y_translation); 86 mYTrans = getResources().getDimensionPixelSize(R.dimen.pin_view_trans_y_entry); 87 mYTransOffset = getResources().getDimensionPixelSize(R.dimen.pin_view_trans_y_entry_offset); 88 } 89 90 /** Use motion layout (new bouncer implementation) if LOCKSCREEN_ENABLE_LANDSCAPE flag is 91 * enabled, instead of constraint layout (old bouncer implementation) */ setIsLockScreenLandscapeEnabled(boolean isLockScreenLandscapeEnabled)92 public void setIsLockScreenLandscapeEnabled(boolean isLockScreenLandscapeEnabled) { 93 mIsSmallLockScreenLandscapeEnabled = isLockScreenLandscapeEnabled; 94 findContainerLayout(); 95 } 96 findContainerLayout()97 private void findContainerLayout() { 98 if (mIsSmallLockScreenLandscapeEnabled) { 99 mContainerMotionLayout = findViewById(R.id.pin_container); 100 } else { 101 mContainerConstraintLayout = findViewById(R.id.pin_container); 102 } 103 } 104 105 106 @Override onConfigurationChanged(Configuration newConfig)107 protected void onConfigurationChanged(Configuration newConfig) { 108 updateMargins(); 109 } 110 onDevicePostureChanged(@evicePostureInt int posture)111 void onDevicePostureChanged(@DevicePostureInt int posture) { 112 if (mLastDevicePosture == posture) return; 113 mLastDevicePosture = posture; 114 115 if (mIsSmallLockScreenLandscapeEnabled) { 116 boolean useSplitBouncerAfterFold = 117 mLastDevicePosture == DEVICE_POSTURE_CLOSED 118 && getResources().getConfiguration().orientation == ORIENTATION_LANDSCAPE 119 && getResources().getBoolean(R.bool.update_bouncer_constraints); 120 121 if (mAlreadyUsingSplitBouncer != useSplitBouncerAfterFold) { 122 updateConstraints(useSplitBouncerAfterFold); 123 } 124 } 125 126 updateMargins(); 127 } 128 129 @Override resetState()130 protected void resetState() { 131 } 132 133 @Override getPasswordTextViewId()134 protected int getPasswordTextViewId() { 135 return R.id.pinEntry; 136 } 137 updateMargins()138 private void updateMargins() { 139 // Re-apply everything to the keys... 140 int bottomMargin = mContext.getResources().getDimensionPixelSize( 141 R.dimen.num_pad_entry_row_margin_bottom); 142 int rightMargin = mContext.getResources().getDimensionPixelSize( 143 R.dimen.num_pad_key_margin_end); 144 String ratio = mContext.getResources().getString(R.string.num_pad_key_ratio); 145 146 // mView contains all Views that make up the PIN pad; row0 = the entry test field, then 147 // rows 1-4 contain the buttons. Iterate over all views that make up the buttons in the pad, 148 // and re-set all the margins. 149 for (int row = 1; row < 5; row++) { 150 for (int column = 0; column < 3; column++) { 151 View key = mViews[row][column]; 152 153 ConstraintLayout.LayoutParams lp = 154 (ConstraintLayout.LayoutParams) key.getLayoutParams(); 155 156 lp.dimensionRatio = ratio; 157 158 // Don't set any margins on the last row of buttons. 159 if (row != 4) { 160 lp.bottomMargin = bottomMargin; 161 } 162 163 // Don't set margins on the rightmost buttons. 164 if (column != 2) { 165 lp.rightMargin = rightMargin; 166 } 167 168 key.setLayoutParams(lp); 169 } 170 } 171 172 if (mIsSmallLockScreenLandscapeEnabled) { 173 updateHalfFoldedConstraints(); 174 } else { 175 updateHalfFoldedGuideline(); 176 } 177 } 178 updateHalfFoldedConstraints()179 private void updateHalfFoldedConstraints() { 180 // Update the constraints based on the device posture... 181 if (mAlreadyUsingSplitBouncer) return; 182 183 boolean shouldCollapsePin = 184 mLastDevicePosture == DEVICE_POSTURE_HALF_OPENED 185 && mContext.getResources().getConfiguration().orientation 186 == ORIENTATION_PORTRAIT; 187 188 int expectedMotionLayoutState = shouldCollapsePin 189 ? R.id.half_folded_single_constraints 190 : R.id.single_constraints; 191 192 transitionToMotionLayoutState(expectedMotionLayoutState); 193 } 194 195 // TODO (b/293252410) - this method can be removed when the flag is enabled/removed updateHalfFoldedGuideline()196 private void updateHalfFoldedGuideline() { 197 // Update the guideline based on the device posture... 198 float halfOpenPercentage = 199 mContext.getResources().getFloat(R.dimen.half_opened_bouncer_height_ratio); 200 201 ConstraintSet cs = new ConstraintSet(); 202 cs.clone(mContainerConstraintLayout); 203 cs.setGuidelinePercent(R.id.pin_pad_top_guideline, 204 mLastDevicePosture == DEVICE_POSTURE_HALF_OPENED ? halfOpenPercentage : 0.0f); 205 cs.applyTo(mContainerConstraintLayout); 206 } 207 transitionToMotionLayoutState(int state)208 private void transitionToMotionLayoutState(int state) { 209 if (mContainerMotionLayout.getCurrentState() != state) { 210 mContainerMotionLayout.transitionToState(state); 211 } 212 } 213 214 /** Updates the keyguard view's constraints (single or split constraints). 215 * Split constraints are only used for small landscape screens. 216 * Only called when flag LANDSCAPE_ENABLE_LOCKSCREEN is enabled. */ 217 @Override updateConstraints(boolean useSplitBouncer)218 protected void updateConstraints(boolean useSplitBouncer) { 219 if (!mIsSmallLockScreenLandscapeEnabled) return; 220 221 mAlreadyUsingSplitBouncer = useSplitBouncer; 222 223 if (useSplitBouncer) { 224 mContainerMotionLayout.jumpToState(R.id.split_constraints); 225 mContainerMotionLayout.setMaxWidth(Integer.MAX_VALUE); 226 } else { 227 boolean useHalfFoldedConstraints = 228 mLastDevicePosture == DEVICE_POSTURE_HALF_OPENED 229 && mContext.getResources().getConfiguration().orientation 230 == ORIENTATION_PORTRAIT; 231 232 if (useHalfFoldedConstraints) { 233 mContainerMotionLayout.jumpToState(R.id.half_folded_single_constraints); 234 } else { 235 mContainerMotionLayout.jumpToState(R.id.single_constraints); 236 } 237 mContainerMotionLayout.setMaxWidth(getResources() 238 .getDimensionPixelSize(R.dimen.keyguard_security_width)); 239 } 240 } 241 242 @Override onFinishInflate()243 protected void onFinishInflate() { 244 super.onFinishInflate(); 245 246 mBouncerMessageArea = findViewById(R.id.bouncer_message_area); 247 mViews = new View[][]{ 248 new View[]{ 249 findViewById(R.id.row0), null, null 250 }, 251 new View[]{ 252 findViewById(R.id.key1), findViewById(R.id.key2), 253 findViewById(R.id.key3) 254 }, 255 new View[]{ 256 findViewById(R.id.key4), findViewById(R.id.key5), 257 findViewById(R.id.key6) 258 }, 259 new View[]{ 260 findViewById(R.id.key7), findViewById(R.id.key8), 261 findViewById(R.id.key9) 262 }, 263 new View[]{ 264 findViewById(R.id.delete_button), findViewById(R.id.key0), 265 findViewById(R.id.key_enter) 266 }, 267 new View[]{ 268 null, mEcaView, null 269 }}; 270 } 271 272 @Override getWrongPasswordStringId()273 public int getWrongPasswordStringId() { 274 return R.string.kg_wrong_pin; 275 } 276 277 @Override startAppearAnimation()278 public void startAppearAnimation() { 279 setAlpha(1f); 280 setTranslationY(0); 281 if (mAppearAnimator.isRunning()) { 282 mAppearAnimator.cancel(); 283 } 284 mAppearAnimator.setDuration(ANIMATION_DURATION); 285 mAppearAnimator.addUpdateListener(animation -> animate(animation.getAnimatedFraction())); 286 mAppearAnimator.addListener(getAnimationListener(CUJ_LOCKSCREEN_PIN_APPEAR)); 287 mAppearAnimator.start(); 288 } 289 startDisappearAnimation(boolean needsSlowUnlockTransition, final Runnable finishRunnable)290 public boolean startDisappearAnimation(boolean needsSlowUnlockTransition, 291 final Runnable finishRunnable) { 292 if (mAppearAnimator.isRunning()) { 293 mAppearAnimator.cancel(); 294 } 295 296 setTranslationY(0); 297 DisappearAnimationUtils disappearAnimationUtils = needsSlowUnlockTransition 298 ? mDisappearAnimationUtilsLocked 299 : mDisappearAnimationUtils; 300 disappearAnimationUtils.createAnimation( 301 this, 0, 200, mDisappearYTranslation, false, 302 mDisappearAnimationUtils.getInterpolator(), () -> { 303 if (finishRunnable != null) { 304 finishRunnable.run(); 305 } 306 }, 307 getAnimationListener(CUJ_LOCKSCREEN_PIN_DISAPPEAR)); 308 return true; 309 } 310 311 @Override hasOverlappingRendering()312 public boolean hasOverlappingRendering() { 313 return false; 314 } 315 316 /** Animate subviews according to expansion or time. */ animate(float progress)317 private void animate(float progress) { 318 Interpolator standardDecelerate = Interpolators.STANDARD_DECELERATE; 319 Interpolator legacyDecelerate = Interpolators.LEGACY_DECELERATE; 320 float standardProgress = standardDecelerate.getInterpolation(progress); 321 322 mBouncerMessageArea.setTranslationY( 323 mYTrans - mYTrans * standardProgress); 324 mBouncerMessageArea.setAlpha(standardProgress); 325 326 for (int i = 0; i < mViews.length; i++) { 327 View[] row = mViews[i]; 328 for (View view : row) { 329 if (view == null) { 330 continue; 331 } 332 333 float scaledProgress = legacyDecelerate.getInterpolation(MathUtils.constrain( 334 (progress - 0.075f * i) / (1f - 0.075f * mViews.length), 335 0f, 336 1f 337 )); 338 view.setAlpha(scaledProgress); 339 int yDistance = mYTrans + mYTransOffset * i; 340 view.setTranslationY( 341 yDistance - (yDistance * standardProgress)); 342 if (view instanceof NumPadAnimationListener) { 343 ((NumPadAnimationListener) view).setProgress(scaledProgress); 344 } 345 } 346 } 347 } 348 } 349