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.keyguard.LockIconView.ICON_FINGERPRINT;
22 import static com.android.keyguard.LockIconView.ICON_LOCK;
23 import static com.android.keyguard.LockIconView.ICON_UNLOCK;
24 import static com.android.systemui.classifier.Classifier.LOCK_ICON;
25 import static com.android.systemui.doze.util.BurnInHelperKt.getBurnInOffset;
26 import static com.android.systemui.doze.util.BurnInHelperKt.getBurnInProgressOffset;
27 
28 import android.content.res.Configuration;
29 import android.content.res.Resources;
30 import android.graphics.PointF;
31 import android.graphics.Rect;
32 import android.graphics.drawable.AnimatedStateListDrawable;
33 import android.hardware.biometrics.BiometricSourceType;
34 import android.hardware.biometrics.SensorLocationInternal;
35 import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
36 import android.media.AudioAttributes;
37 import android.os.Process;
38 import android.os.Vibrator;
39 import android.util.DisplayMetrics;
40 import android.util.Log;
41 import android.util.MathUtils;
42 import android.view.MotionEvent;
43 import android.view.VelocityTracker;
44 import android.view.View;
45 import android.view.WindowManager;
46 import android.view.accessibility.AccessibilityManager;
47 import android.view.accessibility.AccessibilityNodeInfo;
48 
49 import androidx.annotation.NonNull;
50 import androidx.annotation.Nullable;
51 import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
52 
53 import com.android.systemui.Dumpable;
54 import com.android.systemui.R;
55 import com.android.systemui.biometrics.AuthController;
56 import com.android.systemui.biometrics.AuthRippleController;
57 import com.android.systemui.biometrics.UdfpsController;
58 import com.android.systemui.dagger.qualifiers.Main;
59 import com.android.systemui.dump.DumpManager;
60 import com.android.systemui.plugins.FalsingManager;
61 import com.android.systemui.plugins.statusbar.StatusBarStateController;
62 import com.android.systemui.statusbar.StatusBarState;
63 import com.android.systemui.statusbar.phone.dagger.StatusBarComponent;
64 import com.android.systemui.statusbar.policy.ConfigurationController;
65 import com.android.systemui.statusbar.policy.KeyguardStateController;
66 import com.android.systemui.util.ViewController;
67 import com.android.systemui.util.concurrency.DelayableExecutor;
68 
69 import java.io.FileDescriptor;
70 import java.io.PrintWriter;
71 import java.util.Objects;
72 
73 import javax.inject.Inject;
74 
75 /**
76  * Controls when to show the LockIcon affordance (lock/unlocked icon or circle) on lock screen.
77  *
78  * For devices with UDFPS, the lock icon will show at the sensor location. Else, the lock
79  * icon will show a set distance from the bottom of the device.
80  */
81 @StatusBarComponent.StatusBarScope
82 public class LockIconViewController extends ViewController<LockIconView> implements Dumpable {
83     private static final String TAG = "LockIconViewController";
84     private static final float sDefaultDensity =
85             (float) DisplayMetrics.DENSITY_DEVICE_STABLE / (float) DisplayMetrics.DENSITY_DEFAULT;
86     private static final int sLockIconRadiusPx = (int) (sDefaultDensity * 36);
87     private static final AudioAttributes VIBRATION_SONIFICATION_ATTRIBUTES =
88             new AudioAttributes.Builder()
89                 .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
90                 .setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION)
91                 .build();
92     private static final long LONG_PRESS_TIMEOUT = 200L; // milliseconds
93 
94     @NonNull private final KeyguardUpdateMonitor mKeyguardUpdateMonitor;
95     @NonNull private final KeyguardViewController mKeyguardViewController;
96     @NonNull private final StatusBarStateController mStatusBarStateController;
97     @NonNull private final KeyguardStateController mKeyguardStateController;
98     @NonNull private final FalsingManager mFalsingManager;
99     @NonNull private final AuthController mAuthController;
100     @NonNull private final AccessibilityManager mAccessibilityManager;
101     @NonNull private final ConfigurationController mConfigurationController;
102     @NonNull private final DelayableExecutor mExecutor;
103     private boolean mUdfpsEnrolled;
104 
105     @NonNull private final AnimatedStateListDrawable mIcon;
106 
107     @NonNull private CharSequence mUnlockedLabel;
108     @NonNull private CharSequence mLockedLabel;
109     @Nullable private final Vibrator mVibrator;
110     @Nullable private final AuthRippleController mAuthRippleController;
111 
112     // Tracks the velocity of a touch to help filter out the touches that move too fast.
113     private VelocityTracker mVelocityTracker;
114     // The ID of the pointer for which ACTION_DOWN has occurred. -1 means no pointer is active.
115     private int mActivePointerId = -1;
116 
117     private boolean mIsDozing;
118     private boolean mIsBouncerShowing;
119     private boolean mRunningFPS;
120     private boolean mCanDismissLockScreen;
121     private boolean mQsExpanded;
122     private int mStatusBarState;
123     private boolean mIsKeyguardShowing;
124     private boolean mUserUnlockedWithBiometric;
125     private Runnable mCancelDelayedUpdateVisibilityRunnable;
126     private Runnable mOnGestureDetectedRunnable;
127     private Runnable mLongPressCancelRunnable;
128 
129     private boolean mUdfpsSupported;
130     private float mHeightPixels;
131     private float mWidthPixels;
132     private int mBottomPaddingPx;
133 
134     private boolean mShowUnlockIcon;
135     private boolean mShowLockIcon;
136 
137     // for udfps when strong auth is required or unlocked on AOD
138     private boolean mShowAodLockIcon;
139     private boolean mShowAodUnlockedIcon;
140     private final int mMaxBurnInOffsetX;
141     private final int mMaxBurnInOffsetY;
142     private float mInterpolatedDarkAmount;
143 
144     private boolean mDownDetected;
145     private final Rect mSensorTouchLocation = new Rect();
146 
147     @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, @Nullable AuthRippleController authRippleController, @NonNull @Main Resources resources )148     public LockIconViewController(
149             @Nullable LockIconView view,
150             @NonNull StatusBarStateController statusBarStateController,
151             @NonNull KeyguardUpdateMonitor keyguardUpdateMonitor,
152             @NonNull KeyguardViewController keyguardViewController,
153             @NonNull KeyguardStateController keyguardStateController,
154             @NonNull FalsingManager falsingManager,
155             @NonNull AuthController authController,
156             @NonNull DumpManager dumpManager,
157             @NonNull AccessibilityManager accessibilityManager,
158             @NonNull ConfigurationController configurationController,
159             @NonNull @Main DelayableExecutor executor,
160             @Nullable Vibrator vibrator,
161             @Nullable AuthRippleController authRippleController,
162             @NonNull @Main Resources resources
163     ) {
164         super(view);
165         mStatusBarStateController = statusBarStateController;
166         mKeyguardUpdateMonitor = keyguardUpdateMonitor;
167         mAuthController = authController;
168         mKeyguardViewController = keyguardViewController;
169         mKeyguardStateController = keyguardStateController;
170         mFalsingManager = falsingManager;
171         mAccessibilityManager = accessibilityManager;
172         mConfigurationController = configurationController;
173         mExecutor = executor;
174         mVibrator = vibrator;
175         mAuthRippleController = authRippleController;
176 
177         mMaxBurnInOffsetX = resources.getDimensionPixelSize(R.dimen.udfps_burn_in_offset_x);
178         mMaxBurnInOffsetY = resources.getDimensionPixelSize(R.dimen.udfps_burn_in_offset_y);
179 
180         mIcon = (AnimatedStateListDrawable)
181                 resources.getDrawable(R.drawable.super_lock_icon, mView.getContext().getTheme());
182         mView.setImageDrawable(mIcon);
183         mUnlockedLabel = resources.getString(R.string.accessibility_unlock_button);
184         mLockedLabel = resources.getString(R.string.accessibility_lock_icon);
185         dumpManager.registerDumpable(TAG, this);
186     }
187 
188     @Override
onInit()189     protected void onInit() {
190         mView.setAccessibilityDelegate(mAccessibilityDelegate);
191     }
192 
193     @Override
onViewAttached()194     protected void onViewAttached() {
195         updateIsUdfpsEnrolled();
196         updateConfiguration();
197         updateKeyguardShowing();
198         mUserUnlockedWithBiometric = false;
199 
200         mIsBouncerShowing = mKeyguardViewController.isBouncerShowing();
201         mIsDozing = mStatusBarStateController.isDozing();
202         mInterpolatedDarkAmount = mStatusBarStateController.getDozeAmount();
203         mRunningFPS = mKeyguardUpdateMonitor.isFingerprintDetectionRunning();
204         mCanDismissLockScreen = mKeyguardStateController.canDismissLockScreen();
205         mStatusBarState = mStatusBarStateController.getState();
206 
207         updateColors();
208         mConfigurationController.addCallback(mConfigurationListener);
209 
210         mAuthController.addCallback(mAuthControllerCallback);
211         mKeyguardUpdateMonitor.registerCallback(mKeyguardUpdateMonitorCallback);
212         mStatusBarStateController.addCallback(mStatusBarStateListener);
213         mKeyguardStateController.addCallback(mKeyguardStateCallback);
214         mDownDetected = false;
215         updateBurnInOffsets();
216         updateVisibility();
217     }
218 
219     @Override
onViewDetached()220     protected void onViewDetached() {
221         mAuthController.removeCallback(mAuthControllerCallback);
222         mConfigurationController.removeCallback(mConfigurationListener);
223         mKeyguardUpdateMonitor.removeCallback(mKeyguardUpdateMonitorCallback);
224         mStatusBarStateController.removeCallback(mStatusBarStateListener);
225         mKeyguardStateController.removeCallback(mKeyguardStateCallback);
226 
227         if (mCancelDelayedUpdateVisibilityRunnable != null) {
228             mCancelDelayedUpdateVisibilityRunnable.run();
229             mCancelDelayedUpdateVisibilityRunnable = null;
230         }
231     }
232 
getTop()233     public float getTop() {
234         return mView.getLocationTop();
235     }
236 
237     /**
238      * Set whether qs is expanded. When QS is expanded, don't show a DisabledUdfps affordance.
239      */
setQsExpanded(boolean expanded)240     public void setQsExpanded(boolean expanded) {
241         mQsExpanded = expanded;
242         updateVisibility();
243     }
244 
updateVisibility()245     private void updateVisibility() {
246         if (mCancelDelayedUpdateVisibilityRunnable != null) {
247             mCancelDelayedUpdateVisibilityRunnable.run();
248             mCancelDelayedUpdateVisibilityRunnable = null;
249         }
250 
251         if (!mIsKeyguardShowing && !mIsDozing) {
252             mView.setVisibility(View.INVISIBLE);
253             return;
254         }
255 
256         boolean wasShowingUnlock = mShowUnlockIcon;
257         boolean wasShowingFpIcon = mUdfpsEnrolled && !mShowUnlockIcon && !mShowLockIcon
258                 && !mShowAodUnlockedIcon && !mShowAodLockIcon;
259         mShowLockIcon = !mCanDismissLockScreen && !mUserUnlockedWithBiometric && isLockScreen()
260                 && (!mUdfpsEnrolled || !mRunningFPS);
261         mShowUnlockIcon = (mCanDismissLockScreen || mUserUnlockedWithBiometric) && isLockScreen();
262         mShowAodUnlockedIcon = mIsDozing && mUdfpsEnrolled && !mRunningFPS && mCanDismissLockScreen;
263         mShowAodLockIcon = mIsDozing && mUdfpsEnrolled && !mRunningFPS && !mCanDismissLockScreen;
264 
265         final CharSequence prevContentDescription = mView.getContentDescription();
266         if (mShowLockIcon) {
267             mView.updateIcon(ICON_LOCK, false);
268             mView.setContentDescription(mLockedLabel);
269             mView.setVisibility(View.VISIBLE);
270         } else if (mShowUnlockIcon) {
271             if (wasShowingFpIcon) {
272                 // fp icon was shown by UdfpsView, and now we still want to animate the transition
273                 // in this drawable
274                 mView.updateIcon(ICON_FINGERPRINT, false);
275             }
276             mView.updateIcon(ICON_UNLOCK, false);
277             mView.setContentDescription(mUnlockedLabel);
278             mView.setVisibility(View.VISIBLE);
279         } else if (mShowAodUnlockedIcon) {
280             mView.updateIcon(ICON_UNLOCK, true);
281             mView.setContentDescription(mUnlockedLabel);
282             mView.setVisibility(View.VISIBLE);
283         } else if (mShowAodLockIcon) {
284             mView.updateIcon(ICON_LOCK, true);
285             mView.setContentDescription(mLockedLabel);
286             mView.setVisibility(View.VISIBLE);
287         } else {
288             mView.clearIcon();
289             mView.setVisibility(View.INVISIBLE);
290             mView.setContentDescription(null);
291         }
292 
293         if (!Objects.equals(prevContentDescription, mView.getContentDescription())
294                 && mView.getContentDescription() != null) {
295             mView.announceForAccessibility(mView.getContentDescription());
296         }
297     }
298 
299     private final View.AccessibilityDelegate mAccessibilityDelegate =
300             new View.AccessibilityDelegate() {
301         private final AccessibilityNodeInfo.AccessibilityAction mAccessibilityAuthenticateHint =
302                 new AccessibilityNodeInfo.AccessibilityAction(
303                         AccessibilityNodeInfoCompat.ACTION_CLICK,
304                         getResources().getString(R.string.accessibility_authenticate_hint));
305         private final AccessibilityNodeInfo.AccessibilityAction mAccessibilityEnterHint =
306                 new AccessibilityNodeInfo.AccessibilityAction(
307                         AccessibilityNodeInfoCompat.ACTION_CLICK,
308                         getResources().getString(R.string.accessibility_enter_hint));
309         public void onInitializeAccessibilityNodeInfo(View v, AccessibilityNodeInfo info) {
310             super.onInitializeAccessibilityNodeInfo(v, info);
311             if (isActionable()) {
312                 if (mShowLockIcon) {
313                     info.addAction(mAccessibilityAuthenticateHint);
314                 } else if (mShowUnlockIcon) {
315                     info.addAction(mAccessibilityEnterHint);
316                 }
317             }
318         }
319     };
320 
isLockScreen()321     private boolean isLockScreen() {
322         return !mIsDozing
323                 && !mIsBouncerShowing
324                 && !mQsExpanded
325                 && mStatusBarState == StatusBarState.KEYGUARD;
326     }
327 
updateKeyguardShowing()328     private void updateKeyguardShowing() {
329         mIsKeyguardShowing = mKeyguardStateController.isShowing()
330                 && !mKeyguardStateController.isKeyguardGoingAway();
331     }
332 
updateColors()333     private void updateColors() {
334         mView.updateColorAndBackgroundVisibility();
335     }
336 
updateConfiguration()337     private void updateConfiguration() {
338         WindowManager windowManager = getContext().getSystemService(WindowManager.class);
339         Rect bounds = windowManager.getCurrentWindowMetrics().getBounds();
340         mWidthPixels = bounds.right;
341         mHeightPixels = bounds.bottom;
342         mBottomPaddingPx = getResources().getDimensionPixelSize(R.dimen.lock_icon_margin_bottom);
343 
344         mUnlockedLabel = mView.getContext().getResources().getString(
345                 R.string.accessibility_unlock_button);
346         mLockedLabel = mView.getContext()
347                 .getResources().getString(R.string.accessibility_lock_icon);
348 
349         updateLockIconLocation();
350     }
351 
updateLockIconLocation()352     private void updateLockIconLocation() {
353         if (mUdfpsSupported) {
354             FingerprintSensorPropertiesInternal props = mAuthController.getUdfpsProps().get(0);
355             final SensorLocationInternal location = props.getLocation();
356             mView.setCenterLocation(new PointF(location.sensorLocationX, location.sensorLocationY),
357                     location.sensorRadius);
358         } else {
359             mView.setCenterLocation(
360                     new PointF(mWidthPixels / 2,
361                         mHeightPixels - mBottomPaddingPx - sLockIconRadiusPx),
362                         sLockIconRadiusPx);
363         }
364 
365         mView.getHitRect(mSensorTouchLocation);
366     }
367 
368     @Override
dump(@onNull FileDescriptor fd, @NonNull PrintWriter pw, @NonNull String[] args)369     public void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter pw, @NonNull String[] args) {
370         pw.println("mUdfpsSupported: " + mUdfpsSupported);
371         pw.println("mUdfpsEnrolled: " + mUdfpsEnrolled);
372         pw.println("mIsKeyguardShowing: " + mIsKeyguardShowing);
373         pw.println(" mIcon: ");
374         for (int state : mIcon.getState()) {
375             pw.print(" " + state);
376         }
377         pw.println();
378         pw.println(" mShowUnlockIcon: " + mShowUnlockIcon);
379         pw.println(" mShowLockIcon: " + mShowLockIcon);
380         pw.println(" mShowAodUnlockedIcon: " + mShowAodUnlockedIcon);
381         pw.println("  mIsDozing: " + mIsDozing);
382         pw.println("  mIsBouncerShowing: " + mIsBouncerShowing);
383         pw.println("  mUserUnlockedWithBiometric: " + mUserUnlockedWithBiometric);
384         pw.println("  mRunningFPS: " + mRunningFPS);
385         pw.println("  mCanDismissLockScreen: " + mCanDismissLockScreen);
386         pw.println("  mStatusBarState: " + StatusBarState.toShortString(mStatusBarState));
387         pw.println("  mQsExpanded: " + mQsExpanded);
388         pw.println("  mInterpolatedDarkAmount: " + mInterpolatedDarkAmount);
389 
390         if (mView != null) {
391             mView.dump(fd, pw, args);
392         }
393     }
394 
395     /** Every minute, update the aod icon's burn in offset */
dozeTimeTick()396     public void dozeTimeTick() {
397         updateBurnInOffsets();
398     }
399 
updateBurnInOffsets()400     private void updateBurnInOffsets() {
401         float offsetX = MathUtils.lerp(0f,
402                 getBurnInOffset(mMaxBurnInOffsetX * 2, true /* xAxis */)
403                         - mMaxBurnInOffsetX, mInterpolatedDarkAmount);
404         float offsetY = MathUtils.lerp(0f,
405                 getBurnInOffset(mMaxBurnInOffsetY * 2, false /* xAxis */)
406                         - mMaxBurnInOffsetY, mInterpolatedDarkAmount);
407         float progress = MathUtils.lerp(0f, getBurnInProgressOffset(), mInterpolatedDarkAmount);
408 
409         mView.setTranslationX(offsetX);
410         mView.setTranslationY(offsetY);
411     }
412 
updateIsUdfpsEnrolled()413     private void updateIsUdfpsEnrolled() {
414         boolean wasUdfpsSupported = mUdfpsSupported;
415         boolean wasUdfpsEnrolled = mUdfpsEnrolled;
416 
417         mUdfpsSupported = mKeyguardUpdateMonitor.isUdfpsSupported();
418         mView.setUseBackground(mUdfpsSupported);
419 
420         mUdfpsEnrolled = mKeyguardUpdateMonitor.isUdfpsEnrolled();
421         if (wasUdfpsSupported != mUdfpsSupported || wasUdfpsEnrolled != mUdfpsEnrolled) {
422             updateVisibility();
423         }
424     }
425 
426     private StatusBarStateController.StateListener mStatusBarStateListener =
427             new StatusBarStateController.StateListener() {
428                 @Override
429                 public void onDozeAmountChanged(float linear, float eased) {
430                     mInterpolatedDarkAmount = eased;
431                     mView.setDozeAmount(eased);
432                     updateBurnInOffsets();
433                 }
434 
435                 @Override
436                 public void onDozingChanged(boolean isDozing) {
437                     mIsDozing = isDozing;
438                     updateBurnInOffsets();
439                     updateIsUdfpsEnrolled();
440                     updateVisibility();
441                 }
442 
443                 @Override
444                 public void onStateChanged(int statusBarState) {
445                     mStatusBarState = statusBarState;
446                     updateVisibility();
447                 }
448             };
449 
450     private final KeyguardUpdateMonitorCallback mKeyguardUpdateMonitorCallback =
451             new KeyguardUpdateMonitorCallback() {
452                 @Override
453                 public void onKeyguardVisibilityChanged(boolean showing) {
454                     // reset mIsBouncerShowing state in case it was preemptively set
455                     // onLongPress
456                     mIsBouncerShowing = mKeyguardViewController.isBouncerShowing();
457                     updateVisibility();
458                 }
459 
460                 @Override
461                 public void onKeyguardBouncerChanged(boolean bouncer) {
462                     mIsBouncerShowing = bouncer;
463                     updateVisibility();
464                 }
465 
466                 @Override
467                 public void onBiometricRunningStateChanged(boolean running,
468                         BiometricSourceType biometricSourceType) {
469                     final boolean wasRunningFps = mRunningFPS;
470                     final boolean wasUserUnlockedWithBiometric = mUserUnlockedWithBiometric;
471                     mUserUnlockedWithBiometric =
472                             mKeyguardUpdateMonitor.getUserUnlockedWithBiometric(
473                                     KeyguardUpdateMonitor.getCurrentUser());
474 
475                     if (biometricSourceType == FINGERPRINT) {
476                         mRunningFPS = running;
477                         if (wasRunningFps && !mRunningFPS) {
478                             if (mCancelDelayedUpdateVisibilityRunnable != null) {
479                                 mCancelDelayedUpdateVisibilityRunnable.run();
480                             }
481 
482                             // For some devices, auth is cancelled immediately on screen off but
483                             // before dozing state is set. We want to avoid briefly showing the
484                             // button in this case, so we delay updating the visibility by 50ms.
485                             mCancelDelayedUpdateVisibilityRunnable =
486                                     mExecutor.executeDelayed(() -> updateVisibility(), 50);
487                             return;
488                         }
489                     }
490 
491                     if (wasUserUnlockedWithBiometric != mUserUnlockedWithBiometric
492                             || wasRunningFps != mRunningFPS) {
493                         updateVisibility();
494                     }
495                 }
496             };
497 
498     private final KeyguardStateController.Callback mKeyguardStateCallback =
499             new KeyguardStateController.Callback() {
500         @Override
501         public void onUnlockedChanged() {
502             mCanDismissLockScreen = mKeyguardStateController.canDismissLockScreen();
503             updateKeyguardShowing();
504             updateVisibility();
505         }
506 
507         @Override
508         public void onKeyguardShowingChanged() {
509             // Reset values in case biometrics were removed (ie: pin/pattern/password => swipe).
510             // If biometrics were removed, local vars mCanDismissLockScreen and
511             // mUserUnlockedWithBiometric may not be updated.
512             mCanDismissLockScreen = mKeyguardStateController.canDismissLockScreen();
513             updateKeyguardShowing();
514             if (mIsKeyguardShowing) {
515                 mUserUnlockedWithBiometric =
516                     mKeyguardUpdateMonitor.getUserUnlockedWithBiometric(
517                         KeyguardUpdateMonitor.getCurrentUser());
518             }
519             updateIsUdfpsEnrolled();
520             updateVisibility();
521         }
522 
523         @Override
524         public void onKeyguardFadingAwayChanged() {
525             updateKeyguardShowing();
526             updateVisibility();
527         }
528     };
529 
530     private final ConfigurationController.ConfigurationListener mConfigurationListener =
531             new ConfigurationController.ConfigurationListener() {
532         @Override
533         public void onUiModeChanged() {
534             updateColors();
535         }
536 
537         @Override
538         public void onThemeChanged() {
539             updateColors();
540         }
541 
542         @Override
543         public void onConfigChanged(Configuration newConfig) {
544             updateConfiguration();
545             updateColors();
546         }
547     };
548 
549     /**
550      * Handles the touch if it is within the lock icon view and {@link #isActionable()} is true.
551      * Subsequently, will trigger {@link #onLongPress()} if a touch is continuously in the lock icon
552      * area for {@link #LONG_PRESS_TIMEOUT} ms.
553      *
554      * Touch speed debouncing mimics logic from the velocity tracker in {@link UdfpsController}.
555      */
onTouchEvent(MotionEvent event, Runnable onGestureDetectedRunnable)556     public boolean onTouchEvent(MotionEvent event, Runnable onGestureDetectedRunnable) {
557         if (!onInterceptTouchEvent(event)) {
558             cancelTouches();
559             return false;
560         }
561 
562         mOnGestureDetectedRunnable = onGestureDetectedRunnable;
563         switch(event.getActionMasked()) {
564             case MotionEvent.ACTION_DOWN:
565             case MotionEvent.ACTION_HOVER_ENTER:
566                 if (mVibrator != null && !mDownDetected) {
567                     mVibrator.vibrate(
568                             Process.myUid(),
569                             getContext().getOpPackageName(),
570                             UdfpsController.EFFECT_CLICK,
571                             "lock-icon-down",
572                             VIBRATION_SONIFICATION_ATTRIBUTES);
573                 }
574 
575                 // The pointer that causes ACTION_DOWN is always at index 0.
576                 // We need to persist its ID to track it during ACTION_MOVE that could include
577                 // data for many other pointers because of multi-touch support.
578                 mActivePointerId = event.getPointerId(0);
579                 if (mVelocityTracker == null) {
580                     // To simplify the lifecycle of the velocity tracker, make sure it's never null
581                     // after ACTION_DOWN, and always null after ACTION_CANCEL or ACTION_UP.
582                     mVelocityTracker = VelocityTracker.obtain();
583                 } else {
584                     // ACTION_UP or ACTION_CANCEL is not guaranteed to be called before a new
585                     // ACTION_DOWN, in that case we should just reuse the old instance.
586                     mVelocityTracker.clear();
587                 }
588                 mVelocityTracker.addMovement(event);
589 
590                 mDownDetected = true;
591                 mLongPressCancelRunnable = mExecutor.executeDelayed(
592                         this::onLongPress, LONG_PRESS_TIMEOUT);
593                 break;
594             case MotionEvent.ACTION_MOVE:
595             case MotionEvent.ACTION_HOVER_MOVE:
596                 mVelocityTracker.addMovement(event);
597                 // Compute pointer velocity in pixels per second.
598                 mVelocityTracker.computeCurrentVelocity(1000);
599                 float velocity = UdfpsController.computePointerSpeed(mVelocityTracker,
600                         mActivePointerId);
601                 if (event.getClassification() != MotionEvent.CLASSIFICATION_DEEP_PRESS
602                         && UdfpsController.exceedsVelocityThreshold(velocity)) {
603                     Log.v(TAG, "lock icon long-press rescheduled due to "
604                             + "high pointer velocity=" + velocity);
605                     mLongPressCancelRunnable.run();
606                     mLongPressCancelRunnable = mExecutor.executeDelayed(
607                             this::onLongPress, LONG_PRESS_TIMEOUT);
608                 }
609                 break;
610             case MotionEvent.ACTION_UP:
611             case MotionEvent.ACTION_CANCEL:
612             case MotionEvent.ACTION_HOVER_EXIT:
613                 cancelTouches();
614                 break;
615         }
616 
617         return true;
618     }
619 
620     /**
621      * Intercepts the touch if the onDown event and current event are within this lock icon view's
622      * bounds.
623      */
onInterceptTouchEvent(MotionEvent event)624     public boolean onInterceptTouchEvent(MotionEvent event) {
625         if (!inLockIconArea(event) || !isActionable()) {
626             return false;
627         }
628 
629         if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
630             return true;
631         }
632 
633         return mDownDetected;
634     }
635 
onLongPress()636     private void onLongPress() {
637         cancelTouches();
638         if (mFalsingManager.isFalseTouch(LOCK_ICON)) {
639             Log.v(TAG, "lock icon long-press rejected by the falsing manager.");
640             return;
641         }
642 
643         // pre-emptively set to true to hide view
644         mIsBouncerShowing = true;
645         if (mUdfpsSupported && mShowUnlockIcon && mAuthRippleController != null) {
646             mAuthRippleController.showRipple(FINGERPRINT);
647         }
648         updateVisibility();
649         if (mOnGestureDetectedRunnable != null) {
650             mOnGestureDetectedRunnable.run();
651         }
652 
653         if (mVibrator != null) {
654             // play device entry haptic (same as biometric success haptic)
655             mVibrator.vibrate(
656                     Process.myUid(),
657                     getContext().getOpPackageName(),
658                     UdfpsController.EFFECT_CLICK,
659                     "lock-icon-device-entry",
660                     VIBRATION_SONIFICATION_ATTRIBUTES);
661         }
662 
663         mKeyguardViewController.showBouncer(/* scrim */ true);
664     }
665 
666 
cancelTouches()667     private void cancelTouches() {
668         mDownDetected = false;
669         if (mLongPressCancelRunnable != null) {
670             mLongPressCancelRunnable.run();
671         }
672         if (mVelocityTracker != null) {
673             mVelocityTracker.recycle();
674             mVelocityTracker = null;
675         }
676         if (mVibrator != null) {
677             mVibrator.cancel();
678         }
679     }
680 
681 
inLockIconArea(MotionEvent event)682     private boolean inLockIconArea(MotionEvent event) {
683         return mSensorTouchLocation.contains((int) event.getX(), (int) event.getY())
684                 && mView.getVisibility() == View.VISIBLE;
685     }
686 
isActionable()687     private boolean isActionable() {
688         return mUdfpsSupported || mShowUnlockIcon;
689     }
690 
691     /**
692      * Set the alpha of this view.
693      */
setAlpha(float alpha)694     public void setAlpha(float alpha) {
695         mView.setAlpha(alpha);
696     }
697 
698     private final AuthController.Callback mAuthControllerCallback = new AuthController.Callback() {
699         @Override
700         public void onAllAuthenticatorsRegistered() {
701             updateUdfpsConfig();
702         }
703 
704         @Override
705         public void onEnrollmentsChanged() {
706             updateUdfpsConfig();
707         }
708     };
709 
updateUdfpsConfig()710     private void updateUdfpsConfig() {
711         // must be called from the main thread since it may update the views
712         mExecutor.execute(() -> {
713             updateIsUdfpsEnrolled();
714             updateConfiguration();
715         });
716     }
717 }
718