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