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