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 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 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 */ 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 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 */ 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 */ 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 */ 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 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 258 private void cancelShowRunnable() { 259 DejankUtils.removeCallbacks(mShowRunnable); 260 mShowingSoon = false; 261 } 262 263 public void showWithDismissAction(OnDismissAction r, Runnable cancelAction) { 264 ensureView(); 265 mKeyguardView.setOnDismissAction(r, cancelAction); 266 show(false /* resetSecuritySelection */); 267 } 268 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 */ 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 */ 309 public void reset() { 310 cancelShowRunnable(); 311 inflateView(); 312 mFalsingManager.onBouncerHidden(); 313 } 314 315 public void onScreenTurnedOff() { 316 if (mKeyguardView != null && mRoot != null && mRoot.getVisibility() == View.VISIBLE) { 317 mKeyguardView.onPause(); 318 } 319 } 320 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 */ 330 public boolean isAnimatingAway() { 331 return mIsAnimatingAway; 332 } 333 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 */ 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 366 public boolean willDismissWithAction() { 367 return mKeyguardView != null && mKeyguardView.hasDismissActions(); 368 } 369 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 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 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 413 protected void removeView() { 414 if (mRoot != null && mRoot.getParent() == mContainer) { 415 mContainer.removeView(mRoot); 416 mRoot = null; 417 } 418 } 419 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 */ 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 */ 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 */ 452 public boolean isSecure() { 453 return mKeyguardView == null || mKeyguardView.getSecurityMode() != SecurityMode.None; 454 } 455 456 public boolean shouldDismissOnMenuPressed() { 457 return mKeyguardView.shouldEnableMenuKey(); 458 } 459 460 public boolean interceptMediaKey(KeyEvent event) { 461 ensureView(); 462 return mKeyguardView.interceptMediaKey(event); 463 } 464 465 public void notifyKeyguardAuthenticated(boolean strongAuth) { 466 ensureView(); 467 mKeyguardView.finish(strongAuth, KeyguardUpdateMonitor.getCurrentUser()); 468 } 469 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 { 482 void onFullyShown(); 483 void onFullyHidden(); 484 } 485 } 486