1 /*
2  * Copyright (C) 2022 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.settings.biometrics.fingerprint;
18 
19 import android.content.Context;
20 import android.graphics.PointF;
21 import android.hardware.fingerprint.FingerprintManager;
22 import android.os.Build;
23 import android.os.Bundle;
24 import android.os.UserHandle;
25 import android.provider.Settings;
26 import android.util.Log;
27 import android.util.TypedValue;
28 import android.view.accessibility.AccessibilityManager;
29 
30 import androidx.annotation.NonNull;
31 import androidx.annotation.Nullable;
32 
33 import com.android.settings.core.InstrumentedFragment;
34 
35 import java.util.ArrayList;
36 import java.util.List;
37 
38 /**
39  * Helps keep track of enrollment state and animates the progress bar accordingly.
40  */
41 public class UdfpsEnrollHelper extends InstrumentedFragment {
42     private static final String TAG = "UdfpsEnrollHelper";
43 
44     private static final String SCALE_OVERRIDE =
45             "com.android.systemui.biometrics.UdfpsEnrollHelper.scale";
46     private static final float SCALE = 0.5f;
47 
48     private static final String NEW_COORDS_OVERRIDE =
49             "com.android.systemui.biometrics.UdfpsNewCoords";
50 
51     interface Listener {
onEnrollmentProgress(int remaining, int totalSteps)52         void onEnrollmentProgress(int remaining, int totalSteps);
53 
onEnrollmentHelp(int remaining, int totalSteps)54         void onEnrollmentHelp(int remaining, int totalSteps);
55 
onAcquired(boolean animateIfLastStepGood)56         void onAcquired(boolean animateIfLastStepGood);
57 
onPointerDown(int sensorId)58         void onPointerDown(int sensorId);
59 
onPointerUp(int sensorId)60         void onPointerUp(int sensorId);
61     }
62 
63     @NonNull
64     private final Context mContext;
65     @NonNull
66     private final FingerprintManager mFingerprintManager;
67     private final boolean mAccessibilityEnabled;
68     @NonNull
69     private final List<PointF> mGuidedEnrollmentPoints;
70 
71     private int mTotalSteps = -1;
72     private int mRemainingSteps = -1;
73 
74     // Note that this is actually not equal to "mTotalSteps - mRemainingSteps", because the
75     // interface makes no promises about monotonically increasing by one each time.
76     private int mLocationsEnrolled = 0;
77 
78     private int mCenterTouchCount = 0;
79 
80     private int mPace = 1;
81 
82     @Nullable
83     UdfpsEnrollHelper.Listener mListener;
84 
UdfpsEnrollHelper(@onNull Context context, @NonNull FingerprintManager fingerprintManager)85     public UdfpsEnrollHelper(@NonNull Context context,
86             @NonNull FingerprintManager fingerprintManager) {
87 
88         mContext = context;
89         mFingerprintManager = fingerprintManager;
90 
91         final AccessibilityManager am = context.getSystemService(AccessibilityManager.class);
92         mAccessibilityEnabled = am.isEnabled();
93 
94         mGuidedEnrollmentPoints = new ArrayList<>();
95 
96         // Number of pixels per mm
97         float px = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_MM, 1,
98                 context.getResources().getDisplayMetrics());
99         boolean useNewCoords = Settings.Secure.getIntForUser(mContext.getContentResolver(),
100                 NEW_COORDS_OVERRIDE, 0,
101                 UserHandle.USER_CURRENT) != 0;
102         if (useNewCoords && (Build.IS_ENG || Build.IS_USERDEBUG)) {
103             Log.v(TAG, "Using new coordinates");
104             mGuidedEnrollmentPoints.add(new PointF(-0.15f * px, -1.02f * px));
105             mGuidedEnrollmentPoints.add(new PointF(-0.15f * px, 1.02f * px));
106             mGuidedEnrollmentPoints.add(new PointF(0.29f * px, 0.00f * px));
107             mGuidedEnrollmentPoints.add(new PointF(2.17f * px, -2.35f * px));
108             mGuidedEnrollmentPoints.add(new PointF(1.07f * px, -3.96f * px));
109             mGuidedEnrollmentPoints.add(new PointF(-0.37f * px, -4.31f * px));
110             mGuidedEnrollmentPoints.add(new PointF(-1.69f * px, -3.29f * px));
111             mGuidedEnrollmentPoints.add(new PointF(-2.48f * px, -1.23f * px));
112             mGuidedEnrollmentPoints.add(new PointF(-2.48f * px, 1.23f * px));
113             mGuidedEnrollmentPoints.add(new PointF(-1.69f * px, 3.29f * px));
114             mGuidedEnrollmentPoints.add(new PointF(-0.37f * px, 4.31f * px));
115             mGuidedEnrollmentPoints.add(new PointF(1.07f * px, 3.96f * px));
116             mGuidedEnrollmentPoints.add(new PointF(2.17f * px, 2.35f * px));
117             mGuidedEnrollmentPoints.add(new PointF(2.58f * px, 0.00f * px));
118         } else {
119             Log.v(TAG, "Using old coordinates");
120             mGuidedEnrollmentPoints.add(new PointF(2.00f * px, 0.00f * px));
121             mGuidedEnrollmentPoints.add(new PointF(0.87f * px, -2.70f * px));
122             mGuidedEnrollmentPoints.add(new PointF(-1.80f * px, -1.31f * px));
123             mGuidedEnrollmentPoints.add(new PointF(-1.80f * px, 1.31f * px));
124             mGuidedEnrollmentPoints.add(new PointF(0.88f * px, 2.70f * px));
125             mGuidedEnrollmentPoints.add(new PointF(3.94f * px, -1.06f * px));
126             mGuidedEnrollmentPoints.add(new PointF(2.90f * px, -4.14f * px));
127             mGuidedEnrollmentPoints.add(new PointF(-0.52f * px, -5.95f * px));
128             mGuidedEnrollmentPoints.add(new PointF(-3.33f * px, -3.33f * px));
129             mGuidedEnrollmentPoints.add(new PointF(-3.99f * px, -0.35f * px));
130             mGuidedEnrollmentPoints.add(new PointF(-3.62f * px, 2.54f * px));
131             mGuidedEnrollmentPoints.add(new PointF(-1.49f * px, 5.57f * px));
132             mGuidedEnrollmentPoints.add(new PointF(2.29f * px, 4.92f * px));
133             mGuidedEnrollmentPoints.add(new PointF(3.82f * px, 1.78f * px));
134         }
135     }
136 
137     @Override
getMetricsCategory()138     public int getMetricsCategory() {
139         return 0;
140     }
141 
142     @Override
onCreate(@ullable Bundle savedInstanceState)143     public void onCreate(@Nullable Bundle savedInstanceState) {
144         super.onCreate(savedInstanceState);
145         setRetainInstance(true);
146     }
147 
148     /**
149      * Called when a enroll progress update
150      */
onEnrollmentProgress(int totalSteps, int remaining)151     public void onEnrollmentProgress(int totalSteps, int remaining) {
152         if (mTotalSteps == -1) {
153             mTotalSteps = totalSteps;
154         }
155 
156         if (remaining != mRemainingSteps) {
157             mLocationsEnrolled++;
158             if (isCenterEnrollmentStage()) {
159                 mCenterTouchCount++;
160             }
161         }
162 
163         if (mRemainingSteps > remaining) {
164             mPace = mRemainingSteps - remaining;
165         }
166         mRemainingSteps = remaining;
167 
168         if (mListener != null && mTotalSteps != -1) {
169             mListener.onEnrollmentProgress(remaining, mTotalSteps);
170         }
171     }
172 
173     /**
174      * Called when a receive error has been encountered during enrollment.
175      */
onEnrollmentHelp()176     public void onEnrollmentHelp() {
177         if (mListener != null) {
178             mListener.onEnrollmentHelp(mRemainingSteps, mTotalSteps);
179         }
180     }
181 
182     /**
183      * Called when a fingerprint image has been acquired, but wasn't processed yet.
184      */
onAcquired(boolean isAcquiredGood)185     public void onAcquired(boolean isAcquiredGood) {
186         if (mListener != null) {
187             mListener.onAcquired(isAcquiredGood && animateIfLastStep());
188         }
189     }
190 
191     /**
192      * Called when pointer down
193      */
onPointerDown(int sensorId)194     public void onPointerDown(int sensorId) {
195         if (mListener != null) {
196             mListener.onPointerDown(sensorId);
197         }
198     }
199 
200     /**
201      * Called when pointer up
202      */
onPointerUp(int sensorId)203     public void onPointerUp(int sensorId) {
204         if (mListener != null) {
205             mListener.onPointerUp(sensorId);
206         }
207     }
208 
setListener(UdfpsEnrollHelper.Listener listener)209     void setListener(UdfpsEnrollHelper.Listener listener) {
210         mListener = listener;
211 
212         // Only notify during setListener if enrollment is already in progress, so the progress
213         // bar can be updated. If enrollment has not started yet, the progress bar will be empty
214         // anyway.
215         if (mListener != null && mTotalSteps != -1) {
216             mListener.onEnrollmentProgress(mRemainingSteps, mTotalSteps);
217         }
218     }
219 
isCenterEnrollmentStage()220     boolean isCenterEnrollmentStage() {
221         if (mTotalSteps == -1 || mRemainingSteps == -1) {
222             return true;
223         }
224         return mTotalSteps - mRemainingSteps < getStageThresholdSteps(mTotalSteps, 0);
225     }
226 
isTipEnrollmentStage()227     boolean isTipEnrollmentStage() {
228         if (mTotalSteps == -1 || mRemainingSteps == -1) {
229             return false;
230         }
231         final int progressSteps = mTotalSteps - mRemainingSteps;
232         return progressSteps >= getStageThresholdSteps(mTotalSteps, 1)
233                 && progressSteps < getStageThresholdSteps(mTotalSteps, 2);
234     }
235 
isEdgeEnrollmentStage()236     boolean isEdgeEnrollmentStage() {
237         if (mTotalSteps == -1 || mRemainingSteps == -1) {
238             return false;
239         }
240         return mTotalSteps - mRemainingSteps >= getStageThresholdSteps(mTotalSteps, 2);
241     }
242 
243     @NonNull
getNextGuidedEnrollmentPoint()244     PointF getNextGuidedEnrollmentPoint() {
245         if (mAccessibilityEnabled || !isGuidedEnrollmentStage()) {
246             return new PointF(0f, 0f);
247         }
248 
249         float scale = SCALE;
250         if (Build.IS_ENG || Build.IS_USERDEBUG) {
251             scale = Settings.Secure.getFloatForUser(mContext.getContentResolver(),
252                     SCALE_OVERRIDE, SCALE,
253                     UserHandle.USER_CURRENT);
254         }
255         final int index = mLocationsEnrolled - mCenterTouchCount;
256         final PointF originalPoint = mGuidedEnrollmentPoints
257                 .get(index % mGuidedEnrollmentPoints.size());
258         return new PointF(originalPoint.x * scale, originalPoint.y * scale);
259     }
260 
animateIfLastStep()261     boolean animateIfLastStep() {
262         if (mListener == null) {
263             Log.e(TAG, "animateIfLastStep, null listener");
264             return false;
265         }
266 
267         return mRemainingSteps <= mPace && mRemainingSteps >= 0;
268     }
269 
getStageThresholdSteps(int totalSteps, int stageIndex)270     private int getStageThresholdSteps(int totalSteps, int stageIndex) {
271         return Math.round(totalSteps * mFingerprintManager.getEnrollStageThreshold(stageIndex));
272     }
273 
isGuidedEnrollmentStage()274     private boolean isGuidedEnrollmentStage() {
275         if (mAccessibilityEnabled || mTotalSteps == -1 || mRemainingSteps == -1) {
276             return false;
277         }
278         final int progressSteps = mTotalSteps - mRemainingSteps;
279         return progressSteps >= getStageThresholdSteps(mTotalSteps, 0)
280                 && progressSteps < getStageThresholdSteps(mTotalSteps, 1);
281     }
282 }
283