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