• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2020 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.keyguard;
18 
19 import static android.hardware.biometrics.BiometricSourceType.FINGERPRINT;
20 
21 import static com.android.systemui.classifier.Classifier.LOCK_ICON;
22 
23 import android.content.Context;
24 import android.content.res.Configuration;
25 import android.graphics.PointF;
26 import android.graphics.Rect;
27 import android.graphics.drawable.AnimatedVectorDrawable;
28 import android.graphics.drawable.Drawable;
29 import android.hardware.biometrics.BiometricSourceType;
30 import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
31 import android.media.AudioAttributes;
32 import android.os.Process;
33 import android.os.Vibrator;
34 import android.util.DisplayMetrics;
35 import android.view.GestureDetector;
36 import android.view.GestureDetector.SimpleOnGestureListener;
37 import android.view.MotionEvent;
38 import android.view.View;
39 import android.view.accessibility.AccessibilityManager;
40 import android.view.accessibility.AccessibilityNodeInfo;
41 
42 import androidx.annotation.NonNull;
43 import androidx.annotation.Nullable;
44 import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
45 
46 import com.android.systemui.Dumpable;
47 import com.android.systemui.R;
48 import com.android.systemui.biometrics.AuthController;
49 import com.android.systemui.biometrics.UdfpsController;
50 import com.android.systemui.dagger.qualifiers.Main;
51 import com.android.systemui.dump.DumpManager;
52 import com.android.systemui.plugins.FalsingManager;
53 import com.android.systemui.plugins.statusbar.StatusBarStateController;
54 import com.android.systemui.statusbar.StatusBarState;
55 import com.android.systemui.statusbar.phone.dagger.StatusBarComponent;
56 import com.android.systemui.statusbar.policy.ConfigurationController;
57 import com.android.systemui.statusbar.policy.KeyguardStateController;
58 import com.android.systemui.util.ViewController;
59 import com.android.systemui.util.concurrency.DelayableExecutor;
60 
61 import java.io.FileDescriptor;
62 import java.io.PrintWriter;
63 import java.util.Objects;
64 
65 import javax.inject.Inject;
66 
67 /**
68  * Controls when to show the LockIcon affordance (lock/unlocked icon or circle) on lock screen.
69  *
70  * This view will only be shown if the user has UDFPS or FaceAuth enrolled
71  */
72 @StatusBarComponent.StatusBarScope
73 public class LockIconViewController extends ViewController<LockIconView> implements Dumpable {
74     private static final float sDefaultDensity =
75             (float) DisplayMetrics.DENSITY_DEVICE_STABLE / (float) DisplayMetrics.DENSITY_DEFAULT;
76     private static final int sLockIconRadiusPx = (int) (sDefaultDensity * 36);
77     private static final float sDistAboveKgBottomAreaPx = sDefaultDensity * 12;
78     private static final AudioAttributes VIBRATION_SONIFICATION_ATTRIBUTES =
79             new AudioAttributes.Builder()
80                 .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
81                 .setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION)
82                 .build();
83 
84     @NonNull private final KeyguardUpdateMonitor mKeyguardUpdateMonitor;
85     @NonNull private final KeyguardViewController mKeyguardViewController;
86     @NonNull private final StatusBarStateController mStatusBarStateController;
87     @NonNull private final KeyguardStateController mKeyguardStateController;
88     @NonNull private final FalsingManager mFalsingManager;
89     @NonNull private final AuthController mAuthController;
90     @NonNull private final AccessibilityManager mAccessibilityManager;
91     @NonNull private final ConfigurationController mConfigurationController;
92     @NonNull private final DelayableExecutor mExecutor;
93     private boolean mUdfpsEnrolled;
94 
95     @NonNull private final AnimatedVectorDrawable mFpToUnlockIcon;
96     @NonNull private final AnimatedVectorDrawable mLockToUnlockIcon;
97     @NonNull private final Drawable mLockIcon;
98     @NonNull private final Drawable mUnlockIcon;
99     @NonNull private CharSequence mUnlockedLabel;
100     @NonNull private CharSequence mLockedLabel;
101     @Nullable private final Vibrator mVibrator;
102 
103     private boolean mIsDozing;
104     private boolean mIsBouncerShowing;
105     private boolean mRunningFPS;
106     private boolean mCanDismissLockScreen;
107     private boolean mQsExpanded;
108     private int mStatusBarState;
109     private boolean mIsKeyguardShowing;
110     private boolean mUserUnlockedWithBiometric;
111     private Runnable mCancelDelayedUpdateVisibilityRunnable;
112 
113     private boolean mUdfpsSupported;
114     private float mHeightPixels;
115     private float mWidthPixels;
116     private int mBottomPadding; // in pixels
117 
118     private boolean mShowUnlockIcon;
119     private boolean mShowLockIcon;
120 
121     private boolean mDownDetected;
122     private boolean mDetectedLongPress;
123     private final Rect mSensorTouchLocation = new Rect();
124 
125     @Inject
LockIconViewController( @ullable LockIconView view, @NonNull StatusBarStateController statusBarStateController, @NonNull KeyguardUpdateMonitor keyguardUpdateMonitor, @NonNull KeyguardViewController keyguardViewController, @NonNull KeyguardStateController keyguardStateController, @NonNull FalsingManager falsingManager, @NonNull AuthController authController, @NonNull DumpManager dumpManager, @NonNull AccessibilityManager accessibilityManager, @NonNull ConfigurationController configurationController, @NonNull @Main DelayableExecutor executor, @Nullable Vibrator vibrator )126     public LockIconViewController(
127             @Nullable LockIconView view,
128             @NonNull StatusBarStateController statusBarStateController,
129             @NonNull KeyguardUpdateMonitor keyguardUpdateMonitor,
130             @NonNull KeyguardViewController keyguardViewController,
131             @NonNull KeyguardStateController keyguardStateController,
132             @NonNull FalsingManager falsingManager,
133             @NonNull AuthController authController,
134             @NonNull DumpManager dumpManager,
135             @NonNull AccessibilityManager accessibilityManager,
136             @NonNull ConfigurationController configurationController,
137             @NonNull @Main DelayableExecutor executor,
138             @Nullable Vibrator vibrator
139     ) {
140         super(view);
141         mStatusBarStateController = statusBarStateController;
142         mKeyguardUpdateMonitor = keyguardUpdateMonitor;
143         mAuthController = authController;
144         mKeyguardViewController = keyguardViewController;
145         mKeyguardStateController = keyguardStateController;
146         mFalsingManager = falsingManager;
147         mAccessibilityManager = accessibilityManager;
148         mConfigurationController = configurationController;
149         mExecutor = executor;
150         mVibrator = vibrator;
151 
152         final Context context = view.getContext();
153         mUnlockIcon = mView.getContext().getResources().getDrawable(
154             R.drawable.ic_unlock,
155             mView.getContext().getTheme());
156         mLockIcon = mView.getContext().getResources().getDrawable(
157                 R.anim.lock_to_unlock,
158                 mView.getContext().getTheme());
159         mFpToUnlockIcon = (AnimatedVectorDrawable) mView.getContext().getResources().getDrawable(
160                 R.anim.fp_to_unlock, mView.getContext().getTheme());
161         mLockToUnlockIcon = (AnimatedVectorDrawable) mView.getContext().getResources().getDrawable(
162                 R.anim.lock_to_unlock,
163                 mView.getContext().getTheme());
164         mUnlockedLabel = context.getResources().getString(R.string.accessibility_unlock_button);
165         mLockedLabel = context.getResources().getString(R.string.accessibility_lock_icon);
166         dumpManager.registerDumpable("LockIconViewController", this);
167     }
168 
169     @Override
onInit()170     protected void onInit() {
171         mView.setAccessibilityDelegate(mAccessibilityDelegate);
172     }
173 
174     @Override
onViewAttached()175     protected void onViewAttached() {
176         // we check this here instead of onInit since the FingerprintManager + FaceManager may not
177         // have started up yet onInit
178         mUdfpsSupported = mAuthController.getUdfpsSensorLocation() != null;
179 
180         updateConfiguration();
181         updateKeyguardShowing();
182         mUserUnlockedWithBiometric = false;
183         mIsBouncerShowing = mKeyguardViewController.isBouncerShowing();
184         mIsDozing = mStatusBarStateController.isDozing();
185         mRunningFPS = mKeyguardUpdateMonitor.isFingerprintDetectionRunning();
186         mCanDismissLockScreen = mKeyguardStateController.canDismissLockScreen();
187         mStatusBarState = mStatusBarStateController.getState();
188 
189         updateColors();
190         mConfigurationController.addCallback(mConfigurationListener);
191 
192         mKeyguardUpdateMonitor.registerCallback(mKeyguardUpdateMonitorCallback);
193         mStatusBarStateController.addCallback(mStatusBarStateListener);
194         mKeyguardStateController.addCallback(mKeyguardStateCallback);
195         mDownDetected = false;
196         updateVisibility();
197     }
198 
199     @Override
onViewDetached()200     protected void onViewDetached() {
201         mConfigurationController.removeCallback(mConfigurationListener);
202         mKeyguardUpdateMonitor.removeCallback(mKeyguardUpdateMonitorCallback);
203         mStatusBarStateController.removeCallback(mStatusBarStateListener);
204         mKeyguardStateController.removeCallback(mKeyguardStateCallback);
205 
206         if (mCancelDelayedUpdateVisibilityRunnable != null) {
207             mCancelDelayedUpdateVisibilityRunnable.run();
208             mCancelDelayedUpdateVisibilityRunnable = null;
209         }
210     }
211 
getTop()212     public float getTop() {
213         return mView.getLocationTop();
214     }
215 
216     /**
217      * Set whether qs is expanded. When QS is expanded, don't show a DisabledUdfps affordance.
218      */
setQsExpanded(boolean expanded)219     public void setQsExpanded(boolean expanded) {
220         mQsExpanded = expanded;
221         updateVisibility();
222     }
223 
updateVisibility()224     private void updateVisibility() {
225         if (mCancelDelayedUpdateVisibilityRunnable != null) {
226             mCancelDelayedUpdateVisibilityRunnable.run();
227             mCancelDelayedUpdateVisibilityRunnable = null;
228         }
229 
230         if (!mIsKeyguardShowing) {
231             mView.setVisibility(View.INVISIBLE);
232             return;
233         }
234 
235         boolean wasShowingFpIcon = mUdfpsEnrolled && !mShowUnlockIcon && !mShowLockIcon;
236         boolean wasShowingLockIcon = mShowLockIcon;
237         boolean wasShowingUnlockIcon = mShowUnlockIcon;
238         mShowLockIcon = !mCanDismissLockScreen && !mUserUnlockedWithBiometric && isLockScreen()
239             && (!mUdfpsEnrolled || !mRunningFPS);
240         mShowUnlockIcon = mCanDismissLockScreen && isLockScreen();
241 
242         final CharSequence prevContentDescription = mView.getContentDescription();
243         if (mShowLockIcon) {
244             mView.setImageDrawable(mLockIcon);
245             mView.setVisibility(View.VISIBLE);
246             mView.setContentDescription(mLockedLabel);
247         } else if (mShowUnlockIcon) {
248             if (!wasShowingUnlockIcon) {
249                 if (wasShowingFpIcon) {
250                     mView.setImageDrawable(mFpToUnlockIcon);
251                     mFpToUnlockIcon.forceAnimationOnUI();
252                     mFpToUnlockIcon.start();
253                 } else if (wasShowingLockIcon) {
254                     mView.setImageDrawable(mLockToUnlockIcon);
255                     mLockToUnlockIcon.forceAnimationOnUI();
256                     mLockToUnlockIcon.start();
257                 } else {
258                     mView.setImageDrawable(mUnlockIcon);
259                 }
260             }
261             mView.setVisibility(View.VISIBLE);
262             mView.setContentDescription(mUnlockedLabel);
263         } else {
264             mView.setVisibility(View.INVISIBLE);
265             mView.setContentDescription(null);
266         }
267         if (!Objects.equals(prevContentDescription, mView.getContentDescription())
268                 && mView.getContentDescription() != null) {
269             mView.announceForAccessibility(mView.getContentDescription());
270         }
271     }
272 
273     private final View.AccessibilityDelegate mAccessibilityDelegate =
274             new View.AccessibilityDelegate() {
275         private final AccessibilityNodeInfo.AccessibilityAction mAccessibilityAuthenticateHint =
276                 new AccessibilityNodeInfo.AccessibilityAction(
277                         AccessibilityNodeInfoCompat.ACTION_CLICK,
278                         getResources().getString(R.string.accessibility_authenticate_hint));
279         private final AccessibilityNodeInfo.AccessibilityAction mAccessibilityEnterHint =
280                 new AccessibilityNodeInfo.AccessibilityAction(
281                         AccessibilityNodeInfoCompat.ACTION_CLICK,
282                         getResources().getString(R.string.accessibility_enter_hint));
283         public void onInitializeAccessibilityNodeInfo(View v, AccessibilityNodeInfo info) {
284             super.onInitializeAccessibilityNodeInfo(v, info);
285             if (isClickable()) {
286                 if (mShowLockIcon) {
287                     info.addAction(mAccessibilityAuthenticateHint);
288                 } else if (mShowUnlockIcon) {
289                     info.addAction(mAccessibilityEnterHint);
290                 }
291             }
292         }
293     };
294 
isLockScreen()295     private boolean isLockScreen() {
296         return !mIsDozing
297                 && !mIsBouncerShowing
298                 && !mQsExpanded
299                 && mStatusBarState == StatusBarState.KEYGUARD;
300     }
301 
updateKeyguardShowing()302     private void updateKeyguardShowing() {
303         mIsKeyguardShowing = mKeyguardStateController.isShowing()
304                 && !mKeyguardStateController.isKeyguardGoingAway();
305     }
306 
updateColors()307     private void updateColors() {
308         mView.updateColorAndBackgroundVisibility(mUdfpsSupported);
309     }
310 
updateConfiguration()311     private void updateConfiguration() {
312         final DisplayMetrics metrics = mView.getContext().getResources().getDisplayMetrics();
313         mWidthPixels = metrics.widthPixels;
314         mHeightPixels = metrics.heightPixels;
315         mBottomPadding = mView.getContext().getResources().getDimensionPixelSize(
316                 R.dimen.lock_icon_margin_bottom);
317 
318         mUnlockedLabel = mView.getContext().getResources().getString(
319                 R.string.accessibility_unlock_button);
320         mLockedLabel = mView.getContext()
321                 .getResources().getString(R.string.accessibility_lock_icon);
322 
323         updateLockIconLocation();
324     }
325 
updateLockIconLocation()326     private void updateLockIconLocation() {
327         if (mUdfpsSupported) {
328             FingerprintSensorPropertiesInternal props = mAuthController.getUdfpsProps().get(0);
329             mView.setCenterLocation(new PointF(props.sensorLocationX, props.sensorLocationY),
330                     props.sensorRadius);
331         } else {
332             mView.setCenterLocation(
333                     new PointF(mWidthPixels / 2,
334                         mHeightPixels - mBottomPadding - sDistAboveKgBottomAreaPx
335                             - sLockIconRadiusPx), sLockIconRadiusPx);
336         }
337 
338         mView.getHitRect(mSensorTouchLocation);
339     }
340 
341     @Override
dump(@onNull FileDescriptor fd, @NonNull PrintWriter pw, @NonNull String[] args)342     public void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter pw, @NonNull String[] args) {
343         pw.println("mUdfpsEnrolled: " + mUdfpsEnrolled);
344         pw.println("mIsKeyguardShowing: " + mIsKeyguardShowing);
345         pw.println(" mShowUnlockIcon: " + mShowUnlockIcon);
346         pw.println(" mShowLockIcon: " + mShowLockIcon);
347         pw.println("  mIsDozing: " + mIsDozing);
348         pw.println("  mIsBouncerShowing: " + mIsBouncerShowing);
349         pw.println("  mUserUnlockedWithBiometric: " + mUserUnlockedWithBiometric);
350         pw.println("  mRunningFPS: " + mRunningFPS);
351         pw.println("  mCanDismissLockScreen: " + mCanDismissLockScreen);
352         pw.println("  mStatusBarState: " + StatusBarState.toShortString(mStatusBarState));
353         pw.println("  mQsExpanded: " + mQsExpanded);
354 
355         if (mView != null) {
356             mView.dump(fd, pw, args);
357         }
358     }
359 
360     private StatusBarStateController.StateListener mStatusBarStateListener =
361             new StatusBarStateController.StateListener() {
362                 @Override
363                 public void onDozingChanged(boolean isDozing) {
364                     mIsDozing = isDozing;
365                     updateVisibility();
366                 }
367 
368                 @Override
369                 public void onStateChanged(int statusBarState) {
370                     mStatusBarState = statusBarState;
371                     updateVisibility();
372                 }
373             };
374 
375     private final KeyguardUpdateMonitorCallback mKeyguardUpdateMonitorCallback =
376             new KeyguardUpdateMonitorCallback() {
377                 @Override
378                 public void onKeyguardVisibilityChanged(boolean showing) {
379                     // reset mIsBouncerShowing state in case it was preemptively set
380                     // onAffordanceClick
381                     mIsBouncerShowing = mKeyguardViewController.isBouncerShowing();
382                     updateVisibility();
383                 }
384 
385                 @Override
386                 public void onKeyguardBouncerChanged(boolean bouncer) {
387                     mIsBouncerShowing = bouncer;
388                     updateVisibility();
389                 }
390 
391                 @Override
392                 public void onBiometricRunningStateChanged(boolean running,
393                         BiometricSourceType biometricSourceType) {
394                     mUserUnlockedWithBiometric =
395                             mKeyguardUpdateMonitor.getUserUnlockedWithBiometric(
396                                     KeyguardUpdateMonitor.getCurrentUser());
397 
398                     if (biometricSourceType == FINGERPRINT) {
399                         mRunningFPS = running;
400                         if (!mRunningFPS) {
401                             if (mCancelDelayedUpdateVisibilityRunnable != null) {
402                                 mCancelDelayedUpdateVisibilityRunnable.run();
403                             }
404 
405                             // For some devices, auth is cancelled immediately on screen off but
406                             // before dozing state is set. We want to avoid briefly showing the
407                             // button in this case, so we delay updating the visibility by 50ms.
408                             mCancelDelayedUpdateVisibilityRunnable =
409                                     mExecutor.executeDelayed(() -> updateVisibility(), 50);
410                         } else {
411                             updateVisibility();
412                         }
413                     }
414                 }
415             };
416 
417     private final KeyguardStateController.Callback mKeyguardStateCallback =
418             new KeyguardStateController.Callback() {
419         @Override
420         public void onUnlockedChanged() {
421             mCanDismissLockScreen = mKeyguardStateController.canDismissLockScreen();
422             updateKeyguardShowing();
423             updateVisibility();
424         }
425 
426         @Override
427         public void onKeyguardShowingChanged() {
428             // Reset values in case biometrics were removed (ie: pin/pattern/password => swipe).
429             // If biometrics were removed, local vars mCanDismissLockScreen and
430             // mUserUnlockedWithBiometric may not be updated.
431             mCanDismissLockScreen = mKeyguardStateController.canDismissLockScreen();
432             updateKeyguardShowing();
433             if (mIsKeyguardShowing) {
434                 mUserUnlockedWithBiometric =
435                     mKeyguardUpdateMonitor.getUserUnlockedWithBiometric(
436                         KeyguardUpdateMonitor.getCurrentUser());
437             }
438             mUdfpsEnrolled = mKeyguardUpdateMonitor.isUdfpsEnrolled();
439             updateVisibility();
440         }
441 
442         @Override
443         public void onKeyguardFadingAwayChanged() {
444             updateKeyguardShowing();
445             updateVisibility();
446         }
447     };
448 
449     private final ConfigurationController.ConfigurationListener mConfigurationListener =
450             new ConfigurationController.ConfigurationListener() {
451         @Override
452         public void onUiModeChanged() {
453             updateColors();
454         }
455 
456         @Override
457         public void onThemeChanged() {
458             updateColors();
459         }
460 
461         @Override
462         public void onOverlayChanged() {
463             updateColors();
464         }
465 
466         @Override
467         public void onConfigChanged(Configuration newConfig) {
468             updateConfiguration();
469             updateColors();
470         }
471     };
472 
473     private final GestureDetector mGestureDetector =
474             new GestureDetector(new SimpleOnGestureListener() {
475                 public boolean onDown(MotionEvent e) {
476                     mDetectedLongPress = false;
477                     if (!isClickable()) {
478                         mDownDetected = false;
479                         return false;
480                     }
481 
482                     // intercept all following touches until we see MotionEvent.ACTION_CANCEL UP or
483                     // MotionEvent.ACTION_UP (see #onTouchEvent)
484                     mDownDetected = true;
485                     if (mVibrator != null) {
486                         mVibrator.vibrate(
487                                 Process.myUid(),
488                                 getContext().getOpPackageName(),
489                                 UdfpsController.EFFECT_CLICK,
490                                 "lockIcon-onDown",
491                                 VIBRATION_SONIFICATION_ATTRIBUTES);
492                     }
493                     return true;
494                 }
495 
496                 public void onLongPress(MotionEvent e) {
497                     if (!wasClickableOnDownEvent()) {
498                         return;
499                     }
500 
501                     if (mVibrator != null) {
502                         mVibrator.vibrate(
503                                 Process.myUid(),
504                                 getContext().getOpPackageName(),
505                                 UdfpsController.EFFECT_CLICK,
506                                 "lockIcon-onLongPress",
507                                 VIBRATION_SONIFICATION_ATTRIBUTES);
508                     }
509                     mDetectedLongPress = true;
510                     onAffordanceClick();
511                 }
512 
513                 public boolean onSingleTapUp(MotionEvent e) {
514                     if (!wasClickableOnDownEvent()) {
515                         return false;
516                     }
517                     onAffordanceClick();
518                     return true;
519                 }
520 
521                 public boolean onFling(MotionEvent e1, MotionEvent e2,
522                         float velocityX, float velocityY) {
523                     if (!wasClickableOnDownEvent()) {
524                         return false;
525                     }
526                     onAffordanceClick();
527                     return true;
528                 }
529 
530                 private boolean wasClickableOnDownEvent() {
531                     return mDownDetected;
532                 }
533 
534                 private void onAffordanceClick() {
535                     if (mFalsingManager.isFalseTouch(LOCK_ICON)) {
536                         return;
537                     }
538 
539                     // pre-emptively set to true to hide view
540                     mIsBouncerShowing = true;
541                     updateVisibility();
542                     mKeyguardViewController.showBouncer(/* scrim */ true);
543                 }
544             });
545 
546     /**
547      * Send touch events to this view and handles it if the touch is within this view and we are
548      * in a 'clickable' state
549      * @return whether to intercept the touch event
550      */
onTouchEvent(MotionEvent event)551     public boolean onTouchEvent(MotionEvent event) {
552         if (mSensorTouchLocation.contains((int) event.getX(), (int) event.getY())
553                 && mView.getVisibility() == View.VISIBLE) {
554             mGestureDetector.onTouchEvent(event);
555         }
556 
557         // we continue to intercept all following touches until we see MotionEvent.ACTION_CANCEL UP
558         // or MotionEvent.ACTION_UP. this is to avoid passing the touch to NPV
559         // after the lock icon disappears on device entry
560         if (mDownDetected && mDetectedLongPress) {
561             if (event.getAction() == MotionEvent.ACTION_CANCEL
562                     || event.getAction() == MotionEvent.ACTION_UP) {
563                 mDownDetected = false;
564             }
565             return true;
566         }
567         return false;
568     }
569 
isClickable()570     private boolean isClickable() {
571         return mUdfpsSupported || mShowUnlockIcon;
572     }
573 
574     /**
575      * Set the alpha of this view.
576      */
setAlpha(float alpha)577     public void setAlpha(float alpha) {
578         mView.setAlpha(alpha);
579     }
580 }
581