1 /*
2  * Copyright (C) 2015 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.app.settings.SettingsEnums;
20 import android.content.Intent;
21 import android.hardware.fingerprint.FingerprintManager;
22 import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
23 import android.os.Bundle;
24 import android.view.OrientationEventListener;
25 import android.view.Surface;
26 import android.view.View;
27 import android.view.View.OnClickListener;
28 import android.view.accessibility.AccessibilityManager;
29 
30 import androidx.annotation.Nullable;
31 
32 import com.android.settings.R;
33 import com.android.settings.Utils;
34 import com.android.settings.biometrics.BiometricEnrollBase;
35 import com.android.settings.biometrics.BiometricEnrollSidecar;
36 import com.android.settings.biometrics.BiometricUtils;
37 import com.android.settings.password.ChooseLockSettingsHelper;
38 
39 import com.airbnb.lottie.LottieAnimationView;
40 import com.google.android.setupcompat.template.FooterBarMixin;
41 import com.google.android.setupcompat.template.FooterButton;
42 
43 import java.util.List;
44 
45 /**
46  * Activity explaining the fingerprint sensor location for fingerprint enrollment.
47  */
48 public class FingerprintEnrollFindSensor extends BiometricEnrollBase implements
49         BiometricEnrollSidecar.Listener {
50 
51     @Nullable
52     private FingerprintFindSensorAnimation mAnimation;
53 
54     private FingerprintEnrollSidecar mSidecar;
55     private boolean mNextClicked;
56     private boolean mCanAssumeUdfps;
57     private boolean mCanAssumeSidefps;
58 
59     private OrientationEventListener mOrientationEventListener;
60     private int mPreviousRotation = 0;
61 
62     @Override
onCreate(Bundle savedInstanceState)63     protected void onCreate(Bundle savedInstanceState) {
64         super.onCreate(savedInstanceState);
65 
66         final FingerprintManager fingerprintManager = getSystemService(FingerprintManager.class);
67         final List<FingerprintSensorPropertiesInternal> props =
68                 fingerprintManager.getSensorPropertiesInternal();
69         mCanAssumeUdfps = props != null && props.size() == 1 && props.get(0).isAnyUdfpsType();
70         mCanAssumeSidefps = props != null && props.size() == 1 && props.get(0).isAnySidefpsType();
71         setContentView(getContentView());
72         mFooterBarMixin = getLayout().getMixin(FooterBarMixin.class);
73         mFooterBarMixin.setSecondaryButton(
74                 new FooterButton.Builder(this)
75                         .setText(R.string.security_settings_fingerprint_enroll_enrolling_skip)
76                         .setListener(this::onSkipButtonClick)
77                         .setButtonType(FooterButton.ButtonType.SKIP)
78                         .setTheme(R.style.SudGlifButton_Secondary)
79                         .build()
80         );
81 
82         listenOrientationEvent();
83 
84         if (mCanAssumeUdfps) {
85             setHeaderText(R.string.security_settings_udfps_enroll_find_sensor_title);
86             setDescriptionText(R.string.security_settings_udfps_enroll_find_sensor_message);
87             mFooterBarMixin.setPrimaryButton(
88                     new FooterButton.Builder(this)
89                     .setText(R.string.security_settings_udfps_enroll_find_sensor_start_button)
90                     .setListener(this::onStartButtonClick)
91                     .setButtonType(FooterButton.ButtonType.NEXT)
92                     .setTheme(R.style.SudGlifButton_Primary)
93                     .build()
94             );
95 
96             LottieAnimationView lottieAnimationView = findViewById(R.id.illustration_lottie);
97             AccessibilityManager am = getSystemService(AccessibilityManager.class);
98             if (am.isEnabled()) {
99                 lottieAnimationView.setAnimation(R.raw.udfps_edu_a11y_lottie);
100             }
101 
102         } else if (mCanAssumeSidefps) {
103             setHeaderText(R.string.security_settings_fingerprint_enroll_find_sensor_title);
104             setDescriptionText(R.string.security_settings_fingerprint_enroll_find_sensor_message);
105             final LottieAnimationView lottieAnimationView = findViewById(R.id.illustration_lottie);
106             final LottieAnimationView lottieAnimationViewPortrait =
107                     findViewById(R.id.illustration_lottie_portrait);
108             final int rotation = getApplicationContext().getDisplay().getRotation();
109             switch(rotation) {
110                 case Surface.ROTATION_90:
111                     lottieAnimationView.setVisibility(View.GONE);
112                     lottieAnimationViewPortrait.setVisibility(View.VISIBLE);
113                     break;
114                 case Surface.ROTATION_270:
115                     lottieAnimationView.setVisibility(View.GONE);
116                     lottieAnimationViewPortrait.setVisibility(View.VISIBLE);
117                     lottieAnimationViewPortrait.setRotation(180);
118                     break;
119                 default:
120                     lottieAnimationView.setVisibility(View.VISIBLE);
121                     lottieAnimationViewPortrait.setVisibility(View.GONE);
122                     break;
123             }
124         } else {
125             setHeaderText(R.string.security_settings_fingerprint_enroll_find_sensor_title);
126             setDescriptionText(R.string.security_settings_fingerprint_enroll_find_sensor_message);
127         }
128 
129         // This is an entry point for SetNewPasswordController, e.g.
130         // adb shell am start -a android.app.action.SET_NEW_PASSWORD
131         if (mToken == null && BiometricUtils.containsGatekeeperPasswordHandle(getIntent())) {
132             final FingerprintManager fpm = getSystemService(FingerprintManager.class);
133             fpm.generateChallenge(mUserId, (sensorId, userId, challenge) -> {
134                 mChallenge = challenge;
135                 mSensorId = sensorId;
136                 mToken = BiometricUtils.requestGatekeeperHat(this, getIntent(), mUserId, challenge);
137 
138                 // Put this into the intent. This is really just to work around the fact that the
139                 // enrollment sidecar gets the HAT from the activity's intent, rather than having
140                 // it passed in.
141                 getIntent().putExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN, mToken);
142 
143                 startLookingForFingerprint();
144             });
145         } else if (mToken != null) {
146             // HAT passed in from somewhere else, such as FingerprintEnrollIntroduction
147             startLookingForFingerprint();
148         } else {
149             // There's something wrong with the enrollment flow, this should never happen.
150             throw new IllegalStateException("HAT and GkPwHandle both missing...");
151         }
152 
153         mAnimation = null;
154         if (mCanAssumeUdfps) {
155             LottieAnimationView lottieAnimationView = findViewById(R.id.illustration_lottie);
156             lottieAnimationView.setOnClickListener(new OnClickListener() {
157                 @Override
158                 public void onClick(View v) {
159                     onStartButtonClick(v);
160                 }
161             });
162         } else {
163             View animationView = findViewById(R.id.fingerprint_sensor_location_animation);
164             if (animationView instanceof FingerprintFindSensorAnimation) {
165                 mAnimation = (FingerprintFindSensorAnimation) animationView;
166             }
167         }
168     }
169 
170     @Override
onBackPressed()171     public void onBackPressed() {
172         stopLookingForFingerprint();
173         super.onBackPressed();
174     }
175 
getContentView()176     protected int getContentView() {
177         if (mCanAssumeUdfps) {
178             return R.layout.udfps_enroll_find_sensor_layout;
179         } else if (mCanAssumeSidefps) {
180             return R.layout.sfps_enroll_find_sensor_layout;
181         }
182         return R.layout.fingerprint_enroll_find_sensor;
183     }
184 
185     @Override
onStart()186     protected void onStart() {
187         super.onStart();
188         if (mAnimation != null) {
189             mAnimation.startAnimation();
190         }
191     }
192 
stopLookingForFingerprint()193     private void stopLookingForFingerprint() {
194         if (mSidecar != null) {
195             mSidecar.setListener(null);
196             mSidecar.cancelEnrollment();
197             getSupportFragmentManager()
198                     .beginTransaction().remove(mSidecar).commitAllowingStateLoss();
199             mSidecar = null;
200         }
201     }
202 
startLookingForFingerprint()203     private void startLookingForFingerprint() {
204         if (mCanAssumeUdfps) {
205             // UDFPS devices use this screen as an educational screen. Users should tap the
206             // "Start" button to move to the next screen to begin enrollment.
207             return;
208         }
209         mSidecar = (FingerprintEnrollSidecar) getSupportFragmentManager().findFragmentByTag(
210                 FingerprintEnrollEnrolling.TAG_SIDECAR);
211         if (mSidecar == null) {
212             mSidecar = new FingerprintEnrollSidecar();
213             mSidecar.setEnrollReason(FingerprintManager.ENROLL_FIND_SENSOR);
214             getSupportFragmentManager().beginTransaction()
215                     .add(mSidecar, FingerprintEnrollEnrolling.TAG_SIDECAR)
216                     .commitAllowingStateLoss();
217         }
218         mSidecar.setListener(this);
219     }
220 
221     @Override
onEnrollmentProgressChange(int steps, int remaining)222     public void onEnrollmentProgressChange(int steps, int remaining) {
223         mNextClicked = true;
224         proceedToEnrolling(true /* cancelEnrollment */);
225     }
226 
227     @Override
onEnrollmentHelp(int helpMsgId, CharSequence helpString)228     public void onEnrollmentHelp(int helpMsgId, CharSequence helpString) {
229     }
230 
231     @Override
onEnrollmentError(int errMsgId, CharSequence errString)232     public void onEnrollmentError(int errMsgId, CharSequence errString) {
233         if (mNextClicked && errMsgId == FingerprintManager.FINGERPRINT_ERROR_CANCELED) {
234             mNextClicked = false;
235             proceedToEnrolling(false /* cancelEnrollment */);
236         } else {
237             FingerprintErrorDialog.showErrorDialog(this, errMsgId);
238         }
239     }
240 
241     @Override
onStop()242     protected void onStop() {
243         super.onStop();
244         if (mAnimation != null) {
245             mAnimation.pauseAnimation();
246         }
247     }
248 
249     @Override
shouldFinishWhenBackgrounded()250     protected boolean shouldFinishWhenBackgrounded() {
251         return super.shouldFinishWhenBackgrounded() && !mNextClicked;
252     }
253 
254     @Override
onDestroy()255     protected void onDestroy() {
256         stopListenOrientationEvent();
257         super.onDestroy();
258         if (mAnimation != null) {
259             mAnimation.stopAnimation();
260         }
261     }
262 
onStartButtonClick(View view)263     private void onStartButtonClick(View view) {
264         startActivityForResult(getFingerprintEnrollingIntent(), ENROLL_REQUEST);
265     }
266 
onSkipButtonClick(View view)267     protected void onSkipButtonClick(View view) {
268         stopLookingForFingerprint();
269         setResult(RESULT_SKIP);
270         finish();
271     }
272 
proceedToEnrolling(boolean cancelEnrollment)273     private void proceedToEnrolling(boolean cancelEnrollment) {
274         if (mSidecar != null) {
275             if (cancelEnrollment) {
276                 if (mSidecar.cancelEnrollment()) {
277                     // Enrollment cancel requested. When the cancellation is successful,
278                     // onEnrollmentError will be called with FINGERPRINT_ERROR_CANCELED, calling
279                     // this again.
280                     return;
281                 }
282             }
283             getSupportFragmentManager().beginTransaction().remove(mSidecar).
284                     commitAllowingStateLoss();
285             mSidecar = null;
286             startActivityForResult(getFingerprintEnrollingIntent(), ENROLL_REQUEST);
287         }
288     }
289 
290     @Override
onActivityResult(int requestCode, int resultCode, Intent data)291     protected void onActivityResult(int requestCode, int resultCode, Intent data) {
292         if (requestCode == CONFIRM_REQUEST) {
293             if (resultCode == RESULT_OK && data != null) {
294                 throw new IllegalStateException("Pretty sure this is dead code");
295                 /*
296                 mToken = data.getByteArrayExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN);
297                 overridePendingTransition(R.anim.sud_slide_next_in, R.anim.sud_slide_next_out);
298                 getIntent().putExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN, mToken);
299                 startLookingForFingerprint();
300                 */
301             } else {
302                 finish();
303             }
304         } else if (requestCode == ENROLL_REQUEST) {
305             switch (resultCode) {
306                 case RESULT_FINISHED:
307                 case RESULT_SKIP:
308                 case RESULT_TIMEOUT:
309                     setResult(resultCode);
310                     finish();
311                     break;
312                 default:
313                     FingerprintManager fpm = Utils.getFingerprintManagerOrNull(this);
314                     int enrolled = fpm.getEnrolledFingerprints().size();
315                     int max = getResources().getInteger(
316                             com.android.internal.R.integer.config_fingerprintMaxTemplatesPerUser);
317                     if (enrolled >= max) {
318                         finish();
319                     } else {
320                         // We came back from enrolling but it wasn't completed, start again.
321                         startLookingForFingerprint();
322                     }
323                     break;
324             }
325         } else {
326             super.onActivityResult(requestCode, resultCode, data);
327         }
328     }
329 
330     @Override
getMetricsCategory()331     public int getMetricsCategory() {
332         return SettingsEnums.FINGERPRINT_FIND_SENSOR;
333     }
334 
listenOrientationEvent()335     private void listenOrientationEvent() {
336         if (!mCanAssumeSidefps) {
337             // Do nothing if the device doesn't support SideFPS.
338             return;
339         }
340         mOrientationEventListener = new OrientationEventListener(this) {
341             @Override
342             public void onOrientationChanged(int orientation) {
343                 final int currentRotation = getDisplay().getRotation();
344                 if ((mPreviousRotation == Surface.ROTATION_90
345                         && currentRotation == Surface.ROTATION_270) || (
346                         mPreviousRotation == Surface.ROTATION_270
347                                 && currentRotation == Surface.ROTATION_90)) {
348                     mPreviousRotation = currentRotation;
349                     recreate();
350                 }
351             }
352         };
353         mOrientationEventListener.enable();
354         mPreviousRotation = getDisplay().getRotation();
355     }
356 
stopListenOrientationEvent()357     private void stopListenOrientationEvent() {
358         if (!mCanAssumeSidefps) {
359             // Do nothing if the device doesn't support SideFPS.
360             return;
361         }
362         if (mOrientationEventListener != null) {
363             mOrientationEventListener.disable();
364         }
365         mOrientationEventListener = null;
366     }
367 }
368