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.systemui.biometrics;
18 
19 import android.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.content.Context;
22 import android.content.res.TypedArray;
23 import android.graphics.Canvas;
24 import android.graphics.Color;
25 import android.graphics.Paint;
26 import android.graphics.PointF;
27 import android.graphics.RectF;
28 import android.hardware.biometrics.SensorLocationInternal;
29 import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
30 import android.os.Build;
31 import android.os.UserHandle;
32 import android.provider.Settings;
33 import android.text.TextUtils;
34 import android.util.AttributeSet;
35 import android.util.Log;
36 import android.view.MotionEvent;
37 import android.view.Surface;
38 import android.view.View;
39 import android.widget.FrameLayout;
40 
41 import com.android.systemui.R;
42 import com.android.systemui.biometrics.UdfpsHbmTypes.HbmType;
43 import com.android.systemui.doze.DozeReceiver;
44 
45 /**
46  * A view containing 1) A SurfaceView for HBM, and 2) A normal drawable view for all other
47  * animations.
48  */
49 public class UdfpsView extends FrameLayout implements DozeReceiver, UdfpsIlluminator {
50     private static final String TAG = "UdfpsView";
51 
52     private static final String SETTING_HBM_TYPE =
53             "com.android.systemui.biometrics.UdfpsSurfaceView.hbmType";
54     private static final @HbmType int DEFAULT_HBM_TYPE = UdfpsHbmTypes.LOCAL_HBM;
55 
56     private static final int DEBUG_TEXT_SIZE_PX = 32;
57 
58     @NonNull private final RectF mSensorRect;
59     @NonNull private final Paint mDebugTextPaint;
60     private final float mSensorTouchAreaCoefficient;
61     private final int mOnIlluminatedDelayMs;
62     private final @HbmType int mHbmType;
63 
64     // Only used for UdfpsHbmTypes.GLOBAL_HBM.
65     @Nullable private UdfpsSurfaceView mGhbmView;
66     // Can be different for enrollment, BiometricPrompt, Keyguard, etc.
67     @Nullable private UdfpsAnimationViewController mAnimationViewController;
68     // Used to obtain the sensor location.
69     @NonNull private FingerprintSensorPropertiesInternal mSensorProps;
70     @Nullable private UdfpsHbmProvider mHbmProvider;
71     @Nullable private String mDebugMessage;
72     private boolean mIlluminationRequested;
73 
UdfpsView(Context context, AttributeSet attrs)74     public UdfpsView(Context context, AttributeSet attrs) {
75         super(context, attrs);
76 
77         TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.UdfpsView, 0,
78                 0);
79         try {
80             if (!a.hasValue(R.styleable.UdfpsView_sensorTouchAreaCoefficient)) {
81                 throw new IllegalArgumentException(
82                         "UdfpsView must contain sensorTouchAreaCoefficient");
83             }
84             mSensorTouchAreaCoefficient = a.getFloat(
85                     R.styleable.UdfpsView_sensorTouchAreaCoefficient, 0f);
86         } finally {
87             a.recycle();
88         }
89 
90         mSensorRect = new RectF();
91 
92         mDebugTextPaint = new Paint();
93         mDebugTextPaint.setAntiAlias(true);
94         mDebugTextPaint.setColor(Color.BLUE);
95         mDebugTextPaint.setTextSize(DEBUG_TEXT_SIZE_PX);
96 
97         mOnIlluminatedDelayMs = mContext.getResources().getInteger(
98                 com.android.internal.R.integer.config_udfps_illumination_transition_ms);
99 
100         if (Build.IS_ENG || Build.IS_USERDEBUG) {
101             mHbmType = Settings.Secure.getIntForUser(mContext.getContentResolver(),
102                     SETTING_HBM_TYPE, DEFAULT_HBM_TYPE, UserHandle.USER_CURRENT);
103         } else {
104             mHbmType = DEFAULT_HBM_TYPE;
105         }
106     }
107 
108     // Don't propagate any touch events to the child views.
109     @Override
onInterceptTouchEvent(MotionEvent ev)110     public boolean onInterceptTouchEvent(MotionEvent ev) {
111         return mAnimationViewController == null
112                 || !mAnimationViewController.shouldPauseAuth();
113     }
114 
115     @Override
onFinishInflate()116     protected void onFinishInflate() {
117         if (mHbmType == UdfpsHbmTypes.GLOBAL_HBM) {
118             mGhbmView = findViewById(R.id.hbm_view);
119         }
120     }
121 
setSensorProperties(@onNull FingerprintSensorPropertiesInternal properties)122     void setSensorProperties(@NonNull FingerprintSensorPropertiesInternal properties) {
123         mSensorProps = properties;
124     }
125 
126     @Override
setHbmProvider(@ullable UdfpsHbmProvider hbmProvider)127     public void setHbmProvider(@Nullable UdfpsHbmProvider hbmProvider) {
128         mHbmProvider = hbmProvider;
129     }
130 
131     @Override
dozeTimeTick()132     public void dozeTimeTick() {
133         if (mAnimationViewController != null) {
134             mAnimationViewController.dozeTimeTick();
135         }
136     }
137 
138     @Override
onLayout(boolean changed, int left, int top, int right, int bottom)139     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
140         super.onLayout(changed, left, top, right, bottom);
141         int paddingX = mAnimationViewController == null ? 0
142                 : mAnimationViewController.getPaddingX();
143         int paddingY = mAnimationViewController == null ? 0
144                 : mAnimationViewController.getPaddingY();
145         final SensorLocationInternal location = mSensorProps.getLocation();
146         mSensorRect.set(
147                 paddingX,
148                 paddingY,
149                 2 * location.sensorRadius + paddingX,
150                 2 * location.sensorRadius + paddingY);
151 
152         if (mAnimationViewController != null) {
153             mAnimationViewController.onSensorRectUpdated(new RectF(mSensorRect));
154         }
155     }
156 
onTouchOutsideView()157     void onTouchOutsideView() {
158         if (mAnimationViewController != null) {
159             mAnimationViewController.onTouchOutsideView();
160         }
161     }
162 
setAnimationViewController( @ullable UdfpsAnimationViewController animationViewController)163     void setAnimationViewController(
164             @Nullable UdfpsAnimationViewController animationViewController) {
165         mAnimationViewController = animationViewController;
166     }
167 
getAnimationViewController()168     @Nullable UdfpsAnimationViewController getAnimationViewController() {
169         return mAnimationViewController;
170     }
171 
172     @Override
onAttachedToWindow()173     protected void onAttachedToWindow() {
174         super.onAttachedToWindow();
175         Log.v(TAG, "onAttachedToWindow");
176     }
177 
178     @Override
onDetachedFromWindow()179     protected void onDetachedFromWindow() {
180         super.onDetachedFromWindow();
181         Log.v(TAG, "onDetachedFromWindow");
182     }
183 
184     @Override
onDraw(Canvas canvas)185     protected void onDraw(Canvas canvas) {
186         super.onDraw(canvas);
187         if (!mIlluminationRequested) {
188             if (!TextUtils.isEmpty(mDebugMessage)) {
189                 canvas.drawText(mDebugMessage, 0, 160, mDebugTextPaint);
190             }
191         }
192     }
193 
setDebugMessage(String message)194     void setDebugMessage(String message) {
195         mDebugMessage = message;
196         postInvalidate();
197     }
198 
isWithinSensorArea(float x, float y)199     boolean isWithinSensorArea(float x, float y) {
200         // The X and Y coordinates of the sensor's center.
201         final PointF translation = mAnimationViewController == null
202                 ? new PointF(0, 0)
203                 : mAnimationViewController.getTouchTranslation();
204         final float cx = mSensorRect.centerX() + translation.x;
205         final float cy = mSensorRect.centerY() + translation.y;
206         // Radii along the X and Y axes.
207         final float rx = (mSensorRect.right - mSensorRect.left) / 2.0f;
208         final float ry = (mSensorRect.bottom - mSensorRect.top) / 2.0f;
209 
210         return x > (cx - rx * mSensorTouchAreaCoefficient)
211                 && x < (cx + rx * mSensorTouchAreaCoefficient)
212                 && y > (cy - ry * mSensorTouchAreaCoefficient)
213                 && y < (cy + ry * mSensorTouchAreaCoefficient)
214                 && !mAnimationViewController.shouldPauseAuth();
215     }
216 
isIlluminationRequested()217     boolean isIlluminationRequested() {
218         return mIlluminationRequested;
219     }
220 
221     /**
222      * @param onIlluminatedRunnable Runs when the first illumination frame reaches the panel.
223      */
224     @Override
startIllumination(@ullable Runnable onIlluminatedRunnable)225     public void startIllumination(@Nullable Runnable onIlluminatedRunnable) {
226         mIlluminationRequested = true;
227         if (mAnimationViewController != null) {
228             mAnimationViewController.onIlluminationStarting();
229         }
230 
231         if (mGhbmView != null) {
232             mGhbmView.setGhbmIlluminationListener(this::doIlluminate);
233             mGhbmView.setVisibility(View.VISIBLE);
234             mGhbmView.startGhbmIllumination(onIlluminatedRunnable);
235         } else {
236             doIlluminate(null /* surface */, onIlluminatedRunnable);
237         }
238     }
239 
doIlluminate(@ullable Surface surface, @Nullable Runnable onIlluminatedRunnable)240     private void doIlluminate(@Nullable Surface surface, @Nullable Runnable onIlluminatedRunnable) {
241         if (mGhbmView != null && surface == null) {
242             Log.e(TAG, "doIlluminate | surface must be non-null for GHBM");
243         }
244         mHbmProvider.enableHbm(mHbmType, surface, () -> {
245             if (mGhbmView != null) {
246                 mGhbmView.drawIlluminationDot(mSensorRect);
247             }
248             if (onIlluminatedRunnable != null) {
249                 // No framework API can reliably tell when a frame reaches the panel. A timeout
250                 // is the safest solution.
251                 postDelayed(onIlluminatedRunnable, mOnIlluminatedDelayMs);
252             } else {
253                 Log.w(TAG, "doIlluminate | onIlluminatedRunnable is null");
254             }
255         });
256     }
257 
258     @Override
stopIllumination()259     public void stopIllumination() {
260         mIlluminationRequested = false;
261         if (mAnimationViewController != null) {
262             mAnimationViewController.onIlluminationStopped();
263         }
264         if (mGhbmView != null) {
265             mGhbmView.setGhbmIlluminationListener(null);
266             mGhbmView.setVisibility(View.INVISIBLE);
267         }
268         mHbmProvider.disableHbm(null /* onHbmDisabled */);
269     }
270 }
271