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