1 /* 2 * Copyright (C) 2014 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.systemui.statusbar.phone; 18 19 import static com.android.keyguard.KeyguardSecurityModel.SecurityMode; 20 import static com.android.systemui.plugins.ActivityStarter.OnDismissAction; 21 22 import android.content.Context; 23 import android.content.res.ColorStateList; 24 import android.os.Handler; 25 import android.os.UserHandle; 26 import android.os.UserManager; 27 import android.util.Log; 28 import android.util.MathUtils; 29 import android.view.KeyEvent; 30 import android.view.LayoutInflater; 31 import android.view.View; 32 import android.view.ViewGroup; 33 import android.view.ViewTreeObserver; 34 import android.view.WindowInsets; 35 36 import com.android.internal.widget.LockPatternUtils; 37 import com.android.keyguard.KeyguardHostView; 38 import com.android.keyguard.KeyguardSecurityModel; 39 import com.android.keyguard.KeyguardSecurityView; 40 import com.android.keyguard.KeyguardUpdateMonitor; 41 import com.android.keyguard.KeyguardUpdateMonitorCallback; 42 import com.android.keyguard.ViewMediatorCallback; 43 import com.android.systemui.DejankUtils; 44 import com.android.systemui.Dependency; 45 import com.android.systemui.R; 46 import com.android.systemui.keyguard.DismissCallbackRegistry; 47 import com.android.systemui.plugins.FalsingManager; 48 import com.android.systemui.shared.system.SysUiStatsLog; 49 import com.android.systemui.statusbar.policy.KeyguardStateController; 50 51 import java.io.PrintWriter; 52 53 /** 54 * A class which manages the bouncer on the lockscreen. 55 */ 56 public class KeyguardBouncer { 57 58 private static final String TAG = "KeyguardBouncer"; 59 static final long BOUNCER_FACE_DELAY = 1200; 60 static final float ALPHA_EXPANSION_THRESHOLD = 0.95f; 61 static final float EXPANSION_HIDDEN = 1f; 62 static final float EXPANSION_VISIBLE = 0f; 63 64 protected final Context mContext; 65 protected final ViewMediatorCallback mCallback; 66 protected final LockPatternUtils mLockPatternUtils; 67 protected final ViewGroup mContainer; 68 private final FalsingManager mFalsingManager; 69 private final DismissCallbackRegistry mDismissCallbackRegistry; 70 private final Handler mHandler; 71 private final BouncerExpansionCallback mExpansionCallback; 72 private final KeyguardUpdateMonitor mKeyguardUpdateMonitor; 73 private final KeyguardStateController mKeyguardStateController; 74 private final KeyguardUpdateMonitorCallback mUpdateMonitorCallback = 75 new KeyguardUpdateMonitorCallback() { 76 @Override 77 public void onStrongAuthStateChanged(int userId) { 78 mBouncerPromptReason = mCallback.getBouncerPromptReason(); 79 } 80 }; 81 private final Runnable mRemoveViewRunnable = this::removeView; 82 private final KeyguardBypassController mKeyguardBypassController; 83 protected KeyguardHostView mKeyguardView; 84 private final Runnable mResetRunnable = ()-> { 85 if (mKeyguardView != null) { 86 mKeyguardView.resetSecurityContainer(); 87 } 88 }; 89 90 private int mStatusBarHeight; 91 private float mExpansion = EXPANSION_HIDDEN; 92 protected ViewGroup mRoot; 93 private boolean mShowingSoon; 94 private int mBouncerPromptReason; 95 private boolean mIsAnimatingAway; 96 private boolean mIsScrimmed; 97 KeyguardBouncer(Context context, ViewMediatorCallback callback, LockPatternUtils lockPatternUtils, ViewGroup container, DismissCallbackRegistry dismissCallbackRegistry, FalsingManager falsingManager, BouncerExpansionCallback expansionCallback, KeyguardStateController keyguardStateController, KeyguardUpdateMonitor keyguardUpdateMonitor, KeyguardBypassController keyguardBypassController, Handler handler)98 public KeyguardBouncer(Context context, ViewMediatorCallback callback, 99 LockPatternUtils lockPatternUtils, ViewGroup container, 100 DismissCallbackRegistry dismissCallbackRegistry, FalsingManager falsingManager, 101 BouncerExpansionCallback expansionCallback, 102 KeyguardStateController keyguardStateController, 103 KeyguardUpdateMonitor keyguardUpdateMonitor, 104 KeyguardBypassController keyguardBypassController, Handler handler) { 105 mContext = context; 106 mCallback = callback; 107 mLockPatternUtils = lockPatternUtils; 108 mContainer = container; 109 mKeyguardUpdateMonitor = keyguardUpdateMonitor; 110 mFalsingManager = falsingManager; 111 mDismissCallbackRegistry = dismissCallbackRegistry; 112 mExpansionCallback = expansionCallback; 113 mHandler = handler; 114 mKeyguardStateController = keyguardStateController; 115 mKeyguardUpdateMonitor.registerCallback(mUpdateMonitorCallback); 116 mKeyguardBypassController = keyguardBypassController; 117 } 118 show(boolean resetSecuritySelection)119 public void show(boolean resetSecuritySelection) { 120 show(resetSecuritySelection, true /* scrimmed */); 121 } 122 123 /** 124 * Shows the bouncer. 125 * 126 * @param resetSecuritySelection Cleans keyguard view 127 * @param isScrimmed true when the bouncer show show scrimmed, false when the user will be 128 * dragging it and translation should be deferred. 129 */ show(boolean resetSecuritySelection, boolean isScrimmed)130 public void show(boolean resetSecuritySelection, boolean isScrimmed) { 131 final int keyguardUserId = KeyguardUpdateMonitor.getCurrentUser(); 132 if (keyguardUserId == UserHandle.USER_SYSTEM && UserManager.isSplitSystemUser()) { 133 // In split system user mode, we never unlock system user. 134 return; 135 } 136 ensureView(); 137 mIsScrimmed = isScrimmed; 138 139 // On the keyguard, we want to show the bouncer when the user drags up, but it's 140 // not correct to end the falsing session. We still need to verify if those touches 141 // are valid. 142 // Later, at the end of the animation, when the bouncer is at the top of the screen, 143 // onFullyShown() will be called and FalsingManager will stop recording touches. 144 if (isScrimmed) { 145 setExpansion(EXPANSION_VISIBLE); 146 } 147 148 if (resetSecuritySelection) { 149 // showPrimarySecurityScreen() updates the current security method. This is needed in 150 // case we are already showing and the current security method changed. 151 showPrimarySecurityScreen(); 152 } 153 154 if (mRoot.getVisibility() == View.VISIBLE || mShowingSoon) { 155 return; 156 } 157 158 final int activeUserId = KeyguardUpdateMonitor.getCurrentUser(); 159 final boolean isSystemUser = 160 UserManager.isSplitSystemUser() && activeUserId == UserHandle.USER_SYSTEM; 161 final boolean allowDismissKeyguard = !isSystemUser && activeUserId == keyguardUserId; 162 163 // If allowed, try to dismiss the Keyguard. If no security auth (password/pin/pattern) is 164 // set, this will dismiss the whole Keyguard. Otherwise, show the bouncer. 165 if (allowDismissKeyguard && mKeyguardView.dismiss(activeUserId)) { 166 return; 167 } 168 169 // This condition may indicate an error on Android, so log it. 170 if (!allowDismissKeyguard) { 171 Log.w(TAG, "User can't dismiss keyguard: " + activeUserId + " != " + keyguardUserId); 172 } 173 174 mShowingSoon = true; 175 176 // Split up the work over multiple frames. 177 DejankUtils.removeCallbacks(mResetRunnable); 178 if (mKeyguardStateController.isFaceAuthEnabled() && !needsFullscreenBouncer() 179 && !mKeyguardUpdateMonitor.userNeedsStrongAuth() 180 && !mKeyguardBypassController.getBypassEnabled()) { 181 mHandler.postDelayed(mShowRunnable, BOUNCER_FACE_DELAY); 182 } else { 183 DejankUtils.postAfterTraversal(mShowRunnable); 184 } 185 186 mCallback.onBouncerVisiblityChanged(true /* shown */); 187 mExpansionCallback.onStartingToShow(); 188 } 189 isScrimmed()190 public boolean isScrimmed() { 191 return mIsScrimmed; 192 } 193 194 /** 195 * This method must be called at the end of the bouncer animation when 196 * the translation is performed manually by the user, otherwise FalsingManager 197 * will never be notified and its internal state will be out of sync. 198 */ onFullyShown()199 private void onFullyShown() { 200 mFalsingManager.onBouncerShown(); 201 if (mKeyguardView == null) { 202 Log.wtf(TAG, "onFullyShown when view was null"); 203 } else { 204 mKeyguardView.onResume(); 205 if (mRoot != null) { 206 mRoot.announceForAccessibility(mKeyguardView.getAccessibilityTitleForCurrentMode()); 207 } 208 } 209 } 210 211 /** 212 * @see #onFullyShown() 213 */ onFullyHidden()214 private void onFullyHidden() { 215 cancelShowRunnable(); 216 if (mRoot != null) { 217 mRoot.setVisibility(View.INVISIBLE); 218 } 219 mFalsingManager.onBouncerHidden(); 220 DejankUtils.postAfterTraversal(mResetRunnable); 221 } 222 223 private final Runnable mShowRunnable = new Runnable() { 224 @Override 225 public void run() { 226 mRoot.setVisibility(View.VISIBLE); 227 showPromptReason(mBouncerPromptReason); 228 final CharSequence customMessage = mCallback.consumeCustomMessage(); 229 if (customMessage != null) { 230 mKeyguardView.showErrorMessage(customMessage); 231 } 232 // We might still be collapsed and the view didn't have time to layout yet or still 233 // be small, let's wait on the predraw to do the animation in that case. 234 if (mKeyguardView.getHeight() != 0 && mKeyguardView.getHeight() != mStatusBarHeight) { 235 mKeyguardView.startAppearAnimation(); 236 } else { 237 mKeyguardView.getViewTreeObserver().addOnPreDrawListener( 238 new ViewTreeObserver.OnPreDrawListener() { 239 @Override 240 public boolean onPreDraw() { 241 mKeyguardView.getViewTreeObserver().removeOnPreDrawListener(this); 242 mKeyguardView.startAppearAnimation(); 243 return true; 244 } 245 }); 246 mKeyguardView.requestLayout(); 247 } 248 mShowingSoon = false; 249 if (mExpansion == EXPANSION_VISIBLE) { 250 mKeyguardView.onResume(); 251 mKeyguardView.resetSecurityContainer(); 252 showPromptReason(mBouncerPromptReason); 253 } 254 SysUiStatsLog.write(SysUiStatsLog.KEYGUARD_BOUNCER_STATE_CHANGED, 255 SysUiStatsLog.KEYGUARD_BOUNCER_STATE_CHANGED__STATE__SHOWN); 256 } 257 }; 258 259 /** 260 * Show a string explaining why the security view needs to be solved. 261 * 262 * @param reason a flag indicating which string should be shown, see 263 * {@link KeyguardSecurityView#PROMPT_REASON_NONE} 264 * and {@link KeyguardSecurityView#PROMPT_REASON_RESTART} 265 */ showPromptReason(int reason)266 public void showPromptReason(int reason) { 267 if (mKeyguardView != null) { 268 mKeyguardView.showPromptReason(reason); 269 } else { 270 Log.w(TAG, "Trying to show prompt reason on empty bouncer"); 271 } 272 } 273 showMessage(String message, ColorStateList colorState)274 public void showMessage(String message, ColorStateList colorState) { 275 if (mKeyguardView != null) { 276 mKeyguardView.showMessage(message, colorState); 277 } else { 278 Log.w(TAG, "Trying to show message on empty bouncer"); 279 } 280 } 281 cancelShowRunnable()282 private void cancelShowRunnable() { 283 DejankUtils.removeCallbacks(mShowRunnable); 284 mHandler.removeCallbacks(mShowRunnable); 285 mShowingSoon = false; 286 } 287 showWithDismissAction(OnDismissAction r, Runnable cancelAction)288 public void showWithDismissAction(OnDismissAction r, Runnable cancelAction) { 289 ensureView(); 290 mKeyguardView.setOnDismissAction(r, cancelAction); 291 show(false /* resetSecuritySelection */); 292 } 293 hide(boolean destroyView)294 public void hide(boolean destroyView) { 295 if (isShowing()) { 296 SysUiStatsLog.write(SysUiStatsLog.KEYGUARD_BOUNCER_STATE_CHANGED, 297 SysUiStatsLog.KEYGUARD_BOUNCER_STATE_CHANGED__STATE__HIDDEN); 298 mDismissCallbackRegistry.notifyDismissCancelled(); 299 } 300 mIsScrimmed = false; 301 mFalsingManager.onBouncerHidden(); 302 mCallback.onBouncerVisiblityChanged(false /* shown */); 303 cancelShowRunnable(); 304 if (mKeyguardView != null) { 305 mKeyguardView.cancelDismissAction(); 306 mKeyguardView.cleanUp(); 307 } 308 mIsAnimatingAway = false; 309 if (mRoot != null) { 310 mRoot.setVisibility(View.INVISIBLE); 311 if (destroyView) { 312 313 // We have a ViewFlipper that unregisters a broadcast when being detached, which may 314 // be slow because of AM lock contention during unlocking. We can delay it a bit. 315 mHandler.postDelayed(mRemoveViewRunnable, 50); 316 } 317 } 318 } 319 320 /** 321 * See {@link StatusBarKeyguardViewManager#startPreHideAnimation}. 322 */ startPreHideAnimation(Runnable runnable)323 public void startPreHideAnimation(Runnable runnable) { 324 mIsAnimatingAway = true; 325 if (mKeyguardView != null) { 326 mKeyguardView.startDisappearAnimation(runnable); 327 } else if (runnable != null) { 328 runnable.run(); 329 } 330 } 331 332 /** 333 * Reset the state of the view. 334 */ reset()335 public void reset() { 336 cancelShowRunnable(); 337 inflateView(); 338 mFalsingManager.onBouncerHidden(); 339 } 340 onScreenTurnedOff()341 public void onScreenTurnedOff() { 342 if (mKeyguardView != null && mRoot != null && mRoot.getVisibility() == View.VISIBLE) { 343 mKeyguardView.onPause(); 344 } 345 } 346 isShowing()347 public boolean isShowing() { 348 return (mShowingSoon || (mRoot != null && mRoot.getVisibility() == View.VISIBLE)) 349 && mExpansion == EXPANSION_VISIBLE && !isAnimatingAway(); 350 } 351 352 /** 353 * {@link #show(boolean)} was called but we're not showing yet, or being dragged. 354 */ inTransit()355 public boolean inTransit() { 356 return mShowingSoon || mExpansion != EXPANSION_HIDDEN && mExpansion != EXPANSION_VISIBLE; 357 } 358 359 /** 360 * @return {@code true} when bouncer's pre-hide animation already started but isn't completely 361 * hidden yet, {@code false} otherwise. 362 */ isAnimatingAway()363 public boolean isAnimatingAway() { 364 return mIsAnimatingAway; 365 } 366 prepare()367 public void prepare() { 368 boolean wasInitialized = mRoot != null; 369 ensureView(); 370 if (wasInitialized) { 371 showPrimarySecurityScreen(); 372 } 373 mBouncerPromptReason = mCallback.getBouncerPromptReason(); 374 } 375 showPrimarySecurityScreen()376 private void showPrimarySecurityScreen() { 377 mKeyguardView.showPrimarySecurityScreen(); 378 } 379 380 /** 381 * Current notification panel expansion 382 * @param fraction 0 when notification panel is collapsed and 1 when expanded. 383 * @see StatusBarKeyguardViewManager#onPanelExpansionChanged 384 */ setExpansion(float fraction)385 public void setExpansion(float fraction) { 386 float oldExpansion = mExpansion; 387 mExpansion = fraction; 388 if (mKeyguardView != null && !mIsAnimatingAway) { 389 float alpha = MathUtils.map(ALPHA_EXPANSION_THRESHOLD, 1, 1, 0, fraction); 390 mKeyguardView.setAlpha(MathUtils.constrain(alpha, 0f, 1f)); 391 mKeyguardView.setTranslationY(fraction * mKeyguardView.getHeight()); 392 } 393 394 if (fraction == EXPANSION_VISIBLE && oldExpansion != EXPANSION_VISIBLE) { 395 onFullyShown(); 396 mExpansionCallback.onFullyShown(); 397 } else if (fraction == EXPANSION_HIDDEN && oldExpansion != EXPANSION_HIDDEN) { 398 onFullyHidden(); 399 mExpansionCallback.onFullyHidden(); 400 } else if (fraction != EXPANSION_VISIBLE && oldExpansion == EXPANSION_VISIBLE) { 401 mExpansionCallback.onStartingToHide(); 402 if (mKeyguardView != null) { 403 mKeyguardView.onStartingToHide(); 404 } 405 } 406 } 407 willDismissWithAction()408 public boolean willDismissWithAction() { 409 return mKeyguardView != null && mKeyguardView.hasDismissActions(); 410 } 411 getTop()412 public int getTop() { 413 if (mKeyguardView == null) { 414 return 0; 415 } 416 417 int top = mKeyguardView.getTop(); 418 // The password view has an extra top padding that should be ignored. 419 if (mKeyguardView.getCurrentSecurityMode() == SecurityMode.Password) { 420 View messageArea = mKeyguardView.findViewById(R.id.keyguard_message_area); 421 top += messageArea.getTop(); 422 } 423 return top; 424 } 425 ensureView()426 protected void ensureView() { 427 // Removal of the view might be deferred to reduce unlock latency, 428 // in this case we need to force the removal, otherwise we'll 429 // end up in an unpredictable state. 430 boolean forceRemoval = mHandler.hasCallbacks(mRemoveViewRunnable); 431 if (mRoot == null || forceRemoval) { 432 inflateView(); 433 } 434 } 435 inflateView()436 protected void inflateView() { 437 removeView(); 438 mHandler.removeCallbacks(mRemoveViewRunnable); 439 mRoot = (ViewGroup) LayoutInflater.from(mContext).inflate(R.layout.keyguard_bouncer, null); 440 mKeyguardView = mRoot.findViewById(R.id.keyguard_host_view); 441 mKeyguardView.setLockPatternUtils(mLockPatternUtils); 442 mKeyguardView.setViewMediatorCallback(mCallback); 443 mContainer.addView(mRoot, mContainer.getChildCount()); 444 mStatusBarHeight = mRoot.getResources().getDimensionPixelOffset( 445 com.android.systemui.R.dimen.status_bar_height); 446 mRoot.setVisibility(View.INVISIBLE); 447 448 final WindowInsets rootInsets = mRoot.getRootWindowInsets(); 449 if (rootInsets != null) { 450 mRoot.dispatchApplyWindowInsets(rootInsets); 451 } 452 } 453 removeView()454 protected void removeView() { 455 if (mRoot != null && mRoot.getParent() == mContainer) { 456 mContainer.removeView(mRoot); 457 mRoot = null; 458 } 459 } 460 onBackPressed()461 public boolean onBackPressed() { 462 return mKeyguardView != null && mKeyguardView.handleBackKey(); 463 } 464 465 /** 466 * @return True if and only if the security method should be shown before showing the 467 * notifications on Keyguard, like SIM PIN/PUK. 468 */ needsFullscreenBouncer()469 public boolean needsFullscreenBouncer() { 470 SecurityMode mode = Dependency.get(KeyguardSecurityModel.class).getSecurityMode( 471 KeyguardUpdateMonitor.getCurrentUser()); 472 return mode == SecurityMode.SimPin || mode == SecurityMode.SimPuk; 473 } 474 475 /** 476 * Like {@link #needsFullscreenBouncer}, but uses the currently visible security method, which 477 * makes this method much faster. 478 */ isFullscreenBouncer()479 public boolean isFullscreenBouncer() { 480 if (mKeyguardView != null) { 481 SecurityMode mode = mKeyguardView.getCurrentSecurityMode(); 482 return mode == SecurityMode.SimPin || mode == SecurityMode.SimPuk; 483 } 484 return false; 485 } 486 487 /** 488 * WARNING: This method might cause Binder calls. 489 */ isSecure()490 public boolean isSecure() { 491 return mKeyguardView == null || mKeyguardView.getSecurityMode() != SecurityMode.None; 492 } 493 shouldDismissOnMenuPressed()494 public boolean shouldDismissOnMenuPressed() { 495 return mKeyguardView.shouldEnableMenuKey(); 496 } 497 interceptMediaKey(KeyEvent event)498 public boolean interceptMediaKey(KeyEvent event) { 499 ensureView(); 500 return mKeyguardView.interceptMediaKey(event); 501 } 502 notifyKeyguardAuthenticated(boolean strongAuth)503 public void notifyKeyguardAuthenticated(boolean strongAuth) { 504 ensureView(); 505 mKeyguardView.finish(strongAuth, KeyguardUpdateMonitor.getCurrentUser()); 506 } 507 dump(PrintWriter pw)508 public void dump(PrintWriter pw) { 509 pw.println("KeyguardBouncer"); 510 pw.println(" isShowing(): " + isShowing()); 511 pw.println(" mStatusBarHeight: " + mStatusBarHeight); 512 pw.println(" mExpansion: " + mExpansion); 513 pw.println(" mKeyguardView; " + mKeyguardView); 514 pw.println(" mShowingSoon: " + mKeyguardView); 515 pw.println(" mBouncerPromptReason: " + mBouncerPromptReason); 516 pw.println(" mIsAnimatingAway: " + mIsAnimatingAway); 517 } 518 519 public interface BouncerExpansionCallback { onFullyShown()520 void onFullyShown(); onStartingToHide()521 void onStartingToHide(); onStartingToShow()522 void onStartingToShow(); onFullyHidden()523 void onFullyHidden(); 524 } 525 } 526