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