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 package com.android.keyguard; 17 18 import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; 19 import static android.content.res.Configuration.ORIENTATION_PORTRAIT; 20 21 import static com.android.systemui.statusbar.policy.DevicePostureController.DEVICE_POSTURE_CLOSED; 22 import static com.android.systemui.statusbar.policy.DevicePostureController.DEVICE_POSTURE_HALF_OPENED; 23 import static com.android.systemui.statusbar.policy.DevicePostureController.DEVICE_POSTURE_UNKNOWN; 24 25 import android.annotation.Nullable; 26 import android.content.Context; 27 import android.content.res.Configuration; 28 import android.graphics.Rect; 29 import android.os.SystemClock; 30 import android.text.TextUtils; 31 import android.util.AttributeSet; 32 import android.view.MotionEvent; 33 import android.view.View; 34 import android.view.animation.AnimationUtils; 35 import android.view.animation.Interpolator; 36 37 import androidx.constraintlayout.motion.widget.MotionLayout; 38 import androidx.constraintlayout.widget.ConstraintLayout; 39 import androidx.constraintlayout.widget.ConstraintSet; 40 41 import com.android.internal.jank.InteractionJankMonitor; 42 import com.android.internal.widget.LockPatternView; 43 import com.android.settingslib.animation.AppearAnimationCreator; 44 import com.android.settingslib.animation.AppearAnimationUtils; 45 import com.android.settingslib.animation.DisappearAnimationUtils; 46 import com.android.systemui.Flags; 47 import com.android.systemui.bouncer.shared.constants.PatternBouncerConstants.ColorId; 48 import com.android.systemui.res.R; 49 import com.android.systemui.statusbar.policy.DevicePostureController.DevicePostureInt; 50 51 public class KeyguardPatternView extends KeyguardInputView 52 implements AppearAnimationCreator<LockPatternView.CellState> { 53 54 private static final String TAG = "SecurityPatternView"; 55 private static final boolean DEBUG = KeyguardConstants.DEBUG; 56 57 58 // how long we stay awake after each key beyond MIN_PATTERN_BEFORE_POKE_WAKELOCK 59 private static final int UNLOCK_PATTERN_WAKE_INTERVAL_MS = 7000; 60 61 // How much we scale up the duration of the disappear animation when the current user is locked 62 public static final float DISAPPEAR_MULTIPLIER_LOCKED = 1.5f; 63 64 // Extra padding, in pixels, that should eat touch events. 65 private static final int PATTERNS_TOUCH_AREA_EXTENSION = 40; 66 67 private final AppearAnimationUtils mAppearAnimationUtils; 68 private final DisappearAnimationUtils mDisappearAnimationUtils; 69 private final DisappearAnimationUtils mDisappearAnimationUtilsLocked; 70 private final int[] mTmpPosition = new int[2]; 71 private final Rect mTempRect = new Rect(); 72 private final Rect mLockPatternScreenBounds = new Rect(); 73 74 private LockPatternView mLockPatternView; 75 76 /** 77 * Keeps track of the last time we poked the wake lock during dispatching of the touch event. 78 * Initialized to something guaranteed to make us poke the wakelock when the user starts 79 * drawing the pattern. 80 * @see #dispatchTouchEvent(android.view.MotionEvent) 81 */ 82 private long mLastPokeTime = -UNLOCK_PATTERN_WAKE_INTERVAL_MS; 83 84 BouncerKeyguardMessageArea mSecurityMessageDisplay; 85 private View mEcaView; 86 @Nullable private MotionLayout mContainerMotionLayout; 87 // TODO (b/293252410) - usage of mContainerConstraintLayout should be removed 88 // when the flag is enabled/removed 89 @Nullable private ConstraintLayout mContainerConstraintLayout; 90 private boolean mAlreadyUsingSplitBouncer = false; 91 private boolean mIsSmallLockScreenLandscapeEnabled = false; 92 @DevicePostureInt private int mLastDevicePosture = DEVICE_POSTURE_UNKNOWN; 93 KeyguardPatternView(Context context)94 public KeyguardPatternView(Context context) { 95 this(context, null); 96 } 97 KeyguardPatternView(Context context, AttributeSet attrs)98 public KeyguardPatternView(Context context, AttributeSet attrs) { 99 super(context, attrs); 100 mAppearAnimationUtils = new AppearAnimationUtils(context, 101 AppearAnimationUtils.DEFAULT_APPEAR_DURATION, 1.5f /* translationScale */, 102 2.0f /* delayScale */, AnimationUtils.loadInterpolator( 103 mContext, android.R.interpolator.linear_out_slow_in)); 104 mDisappearAnimationUtils = new DisappearAnimationUtils(context, 105 125, 1.2f /* translationScale */, 106 0.6f /* delayScale */, AnimationUtils.loadInterpolator( 107 mContext, android.R.interpolator.fast_out_linear_in)); 108 mDisappearAnimationUtilsLocked = new DisappearAnimationUtils(context, 109 (long) (125 * DISAPPEAR_MULTIPLIER_LOCKED), 1.2f /* translationScale */, 110 0.6f /* delayScale */, AnimationUtils.loadInterpolator( 111 mContext, android.R.interpolator.fast_out_linear_in)); 112 } 113 114 /** 115 * Use motion layout (new bouncer implementation) if LOCKSCREEN_ENABLE_LANDSCAPE flag is 116 * enabled, instead of constraint layout (old bouncer implementation) 117 */ setIsLockScreenLandscapeEnabled(boolean isLockScreenLandscapeEnabled)118 public void setIsLockScreenLandscapeEnabled(boolean isLockScreenLandscapeEnabled) { 119 mIsSmallLockScreenLandscapeEnabled = isLockScreenLandscapeEnabled; 120 findContainerLayout(); 121 } 122 findContainerLayout()123 private void findContainerLayout() { 124 if (mIsSmallLockScreenLandscapeEnabled) { 125 mContainerMotionLayout = findViewById(R.id.pattern_container); 126 } else { 127 mContainerConstraintLayout = findViewById(R.id.pattern_container); 128 } 129 } 130 131 @Override onConfigurationChanged(Configuration newConfig)132 protected void onConfigurationChanged(Configuration newConfig) { 133 updateMargins(); 134 } 135 onDevicePostureChanged(@evicePostureInt int posture)136 void onDevicePostureChanged(@DevicePostureInt int posture) { 137 if (mLastDevicePosture == posture) return; 138 mLastDevicePosture = posture; 139 140 if (mIsSmallLockScreenLandscapeEnabled) { 141 boolean useSplitBouncerAfterFold = 142 mLastDevicePosture == DEVICE_POSTURE_CLOSED 143 && getResources().getConfiguration().orientation == ORIENTATION_LANDSCAPE 144 && getResources().getBoolean(R.bool.update_bouncer_constraints); 145 146 if (mAlreadyUsingSplitBouncer != useSplitBouncerAfterFold) { 147 updateConstraints(useSplitBouncerAfterFold); 148 } 149 } 150 151 updateMargins(); 152 } 153 updateMargins()154 private void updateMargins() { 155 if (mIsSmallLockScreenLandscapeEnabled) { 156 updateHalfFoldedConstraints(); 157 } else { 158 updateHalfFoldedGuideline(); 159 } 160 } 161 updateHalfFoldedConstraints()162 private void updateHalfFoldedConstraints() { 163 // Update the constraints based on the device posture... 164 if (mAlreadyUsingSplitBouncer) return; 165 166 boolean shouldCollapsePattern = 167 mLastDevicePosture == DEVICE_POSTURE_HALF_OPENED 168 && mContext.getResources().getConfiguration().orientation 169 == ORIENTATION_PORTRAIT; 170 171 int expectedMotionLayoutState = shouldCollapsePattern 172 ? R.id.half_folded_single_constraints 173 : R.id.single_constraints; 174 175 transitionToMotionLayoutState(expectedMotionLayoutState); 176 } 177 178 // TODO (b/293252410) - this method can be removed when the flag is enabled/removed updateHalfFoldedGuideline()179 private void updateHalfFoldedGuideline() { 180 // Update the guideline based on the device posture... 181 float halfOpenPercentage = 182 mContext.getResources().getFloat(R.dimen.half_opened_bouncer_height_ratio); 183 184 ConstraintSet cs = new ConstraintSet(); 185 cs.clone(mContainerConstraintLayout); 186 cs.setGuidelinePercent(R.id.pattern_top_guideline, 187 mLastDevicePosture == DEVICE_POSTURE_HALF_OPENED ? halfOpenPercentage : 0.0f); 188 cs.applyTo(mContainerConstraintLayout); 189 } 190 transitionToMotionLayoutState(int state)191 private void transitionToMotionLayoutState(int state) { 192 if (mContainerMotionLayout.getCurrentState() != state) { 193 mContainerMotionLayout.transitionToState(state); 194 } 195 } 196 197 /** 198 * Updates the keyguard view's constraints (single or split constraints). 199 * Split constraints are only used for small landscape screens. 200 * Only called when flag LANDSCAPE_ENABLE_LOCKSCREEN is enabled. 201 */ 202 @Override updateConstraints(boolean useSplitBouncer)203 protected void updateConstraints(boolean useSplitBouncer) { 204 if (!mIsSmallLockScreenLandscapeEnabled) return; 205 206 mAlreadyUsingSplitBouncer = useSplitBouncer; 207 208 if (useSplitBouncer) { 209 mContainerMotionLayout.jumpToState(R.id.split_constraints); 210 mContainerMotionLayout.setMaxWidth(Integer.MAX_VALUE); 211 } else { 212 boolean useHalfFoldedConstraints = 213 mLastDevicePosture == DEVICE_POSTURE_HALF_OPENED 214 && mContext.getResources().getConfiguration().orientation 215 == ORIENTATION_PORTRAIT; 216 217 if (useHalfFoldedConstraints) { 218 mContainerMotionLayout.jumpToState(R.id.half_folded_single_constraints); 219 } else { 220 mContainerMotionLayout.jumpToState(R.id.single_constraints); 221 } 222 mContainerMotionLayout.setMaxWidth(getResources() 223 .getDimensionPixelSize(R.dimen.biometric_auth_pattern_view_max_size)); 224 } 225 } 226 227 @Override onFinishInflate()228 protected void onFinishInflate() { 229 super.onFinishInflate(); 230 231 mLockPatternView = findViewById(R.id.lockPatternView); 232 if (Flags.bouncerUiRevamp2()) { 233 mLockPatternView.setDotColors(mContext.getColor(ColorId.dotColor), mContext.getColor( 234 ColorId.activatedDotColor)); 235 mLockPatternView.setColors(mContext.getColor(ColorId.pathColor), 0, 0); 236 mLockPatternView.setDotSizes( 237 getResources().getDimensionPixelSize(R.dimen.keyguard_pattern_dot_size), 238 getResources().getDimensionPixelSize( 239 R.dimen.keyguard_pattern_activated_dot_size)); 240 mLockPatternView.setPathWidth( 241 getResources().getDimensionPixelSize(R.dimen.keyguard_pattern_stroke_width)); 242 } 243 244 mEcaView = findViewById(R.id.keyguard_selector_fade_container); 245 } 246 247 @Override onAttachedToWindow()248 protected void onAttachedToWindow() { 249 super.onAttachedToWindow(); 250 mSecurityMessageDisplay = findViewById(R.id.bouncer_message_area); 251 } 252 253 @Override onTouchEvent(MotionEvent ev)254 public boolean onTouchEvent(MotionEvent ev) { 255 boolean result = super.onTouchEvent(ev); 256 // as long as the user is entering a pattern (i.e sending a touch event that was handled 257 // by this screen), keep poking the wake lock so that the screen will stay on. 258 final long elapsed = SystemClock.elapsedRealtime() - mLastPokeTime; 259 if (result && (elapsed > (UNLOCK_PATTERN_WAKE_INTERVAL_MS - 100))) { 260 mLastPokeTime = SystemClock.elapsedRealtime(); 261 } 262 mTempRect.set(0, 0, 0, 0); 263 offsetRectIntoDescendantCoords(mLockPatternView, mTempRect); 264 ev.offsetLocation(mTempRect.left, mTempRect.top); 265 result = mLockPatternView.dispatchTouchEvent(ev) || result; 266 ev.offsetLocation(-mTempRect.left, -mTempRect.top); 267 return result; 268 } 269 270 @Override onLayout(boolean changed, int l, int t, int r, int b)271 protected void onLayout(boolean changed, int l, int t, int r, int b) { 272 super.onLayout(changed, l, t, r, b); 273 mLockPatternView.getLocationOnScreen(mTmpPosition); 274 mLockPatternScreenBounds.set(mTmpPosition[0] - PATTERNS_TOUCH_AREA_EXTENSION, 275 mTmpPosition[1] - PATTERNS_TOUCH_AREA_EXTENSION, 276 mTmpPosition[0] + mLockPatternView.getWidth() + PATTERNS_TOUCH_AREA_EXTENSION, 277 mTmpPosition[1] + mLockPatternView.getHeight() + PATTERNS_TOUCH_AREA_EXTENSION); 278 } 279 280 @Override disallowInterceptTouch(MotionEvent event)281 boolean disallowInterceptTouch(MotionEvent event) { 282 return !mLockPatternView.isEmpty() 283 || mLockPatternScreenBounds.contains((int) event.getRawX(), (int) event.getRawY()); 284 } 285 startAppearAnimation()286 public void startAppearAnimation() { 287 enableClipping(false); 288 setAlpha(0f); 289 setTranslationY(mAppearAnimationUtils.getStartTranslation()); 290 AppearAnimationUtils.startTranslationYAnimation(this, 0 /* delay */, 500 /* duration */, 291 0, mAppearAnimationUtils.getInterpolator(), 292 getAnimationListener(InteractionJankMonitor.CUJ_LOCKSCREEN_PATTERN_APPEAR)); 293 mLockPatternView.post(() -> { 294 setAlpha(1f); 295 mAppearAnimationUtils.startAnimation2d( 296 mLockPatternView.getCellStates(), 297 () -> { 298 enableClipping(true); 299 mLockPatternView.invalidate(); 300 }, 301 KeyguardPatternView.this); 302 }); 303 if (!TextUtils.isEmpty(mSecurityMessageDisplay.getText())) { 304 mAppearAnimationUtils.createAnimation(mSecurityMessageDisplay, 0, 305 AppearAnimationUtils.DEFAULT_APPEAR_DURATION, 306 mAppearAnimationUtils.getStartTranslation(), 307 true /* appearing */, 308 mAppearAnimationUtils.getInterpolator(), 309 null /* finishRunnable */); 310 } 311 } 312 startDisappearAnimation(boolean needsSlowUnlockTransition, final Runnable finishRunnable)313 public boolean startDisappearAnimation(boolean needsSlowUnlockTransition, 314 final Runnable finishRunnable) { 315 float durationMultiplier = needsSlowUnlockTransition ? DISAPPEAR_MULTIPLIER_LOCKED : 1f; 316 mLockPatternView.clearPattern(); 317 enableClipping(false); 318 setTranslationY(0); 319 AppearAnimationUtils.startTranslationYAnimation(this, 0 /* delay */, 320 (long) (300 * durationMultiplier), 321 -mDisappearAnimationUtils.getStartTranslation(), 322 mDisappearAnimationUtils.getInterpolator(), 323 getAnimationListener(InteractionJankMonitor.CUJ_LOCKSCREEN_PATTERN_DISAPPEAR)); 324 325 DisappearAnimationUtils disappearAnimationUtils = needsSlowUnlockTransition 326 ? mDisappearAnimationUtilsLocked : mDisappearAnimationUtils; 327 disappearAnimationUtils.startAnimation2d(mLockPatternView.getCellStates(), 328 () -> { 329 enableClipping(true); 330 if (finishRunnable != null) { 331 finishRunnable.run(); 332 } 333 }, KeyguardPatternView.this); 334 if (!TextUtils.isEmpty(mSecurityMessageDisplay.getText())) { 335 mDisappearAnimationUtils.createAnimation(mSecurityMessageDisplay, 0, 336 (long) (200 * durationMultiplier), 337 -mDisappearAnimationUtils.getStartTranslation() * 3, 338 false /* appearing */, 339 mDisappearAnimationUtils.getInterpolator(), 340 null /* finishRunnable */); 341 } 342 return true; 343 } 344 enableClipping(boolean enable)345 private void enableClipping(boolean enable) { 346 if (mContainerConstraintLayout != null) { 347 setClipChildren(enable); 348 mContainerConstraintLayout.setClipToPadding(enable); 349 mContainerConstraintLayout.setClipChildren(enable); 350 } 351 if (mContainerMotionLayout != null) { 352 setClipChildren(enable); 353 mContainerMotionLayout.setClipToPadding(enable); 354 mContainerMotionLayout.setClipChildren(enable); 355 } 356 } 357 358 @Override createAnimation(final LockPatternView.CellState animatedCell, long delay, long duration, float translationY, final boolean appearing, Interpolator interpolator, final Runnable finishListener)359 public void createAnimation(final LockPatternView.CellState animatedCell, long delay, 360 long duration, float translationY, final boolean appearing, 361 Interpolator interpolator, 362 final Runnable finishListener) { 363 mLockPatternView.startCellStateAnimation(animatedCell, 364 1f, appearing ? 1f : 0f, /* alpha */ 365 appearing ? translationY : 0f, appearing ? 0f : translationY, /* translation */ 366 appearing ? 0f : 1f, 1f /* scale */, 367 delay, duration, interpolator, finishListener); 368 if (finishListener != null) { 369 // Also animate the Emergency call 370 mAppearAnimationUtils.createAnimation(mEcaView, delay, duration, translationY, 371 appearing, interpolator, null); 372 } 373 } 374 375 @Override hasOverlappingRendering()376 public boolean hasOverlappingRendering() { 377 return false; 378 } 379 380 @Override getTitle()381 public CharSequence getTitle() { 382 return getResources().getString( 383 com.android.internal.R.string.keyguard_accessibility_pattern_unlock); 384 } 385 } 386