• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2023 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.biometrics2.ui.view;
18 
19 import static android.hardware.fingerprint.FingerprintManager.ENROLL_ENROLL;
20 
21 import android.annotation.RawRes;
22 import android.app.Activity;
23 import android.content.Context;
24 import android.content.res.Configuration;
25 import android.hardware.fingerprint.FingerprintManager;
26 import android.os.Bundle;
27 import android.text.TextUtils;
28 import android.util.DisplayMetrics;
29 import android.util.Log;
30 import android.view.LayoutInflater;
31 import android.view.Surface;
32 import android.view.View;
33 import android.view.ViewGroup;
34 import android.widget.Button;
35 import android.widget.ImageView;
36 import android.widget.RelativeLayout;
37 import android.widget.TextView;
38 
39 import androidx.activity.OnBackPressedCallback;
40 import androidx.annotation.NonNull;
41 import androidx.fragment.app.Fragment;
42 import androidx.fragment.app.FragmentActivity;
43 import androidx.lifecycle.Observer;
44 import androidx.lifecycle.ViewModelProvider;
45 
46 import com.android.settings.R;
47 import com.android.settings.biometrics.BiometricUtils;
48 import com.android.settings.biometrics.fingerprint.FingerprintErrorDialog;
49 import com.android.settings.biometrics2.ui.model.EnrollmentProgress;
50 import com.android.settings.biometrics2.ui.model.EnrollmentStatusMessage;
51 import com.android.settings.biometrics2.ui.viewmodel.DeviceRotationViewModel;
52 import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollEnrollingViewModel;
53 import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollProgressViewModel;
54 import com.android.settings.biometrics2.ui.widget.UdfpsEnrollView;
55 import com.android.settingslib.display.DisplayDensityUtils;
56 
57 import com.airbnb.lottie.LottieAnimationView;
58 import com.airbnb.lottie.LottieCompositionFactory;
59 
60 /**
61  * Fragment is used to handle enrolling process for udfps
62  */
63 public class FingerprintEnrollEnrollingUdfpsFragment extends Fragment {
64 
65     private static final String TAG = FingerprintEnrollEnrollingUdfpsFragment.class.getSimpleName();
66 
67     private static final int PROGRESS_BAR_MAX = 10000;
68 
69     private static final int STAGE_UNKNOWN = -1;
70     private static final int STAGE_CENTER = 0;
71     private static final int STAGE_GUIDED = 1;
72     private static final int STAGE_FINGERTIP = 2;
73     private static final int STAGE_LEFT_EDGE = 3;
74     private static final int STAGE_RIGHT_EDGE = 4;
75 
76     private FingerprintEnrollEnrollingViewModel mEnrollingViewModel;
77     private DeviceRotationViewModel mRotationViewModel;
78     private FingerprintEnrollProgressViewModel mProgressViewModel;
79 
80     private LottieAnimationView mIllustrationLottie;
81     private boolean mHaveShownUdfpsTipLottie;
82     private boolean mHaveShownUdfpsLeftEdgeLottie;
83     private boolean mHaveShownUdfpsRightEdgeLottie;
84     private boolean mHaveShownUdfpsCenterLottie;
85     private boolean mHaveShownUdfpsGuideLottie;
86 
87     private TextView mTitleText;
88     private TextView mSubTitleText;
89     private UdfpsEnrollView mUdfpsEnrollView;
90     private Button mSkipBtn;
91     private ImageView mIcon;
92 
93     private boolean mShouldShowLottie;
94     private boolean mIsAccessibilityEnabled;
95 
96     private int mRotation = -1;
97 
98     private final View.OnClickListener mOnSkipClickListener =
99             (v) -> mEnrollingViewModel.onCancelledDueToOnSkipPressed();
100 
101     private final Observer<EnrollmentProgress> mProgressObserver = progress -> {
102         if (progress != null) {
103             onEnrollmentProgressChange(progress);
104         }
105     };
106     private final Observer<EnrollmentStatusMessage> mHelpMessageObserver = helpMessage -> {
107         if (helpMessage != null) {
108             onEnrollmentHelp(helpMessage);
109         }
110     };
111     private final Observer<EnrollmentStatusMessage> mErrorMessageObserver = errorMessage -> {
112         if (errorMessage != null) {
113             onEnrollmentError(errorMessage);
114         }
115     };
116     private final Observer<Boolean> mAcquireObserver = isAcquiredGood -> {
117         if (isAcquiredGood != null) {
118             onAcquired(isAcquiredGood);
119         }
120     };
121     private final Observer<Integer> mPointerDownObserver = sensorId -> {
122         if (sensorId != null) {
123             onPointerDown(sensorId);
124         }
125     };
126     private final Observer<Integer> mPointerUpObserver = sensorId -> {
127         if (sensorId != null) {
128             onPointerUp(sensorId);
129         }
130     };
131 
132     private final Observer<Integer> mRotationObserver = rotation -> {
133         if (rotation != null) {
134             onRotationChanged(rotation);
135         }
136     };
137 
138     private final OnBackPressedCallback mOnBackPressedCallback = new OnBackPressedCallback(true) {
139         @Override
140         public void handleOnBackPressed() {
141             setEnabled(false);
142             mEnrollingViewModel.setOnBackPressed();
143             cancelEnrollment();
144         }
145     };
146 
147     @Override
onAttach(@onNull Context context)148     public void onAttach(@NonNull Context context) {
149         final FragmentActivity activity = getActivity();
150         final ViewModelProvider provider = new ViewModelProvider(activity);
151         mEnrollingViewModel = provider.get(FingerprintEnrollEnrollingViewModel.class);
152         mRotationViewModel = provider.get(DeviceRotationViewModel.class);
153         mProgressViewModel = provider.get(FingerprintEnrollProgressViewModel.class);
154         super.onAttach(context);
155         activity.getOnBackPressedDispatcher().addCallback(mOnBackPressedCallback);
156     }
157 
158     @Override
onDetach()159     public void onDetach() {
160         mOnBackPressedCallback.setEnabled(false);
161         super.onDetach();
162     }
163 
164     @Override
onCreate(Bundle savedInstanceState)165     public void onCreate(Bundle savedInstanceState) {
166         super.onCreate(savedInstanceState);
167         mIsAccessibilityEnabled = mEnrollingViewModel.isAccessibilityEnabled();
168     }
169 
170     @Override
onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)171     public View onCreateView(LayoutInflater inflater, ViewGroup container,
172             Bundle savedInstanceState) {
173         final RelativeLayout containView = (RelativeLayout) inflater.inflate(
174                 R.layout.udfps_enroll_enrolling_v2, container, false);
175 
176         final Activity activity = getActivity();
177         mIcon = containView.findViewById(R.id.sud_layout_icon);
178         mTitleText = containView.findViewById(R.id.suc_layout_title);
179         mSubTitleText = containView.findViewById(R.id.sud_layout_subtitle);
180         mSkipBtn = containView.findViewById(R.id.skip_btn);
181         mSkipBtn.setOnClickListener(mOnSkipClickListener);
182         mUdfpsEnrollView = containView.findViewById(R.id.udfps_animation_view);
183         mUdfpsEnrollView.setSensorProperties(
184                 mEnrollingViewModel.getFirstFingerprintSensorPropertiesInternal());
185         mShouldShowLottie = shouldShowLottie();
186         final boolean isLandscape = BiometricUtils.isReverseLandscape(activity)
187                 || BiometricUtils.isLandscape(activity);
188         updateOrientation(containView, (isLandscape
189                 ? Configuration.ORIENTATION_LANDSCAPE : Configuration.ORIENTATION_PORTRAIT));
190 
191         mRotation = mRotationViewModel.getLiveData().getValue();
192         configLayout(mRotation);
193         return containView;
194     }
195 
196     @Override
onStart()197     public void onStart() {
198         super.onStart();
199         startEnrollment();
200         updateProgress(false /* animate */, mProgressViewModel.getProgressLiveData().getValue());
201         final EnrollmentStatusMessage msg = mProgressViewModel.getHelpMessageLiveData().getValue();
202         if (msg != null) {
203             onEnrollmentHelp(msg);
204         } else {
205             updateTitleAndDescription();
206         }
207     }
208 
209     @Override
onResume()210     public void onResume() {
211         super.onResume();
212         mRotationViewModel.getLiveData().observe(this, mRotationObserver);
213     }
214 
215     @Override
onPause()216     public void onPause() {
217         mRotationViewModel.getLiveData().removeObserver(mRotationObserver);
218         super.onPause();
219     }
220 
221     @Override
onStop()222     public void onStop() {
223         removeEnrollmentObservers();
224         if (!getActivity().isChangingConfigurations() && mProgressViewModel.isEnrolling()) {
225             mProgressViewModel.cancelEnrollment();
226         }
227         super.onStop();
228     }
229 
removeEnrollmentObservers()230     private void removeEnrollmentObservers() {
231         preRemoveEnrollmentObservers();
232         mProgressViewModel.getErrorMessageLiveData().removeObserver(mErrorMessageObserver);
233     }
234 
preRemoveEnrollmentObservers()235     private void preRemoveEnrollmentObservers() {
236         mProgressViewModel.getProgressLiveData().removeObserver(mProgressObserver);
237         mProgressViewModel.getHelpMessageLiveData().removeObserver(mHelpMessageObserver);
238         mProgressViewModel.getAcquireLiveData().removeObserver(mAcquireObserver);
239         mProgressViewModel.getPointerDownLiveData().removeObserver(mPointerDownObserver);
240         mProgressViewModel.getPointerUpLiveData().removeObserver(mPointerUpObserver);
241     }
242 
cancelEnrollment()243     private void cancelEnrollment() {
244         preRemoveEnrollmentObservers();
245         mProgressViewModel.cancelEnrollment();
246     }
247 
startEnrollment()248     private void startEnrollment() {
249         final boolean startResult = mProgressViewModel.startEnrollment(ENROLL_ENROLL);
250         if (!startResult) {
251             Log.e(TAG, "startEnrollment(), failed");
252         }
253         mProgressViewModel.getProgressLiveData().observe(this, mProgressObserver);
254         mProgressViewModel.getHelpMessageLiveData().observe(this, mHelpMessageObserver);
255         mProgressViewModel.getErrorMessageLiveData().observe(this, mErrorMessageObserver);
256         mProgressViewModel.getAcquireLiveData().observe(this, mAcquireObserver);
257         mProgressViewModel.getPointerDownLiveData().observe(this, mPointerDownObserver);
258         mProgressViewModel.getPointerUpLiveData().observe(this, mPointerUpObserver);
259     }
260 
updateProgress(boolean animate, @NonNull EnrollmentProgress enrollmentProgress)261     private void updateProgress(boolean animate, @NonNull EnrollmentProgress enrollmentProgress) {
262         if (!mProgressViewModel.isEnrolling()) {
263             Log.d(TAG, "Enrollment not started yet");
264             return;
265         }
266 
267         final int progress = getProgress(enrollmentProgress);
268 
269         if (mProgressViewModel.getProgressLiveData().getValue().getSteps() != -1) {
270             mUdfpsEnrollView.onEnrollmentProgress(enrollmentProgress.getRemaining(),
271                     enrollmentProgress.getSteps());
272         }
273 
274         if (animate) {
275             animateProgress(progress);
276         } else if (progress >= PROGRESS_BAR_MAX) {
277             mDelayedFinishRunnable.run();
278         }
279     }
280 
getProgress(@onNull EnrollmentProgress progress)281     private int getProgress(@NonNull EnrollmentProgress progress) {
282         if (progress.getSteps() == -1) {
283             return 0;
284         }
285         int displayProgress = Math.max(0, progress.getSteps() + 1 - progress.getRemaining());
286         return PROGRESS_BAR_MAX * displayProgress / (progress.getSteps() + 1);
287     }
288 
animateProgress(int progress)289     private void animateProgress(int progress) {
290         // UDFPS animations are owned by SystemUI
291         if (progress >= PROGRESS_BAR_MAX) {
292             // Wait for any animations in SysUI to finish, then proceed to next page
293             getActivity().getMainThreadHandler().postDelayed(mDelayedFinishRunnable, 400L);
294         }
295     }
296 
updateTitleAndDescription()297     private void updateTitleAndDescription() {
298         switch (getCurrentStage()) {
299             case STAGE_CENTER:
300                 mTitleText.setText(R.string.security_settings_fingerprint_enroll_repeat_title);
301                 if (mIsAccessibilityEnabled || mIllustrationLottie == null) {
302                     mSubTitleText.setText(R.string.security_settings_udfps_enroll_start_message);
303                 } else if (!mHaveShownUdfpsCenterLottie) {
304                     mHaveShownUdfpsCenterLottie = true;
305                     // Note: Update string reference when differentiate in between udfps & sfps
306                     mIllustrationLottie.setContentDescription(
307                             getString(R.string.security_settings_sfps_enroll_finger_center_title)
308                     );
309                     configureEnrollmentStage(R.raw.udfps_center_hint_lottie);
310                 }
311                 break;
312 
313             case STAGE_GUIDED:
314                 mTitleText.setText(R.string.security_settings_fingerprint_enroll_repeat_title);
315                 if (mIsAccessibilityEnabled || mIllustrationLottie == null) {
316                     mSubTitleText.setText(
317                             R.string.security_settings_udfps_enroll_repeat_a11y_message);
318                 } else if (!mHaveShownUdfpsGuideLottie) {
319                     mHaveShownUdfpsGuideLottie = true;
320                     mIllustrationLottie.setContentDescription(
321                             getString(R.string.security_settings_fingerprint_enroll_repeat_message)
322                     );
323                     // TODO(b/228100413) Could customize guided lottie animation
324                     configureEnrollmentStage(R.raw.udfps_center_hint_lottie);
325                 }
326                 break;
327             case STAGE_FINGERTIP:
328                 mTitleText.setText(R.string.security_settings_udfps_enroll_fingertip_title);
329                 if (!mHaveShownUdfpsTipLottie && mIllustrationLottie != null) {
330                     mHaveShownUdfpsTipLottie = true;
331                     mIllustrationLottie.setContentDescription(
332                             getString(R.string.security_settings_udfps_tip_fingerprint_help)
333                     );
334                     configureEnrollmentStage(R.raw.udfps_tip_hint_lottie);
335                 }
336                 break;
337             case STAGE_LEFT_EDGE:
338                 mTitleText.setText(R.string.security_settings_udfps_enroll_left_edge_title);
339                 if (!mHaveShownUdfpsLeftEdgeLottie && mIllustrationLottie != null) {
340                     mHaveShownUdfpsLeftEdgeLottie = true;
341                     mIllustrationLottie.setContentDescription(
342                             getString(R.string.security_settings_udfps_side_fingerprint_help)
343                     );
344                     configureEnrollmentStage(R.raw.udfps_left_edge_hint_lottie);
345                 } else if (mIllustrationLottie == null) {
346                     if (isStageHalfCompleted()) {
347                         mSubTitleText.setText(
348                                 R.string.security_settings_fingerprint_enroll_repeat_message);
349                     } else {
350                         mSubTitleText.setText(R.string.security_settings_udfps_enroll_edge_message);
351                     }
352                 }
353                 break;
354             case STAGE_RIGHT_EDGE:
355                 mTitleText.setText(R.string.security_settings_udfps_enroll_right_edge_title);
356                 if (!mHaveShownUdfpsRightEdgeLottie && mIllustrationLottie != null) {
357                     mHaveShownUdfpsRightEdgeLottie = true;
358                     mIllustrationLottie.setContentDescription(
359                             getString(R.string.security_settings_udfps_side_fingerprint_help)
360                     );
361                     configureEnrollmentStage(R.raw.udfps_right_edge_hint_lottie);
362 
363                 } else if (mIllustrationLottie == null) {
364                     if (isStageHalfCompleted()) {
365                         mSubTitleText.setText(
366                                 R.string.security_settings_fingerprint_enroll_repeat_message);
367                     } else {
368                         mSubTitleText.setText(R.string.security_settings_udfps_enroll_edge_message);
369                     }
370                 }
371                 break;
372 
373             case STAGE_UNKNOWN:
374             default:
375                 mTitleText.setText(R.string.security_settings_fingerprint_enroll_udfps_title);
376                 mSubTitleText.setText(R.string.security_settings_udfps_enroll_start_message);
377                 final CharSequence description = getString(
378                         R.string.security_settings_udfps_enroll_a11y);
379                 getActivity().setTitle(description);
380                 break;
381         }
382     }
383 
shouldShowLottie()384     private boolean shouldShowLottie() {
385         DisplayDensityUtils displayDensity = new DisplayDensityUtils(getContext());
386         int currentDensityIndex = displayDensity.getCurrentIndexForDefaultDisplay();
387         final int currentDensity = displayDensity.getDefaultDisplayDensityValues()
388                 [currentDensityIndex];
389         final int defaultDensity = displayDensity.getDefaultDensityForDefaultDisplay();
390         return defaultDensity == currentDensity;
391     }
392 
updateOrientation(@onNull RelativeLayout content, int orientation)393     private void updateOrientation(@NonNull RelativeLayout content, int orientation) {
394         switch (orientation) {
395             case Configuration.ORIENTATION_LANDSCAPE: {
396                 mIllustrationLottie = null;
397                 break;
398             }
399             case Configuration.ORIENTATION_PORTRAIT: {
400                 if (mShouldShowLottie) {
401                     mIllustrationLottie = content.findViewById(R.id.illustration_lottie);
402                 }
403                 break;
404             }
405             default:
406                 Log.e(TAG, "Error unhandled configuration change");
407                 break;
408         }
409     }
410 
getCurrentStage()411     private int getCurrentStage() {
412         EnrollmentProgress progressLiveData = mProgressViewModel.getProgressLiveData().getValue();
413 
414         if (progressLiveData == null || progressLiveData.getSteps() == -1) {
415             return STAGE_UNKNOWN;
416         }
417 
418         final int progressSteps = progressLiveData.getSteps() - progressLiveData.getRemaining();
419         if (progressSteps < getStageThresholdSteps(0)) {
420             return STAGE_CENTER;
421         } else if (progressSteps < getStageThresholdSteps(1)) {
422             return STAGE_GUIDED;
423         } else if (progressSteps < getStageThresholdSteps(2)) {
424             return STAGE_FINGERTIP;
425         } else if (progressSteps < getStageThresholdSteps(3)) {
426             return STAGE_LEFT_EDGE;
427         } else {
428             return STAGE_RIGHT_EDGE;
429         }
430     }
431 
isStageHalfCompleted()432     private boolean isStageHalfCompleted() {
433         EnrollmentProgress progressLiveData = mProgressViewModel.getProgressLiveData().getValue();
434         if (progressLiveData == null || progressLiveData.getSteps() == -1) {
435             return false;
436         }
437 
438         final int progressSteps = progressLiveData.getSteps() - progressLiveData.getRemaining();
439         int prevThresholdSteps = 0;
440         for (int i = 0; i < mEnrollingViewModel.getEnrollStageCount(); i++) {
441             final int thresholdSteps = getStageThresholdSteps(i);
442             if (progressSteps >= prevThresholdSteps && progressSteps < thresholdSteps) {
443                 final int adjustedProgress = progressSteps - prevThresholdSteps;
444                 final int adjustedThreshold = thresholdSteps - prevThresholdSteps;
445                 return adjustedProgress >= adjustedThreshold / 2;
446             }
447             prevThresholdSteps = thresholdSteps;
448         }
449 
450         // After last enrollment step.
451         return true;
452     }
453 
getStageThresholdSteps(int index)454     private int getStageThresholdSteps(int index) {
455 
456         EnrollmentProgress progressLiveData = mProgressViewModel.getProgressLiveData().getValue();
457 
458         if (progressLiveData == null || progressLiveData.getSteps() == -1) {
459             Log.w(TAG, "getStageThresholdSteps: Enrollment not started yet");
460             return 1;
461         }
462         return Math.round(progressLiveData.getSteps()
463                 * mEnrollingViewModel.getEnrollStageThreshold(index));
464     }
465 
configureEnrollmentStage(@awRes int lottie)466     private void configureEnrollmentStage(@RawRes int lottie) {
467         mSubTitleText.setText("");
468         LottieCompositionFactory.fromRawRes(getActivity(), lottie)
469                 .addListener((c) -> {
470                     mIllustrationLottie.setComposition(c);
471                     mIllustrationLottie.setVisibility(View.VISIBLE);
472                     mIllustrationLottie.playAnimation();
473                 });
474     }
475 
onEnrollmentProgressChange(@onNull EnrollmentProgress progress)476     private void onEnrollmentProgressChange(@NonNull EnrollmentProgress progress) {
477         updateProgress(true /* animate */, progress);
478 
479         updateTitleAndDescription();
480 
481         if (mIsAccessibilityEnabled) {
482             final int steps = progress.getSteps();
483             final int remaining = progress.getRemaining();
484             final int percent = (int) (((float) (steps - remaining) / (float) steps) * 100);
485             CharSequence announcement = getActivity().getString(
486                     R.string.security_settings_udfps_enroll_progress_a11y_message, percent);
487             mEnrollingViewModel.sendAccessibilityEvent(announcement);
488         }
489 
490     }
491 
onEnrollmentHelp(@onNull EnrollmentStatusMessage helpMessage)492     private void onEnrollmentHelp(@NonNull EnrollmentStatusMessage helpMessage) {
493         final CharSequence helpStr = helpMessage.getStr();
494         if (!TextUtils.isEmpty(helpStr)) {
495             showError(helpStr);
496             mUdfpsEnrollView.onEnrollmentHelp();
497         }
498     }
onEnrollmentError(@onNull EnrollmentStatusMessage errorMessage)499     private void onEnrollmentError(@NonNull EnrollmentStatusMessage errorMessage) {
500         removeEnrollmentObservers();
501 
502         if (mEnrollingViewModel.getOnBackPressed()
503                 && errorMessage.getMsgId() == FingerprintManager.FINGERPRINT_ERROR_CANCELED) {
504             mEnrollingViewModel.onCancelledDueToOnBackPressed();
505         } else if (mEnrollingViewModel.getOnSkipPressed()
506                 && errorMessage.getMsgId() == FingerprintManager.FINGERPRINT_ERROR_CANCELED) {
507             mEnrollingViewModel.onCancelledDueToOnSkipPressed();
508         } else {
509             final int errMsgId = errorMessage.getMsgId();
510             mEnrollingViewModel.showErrorDialog(
511                     new FingerprintEnrollEnrollingViewModel.ErrorDialogData(
512                             getString(FingerprintErrorDialog.getErrorMessage(errMsgId)),
513                             getString(FingerprintErrorDialog.getErrorTitle(errMsgId)),
514                             errMsgId
515                     ));
516             mProgressViewModel.cancelEnrollment();
517         }
518     }
519 
onAcquired(boolean isAcquiredGood)520     private void onAcquired(boolean isAcquiredGood) {
521         if (mUdfpsEnrollView != null) {
522             mUdfpsEnrollView.onAcquired(isAcquiredGood);
523         }
524     }
525 
onPointerDown(int sensorId)526     private void onPointerDown(int sensorId) {
527         if (mUdfpsEnrollView != null) {
528             mUdfpsEnrollView.onPointerDown(sensorId);
529         }
530     }
531 
onPointerUp(int sensorId)532     private void onPointerUp(int sensorId) {
533         if (mUdfpsEnrollView != null) {
534             mUdfpsEnrollView.onPointerUp(sensorId);
535         }
536     }
537 
showError(CharSequence error)538     private void showError(CharSequence error) {
539         mTitleText.setText(error);
540         mTitleText.setContentDescription(error);
541         mSubTitleText.setContentDescription("");
542     }
543 
onRotationChanged(int newRotation)544     private void onRotationChanged(int newRotation) {
545         if( (newRotation +2) % 4 == mRotation) {
546             mRotation = newRotation;
547             configLayout(newRotation);
548         }
549     }
550 
configLayout(int newRotation)551     private void configLayout(int newRotation) {
552         final Activity activity = getActivity();
553         if (newRotation == Surface.ROTATION_270) {
554             RelativeLayout.LayoutParams iconLP = new RelativeLayout.LayoutParams(-2, -2);
555             iconLP.addRule(RelativeLayout.ALIGN_PARENT_TOP);
556             iconLP.addRule(RelativeLayout.END_OF, R.id.udfps_animation_view);
557             iconLP.topMargin = (int) convertDpToPixel(76.64f, activity);
558             iconLP.leftMargin = (int) convertDpToPixel(151.54f, activity);
559             mIcon.setLayoutParams(iconLP);
560 
561             RelativeLayout.LayoutParams titleLP = new RelativeLayout.LayoutParams(-1, -2);
562             titleLP.addRule(RelativeLayout.ALIGN_PARENT_TOP);
563             titleLP.addRule(RelativeLayout.END_OF, R.id.udfps_animation_view);
564             titleLP.topMargin = (int) convertDpToPixel(138f, activity);
565             titleLP.leftMargin = (int) convertDpToPixel(144f, activity);
566             mTitleText.setLayoutParams(titleLP);
567 
568             RelativeLayout.LayoutParams subtitleLP = new RelativeLayout.LayoutParams(-1, -2);
569             subtitleLP.addRule(RelativeLayout.ALIGN_PARENT_TOP);
570             subtitleLP.addRule(RelativeLayout.END_OF, R.id.udfps_animation_view);
571             subtitleLP.topMargin = (int) convertDpToPixel(198f, activity);
572             subtitleLP.leftMargin = (int) convertDpToPixel(144f, activity);
573             mSubTitleText.setLayoutParams(subtitleLP);
574         } else if (newRotation == Surface.ROTATION_90) {
575             DisplayMetrics metrics = activity.getResources().getDisplayMetrics();
576             RelativeLayout.LayoutParams iconLP = new RelativeLayout.LayoutParams(-2, -2);
577             iconLP.addRule(RelativeLayout.ALIGN_PARENT_TOP);
578             iconLP.addRule(RelativeLayout.ALIGN_PARENT_START);
579             iconLP.topMargin = (int) convertDpToPixel(76.64f, activity);
580             iconLP.leftMargin = (int) convertDpToPixel(71.99f, activity);
581             mIcon.setLayoutParams(iconLP);
582 
583             RelativeLayout.LayoutParams titleLP = new RelativeLayout.LayoutParams(
584                     metrics.widthPixels / 2, -2);
585             titleLP.addRule(RelativeLayout.ALIGN_PARENT_TOP);
586             titleLP.addRule(RelativeLayout.ALIGN_PARENT_START, R.id.udfps_animation_view);
587             titleLP.topMargin = (int) convertDpToPixel(138f, activity);
588             titleLP.leftMargin = (int) convertDpToPixel(66f, activity);
589             mTitleText.setLayoutParams(titleLP);
590 
591             RelativeLayout.LayoutParams subtitleLP = new RelativeLayout.LayoutParams(
592                     metrics.widthPixels / 2, -2);
593             subtitleLP.addRule(RelativeLayout.ALIGN_PARENT_TOP);
594             subtitleLP.addRule(RelativeLayout.ALIGN_PARENT_START);
595             subtitleLP.topMargin = (int) convertDpToPixel(198f, activity);
596             subtitleLP.leftMargin = (int) convertDpToPixel(66f, activity);
597             mSubTitleText.setLayoutParams(subtitleLP);
598         }
599 
600         if (newRotation == Surface.ROTATION_90 || newRotation == Surface.ROTATION_270) {
601             RelativeLayout.LayoutParams skipBtnLP =
602                     (RelativeLayout.LayoutParams) mSkipBtn.getLayoutParams();
603             skipBtnLP.topMargin = (int) convertDpToPixel(26f, activity);
604             skipBtnLP.leftMargin = (int) convertDpToPixel(54f, activity);
605             mSkipBtn.requestLayout();
606         }
607     }
608 
convertDpToPixel(float dp, Context context)609     private float convertDpToPixel(float dp, Context context) {
610         return dp * getDensity(context);
611     }
612 
getDensity(Context context)613     private float getDensity(Context context) {
614         DisplayMetrics metrics = context.getResources().getDisplayMetrics();
615         return metrics.density;
616     }
617 
618     // Give the user a chance to see progress completed before jumping to the next stage.
619     private final Runnable mDelayedFinishRunnable = () -> mEnrollingViewModel.onEnrollingDone();
620 }
621