Merge "Fix b/265746746: Announce "Battery usage for [slot_timestamp]" instead of changing focus when Talk Back on."
diff --git a/src/com/android/settings/biometrics2/data/repository/AccessibilityRepository.java b/src/com/android/settings/biometrics2/data/repository/AccessibilityRepository.java
new file mode 100644
index 0000000..5353f89
--- /dev/null
+++ b/src/com/android/settings/biometrics2/data/repository/AccessibilityRepository.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.biometrics2.data.repository;
+
+import android.view.accessibility.AccessibilityManager;
+
+/**
+ * This repository is used to call all APIs in {@link AccessibilityManager}
+ */
+public class AccessibilityRepository {
+
+    private final AccessibilityManager mAccessibilityManager;
+
+    public AccessibilityRepository(AccessibilityManager accessibilityManager) {
+        mAccessibilityManager = accessibilityManager;
+    }
+
+    /**
+     * Requests interruption of the accessibility feedback from all accessibility services.
+     */
+    public void interrupt() {
+        mAccessibilityManager.interrupt();
+    }
+
+    /**
+     * Returns if the {@link AccessibilityManager} is enabled.
+     *
+     * @return True if this {@link AccessibilityManager} is enabled, false otherwise.
+     */
+    public boolean isEnabled() {
+        return mAccessibilityManager.isEnabled();
+    }
+}
diff --git a/src/com/android/settings/biometrics2/data/repository/FingerprintRepository.java b/src/com/android/settings/biometrics2/data/repository/FingerprintRepository.java
index 64bf898..8f432e6 100644
--- a/src/com/android/settings/biometrics2/data/repository/FingerprintRepository.java
+++ b/src/com/android/settings/biometrics2/data/repository/FingerprintRepository.java
@@ -42,7 +42,8 @@
 public class FingerprintRepository {
 
     private static final String TAG = "FingerprintRepository";
-    @NonNull private final FingerprintManager mFingerprintManager;
+    @NonNull
+    private final FingerprintManager mFingerprintManager;
 
     private List<FingerprintSensorPropertiesInternal> mSensorPropertiesCache;
 
@@ -130,4 +131,18 @@
         return RestrictedLockUtilsInternal.checkIfKeyguardFeaturesDisabled(
                 context, DevicePolicyManager.KEYGUARD_DISABLE_FINGERPRINT, userId) != null;
     }
+
+    /**
+     * Get fingerprint enroll stage threshold
+     */
+    public float getEnrollStageThreshold(int index) {
+        return mFingerprintManager.getEnrollStageThreshold(index);
+    }
+
+    /**
+     * Get fingerprint enroll stage count
+     */
+    public int getEnrollStageCount() {
+        return mFingerprintManager.getEnrollStageCount();
+    }
 }
diff --git a/src/com/android/settings/biometrics2/data/repository/PackageManagerRepository.java b/src/com/android/settings/biometrics2/data/repository/PackageManagerRepository.java
new file mode 100644
index 0000000..ae00221
--- /dev/null
+++ b/src/com/android/settings/biometrics2/data/repository/PackageManagerRepository.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.biometrics2.data.repository;
+
+import android.annotation.NonNull;
+import android.content.ComponentName;
+import android.content.pm.PackageManager;
+
+/**
+ * This repository is used to call all APIs in {@link PackageManager}
+ */
+public class PackageManagerRepository {
+
+    private final PackageManager mPackageManager;
+
+    public PackageManagerRepository(PackageManager packageManager) {
+        mPackageManager = packageManager;
+    }
+
+    /**
+     * Set the enabled setting for a package component (activity, receiver, service, provider).
+     * This setting will override any enabled state which may have been set by the component in its
+     * manifest.
+     */
+    public void setComponentEnabledSetting(@NonNull ComponentName componentName,
+            @PackageManager.EnabledState int newState, @PackageManager.EnabledFlags int flags) {
+        mPackageManager.setComponentEnabledSetting(componentName, newState, flags);
+    }
+}
diff --git a/src/com/android/settings/biometrics2/data/repository/VibratorRepository.java b/src/com/android/settings/biometrics2/data/repository/VibratorRepository.java
new file mode 100644
index 0000000..cccafff
--- /dev/null
+++ b/src/com/android/settings/biometrics2/data/repository/VibratorRepository.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.biometrics2.data.repository;
+
+import android.annotation.NonNull;
+import android.os.VibrationAttributes;
+import android.os.VibrationEffect;
+import android.os.Vibrator;
+
+/**
+ * This repository is used to call all APIs in {@link Vibrator}
+ */
+public class VibratorRepository {
+
+    private final Vibrator mVibrator;
+
+    public VibratorRepository(Vibrator vibrator) {
+        mVibrator = vibrator;
+    }
+
+    /**
+     * Like {@link #vibrate(VibrationEffect, VibrationAttributes)}, but allows the
+     * caller to specify the vibration is owned by someone else and set a reason for vibration.
+     */
+    public void vibrate(int uid, String opPkg, @NonNull VibrationEffect vibe,
+            String reason, @NonNull VibrationAttributes attributes) {
+        mVibrator.vibrate(uid, opPkg, vibe, reason, attributes);
+    }
+}
diff --git a/src/com/android/settings/biometrics2/factory/BiometricsRepositoryProvider.java b/src/com/android/settings/biometrics2/factory/BiometricsRepositoryProvider.java
index fdc5745..8e17ba4 100644
--- a/src/com/android/settings/biometrics2/factory/BiometricsRepositoryProvider.java
+++ b/src/com/android/settings/biometrics2/factory/BiometricsRepositoryProvider.java
@@ -21,7 +21,9 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
+import com.android.settings.biometrics2.data.repository.AccessibilityRepository;
 import com.android.settings.biometrics2.data.repository.FingerprintRepository;
+import com.android.settings.biometrics2.data.repository.VibratorRepository;
 
 /**
  * Interface for BiometricsRepositoryProvider
@@ -33,4 +35,16 @@
      */
     @Nullable
     FingerprintRepository getFingerprintRepository(@NonNull Application application);
+
+    /**
+     * Get VibtatorRepository
+     */
+    @Nullable
+    VibratorRepository getVibratorRepository(@NonNull Application application);
+
+    /**
+     * Get AccessibilityRepository
+     */
+    @Nullable
+    AccessibilityRepository getAccessibilityRepository(@NonNull Application application);
 }
diff --git a/src/com/android/settings/biometrics2/factory/BiometricsRepositoryProviderImpl.java b/src/com/android/settings/biometrics2/factory/BiometricsRepositoryProviderImpl.java
index 22409c8..7b1fe16 100644
--- a/src/com/android/settings/biometrics2/factory/BiometricsRepositoryProviderImpl.java
+++ b/src/com/android/settings/biometrics2/factory/BiometricsRepositoryProviderImpl.java
@@ -18,12 +18,16 @@
 
 import android.app.Application;
 import android.hardware.fingerprint.FingerprintManager;
+import android.os.Vibrator;
+import android.view.accessibility.AccessibilityManager;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
 import com.android.settings.Utils;
+import com.android.settings.biometrics2.data.repository.AccessibilityRepository;
 import com.android.settings.biometrics2.data.repository.FingerprintRepository;
+import com.android.settings.biometrics2.data.repository.VibratorRepository;
 
 /**
  * Implementation for BiometricsRepositoryProvider
@@ -31,6 +35,8 @@
 public class BiometricsRepositoryProviderImpl implements BiometricsRepositoryProvider {
 
     private static volatile FingerprintRepository sFingerprintRepository;
+    private static volatile VibratorRepository sVibratorRepository;
+    private static volatile AccessibilityRepository sAccessibilityRepository;
 
     /**
      * Get FingerprintRepository
@@ -52,4 +58,49 @@
         }
         return sFingerprintRepository;
     }
+
+    /**
+     * Get VibratorRepository
+     */
+    @Nullable
+    @Override
+    public VibratorRepository getVibratorRepository(@NonNull Application application) {
+
+        final Vibrator vibrator = application.getSystemService(Vibrator.class);
+        if (vibrator == null) {
+            return null;
+        }
+
+        if (sVibratorRepository == null) {
+            synchronized (VibratorRepository.class) {
+                if (sVibratorRepository == null) {
+                    sVibratorRepository = new VibratorRepository(vibrator);
+                }
+            }
+        }
+        return sVibratorRepository;
+    }
+
+    /**
+     * Get AccessibilityRepository
+     */
+    @Nullable
+    @Override
+    public AccessibilityRepository getAccessibilityRepository(@NonNull Application application) {
+
+        final AccessibilityManager accessibilityManager = application.getSystemService(
+                AccessibilityManager.class);
+        if (accessibilityManager == null) {
+            return null;
+        }
+
+        if (sAccessibilityRepository == null) {
+            synchronized (AccessibilityRepository.class) {
+                if (sAccessibilityRepository == null) {
+                    sAccessibilityRepository = new AccessibilityRepository(accessibilityManager);
+                }
+            }
+        }
+        return sAccessibilityRepository;
+    }
 }
diff --git a/src/com/android/settings/biometrics2/factory/BiometricsViewModelFactory.java b/src/com/android/settings/biometrics2/factory/BiometricsViewModelFactory.java
index 0b84f4c..7bf9d53 100644
--- a/src/com/android/settings/biometrics2/factory/BiometricsViewModelFactory.java
+++ b/src/com/android/settings/biometrics2/factory/BiometricsViewModelFactory.java
@@ -28,12 +28,15 @@
 
 import com.android.internal.widget.LockPatternUtils;
 import com.android.settings.biometrics.fingerprint.FingerprintUpdater;
+import com.android.settings.biometrics2.data.repository.AccessibilityRepository;
 import com.android.settings.biometrics2.data.repository.FingerprintRepository;
+import com.android.settings.biometrics2.data.repository.VibratorRepository;
 import com.android.settings.biometrics2.ui.model.EnrollmentRequest;
 import com.android.settings.biometrics2.ui.viewmodel.AutoCredentialViewModel;
 import com.android.settings.biometrics2.ui.viewmodel.AutoCredentialViewModel.ChallengeGenerator;
 import com.android.settings.biometrics2.ui.viewmodel.DeviceFoldedViewModel;
 import com.android.settings.biometrics2.ui.viewmodel.DeviceRotationViewModel;
+import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollEnrollingViewModel;
 import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollFindSensorViewModel;
 import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollIntroViewModel;
 import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollProgressViewModel;
@@ -109,6 +112,16 @@
                 return (T) new FingerprintEnrollProgressViewModel(application,
                         new FingerprintUpdater(application), userId);
             }
+        } else if (modelClass.isAssignableFrom(FingerprintEnrollEnrollingViewModel.class)) {
+            final FingerprintRepository fingerprint = provider.getFingerprintRepository(
+                    application);
+            final AccessibilityRepository accessibility = provider.getAccessibilityRepository(
+                    application);
+            final VibratorRepository vibrator = provider.getVibratorRepository(application);
+            if (fingerprint != null && accessibility != null && vibrator != null) {
+                return (T) new FingerprintEnrollEnrollingViewModel(application, fingerprint,
+                        accessibility, vibrator);
+            }
         }
         return create(modelClass);
     }
diff --git a/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollEnrollingRfpsFragment.java b/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollEnrollingRfpsFragment.java
new file mode 100644
index 0000000..30b66a2
--- /dev/null
+++ b/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollEnrollingRfpsFragment.java
@@ -0,0 +1,272 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.biometrics2.ui.view;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.graphics.PorterDuff;
+import android.graphics.drawable.Animatable2;
+import android.graphics.drawable.AnimatedVectorDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.LayerDrawable;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.animation.AnimationUtils;
+import android.view.animation.Interpolator;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentActivity;
+import androidx.lifecycle.ViewModelProvider;
+
+import com.android.settings.R;
+import com.android.settings.biometrics.BiometricUtils;
+import com.android.settings.biometrics2.ui.model.EnrollmentProgress;
+import com.android.settings.biometrics2.ui.viewmodel.DeviceRotationViewModel;
+import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollEnrollingViewModel;
+import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollProgressViewModel;
+import com.android.settingslib.display.DisplayDensityUtils;
+
+import com.airbnb.lottie.LottieAnimationView;
+import com.google.android.setupcompat.template.FooterBarMixin;
+import com.google.android.setupcompat.template.FooterButton;
+import com.google.android.setupdesign.GlifLayout;
+
+/**
+ * Fragment is used to handle enrolling process for rfps
+ */
+public class FingerprintEnrollEnrollingRfpsFragment extends Fragment {
+
+    private static final String TAG = FingerprintEnrollEnrollingRfpsFragment.class.getSimpleName();
+
+    private static final long ICON_TOUCH_DURATION_UNTIL_DIALOG_SHOWN = 500;
+    private static final int ICON_TOUCH_COUNT_SHOW_UNTIL_DIALOG_SHOWN = 3;
+    private static final int HINT_TIMEOUT_DURATION = 2500;
+
+    private FingerprintEnrollEnrollingViewModel mEnrollingViewModel;
+    private DeviceRotationViewModel mRotationViewModel;
+    private FingerprintEnrollProgressViewModel mProgressViewModel;
+
+    private Interpolator mFastOutSlowInInterpolator;
+    private Interpolator mLinearOutSlowInInterpolator;
+    private Interpolator mFastOutLinearInInterpolator;
+    private boolean mAnimationCancelled;
+
+    private View mView;
+    private ProgressBar mProgressBar;
+    private TextView mErrorText;
+    private FooterBarMixin mFooterBarMixin;
+    private AnimatedVectorDrawable mIconAnimationDrawable;
+    private AnimatedVectorDrawable mIconBackgroundBlinksDrawable;
+
+    private LottieAnimationView mIllustrationLottie;
+    private boolean mShouldShowLottie;
+    private boolean mIsAccessibilityEnabled;
+
+    private boolean mHaveShownSfpsNoAnimationLottie;
+    private boolean mHaveShownSfpsCenterLottie;
+    private boolean mHaveShownSfpsTipLottie;
+    private boolean mHaveShownSfpsLeftEdgeLottie;
+    private boolean mHaveShownSfpsRightEdgeLottie;
+
+    private final View.OnClickListener mOnSkipClickListener =
+            (v) -> mEnrollingViewModel.onSkipButtonClick();
+
+    private int mIconTouchCount;
+
+    @Override
+    public void onAttach(@NonNull Context context) {
+        final FragmentActivity activity = getActivity();
+        final ViewModelProvider provider = new ViewModelProvider(activity);
+        mEnrollingViewModel = provider.get(FingerprintEnrollEnrollingViewModel.class);
+        mRotationViewModel = provider.get(DeviceRotationViewModel.class);
+        mProgressViewModel = provider.get(FingerprintEnrollProgressViewModel.class);
+        super.onAttach(context);
+    }
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        mIsAccessibilityEnabled = mEnrollingViewModel.isAccessibilityEnabled();
+    }
+
+    @Override
+    public View onCreateView(LayoutInflater inflater, ViewGroup container,
+            Bundle savedInstanceState) {
+        mView = initRfpsLayout(inflater, container);
+        return mView;
+    }
+
+    private View initRfpsLayout(LayoutInflater inflater, ViewGroup container) {
+        final View containView = inflater.inflate(R.layout.sfps_enroll_enrolling, container, false);
+        final Activity activity = getActivity();
+        final GlifLayoutHelper glifLayoutHelper = new GlifLayoutHelper(activity,
+                (GlifLayout) containView);
+        glifLayoutHelper.setDescriptionText(
+                R.string.security_settings_fingerprint_enroll_start_message);
+        glifLayoutHelper.setHeaderText(R.string.security_settings_fingerprint_enroll_repeat_title);
+
+        mShouldShowLottie = shouldShowLottie();
+        boolean isLandscape = BiometricUtils.isReverseLandscape(activity)
+                || BiometricUtils.isLandscape(activity);
+        updateOrientation((isLandscape
+                ? Configuration.ORIENTATION_LANDSCAPE : Configuration.ORIENTATION_PORTRAIT));
+
+        mErrorText = containView.findViewById(R.id.error_text);
+        mProgressBar = containView.findViewById(R.id.fingerprint_progress_bar);
+        mFooterBarMixin = ((GlifLayout) containView).getMixin(FooterBarMixin.class);
+        mFooterBarMixin.setSecondaryButton(
+                new FooterButton.Builder(activity)
+                        .setText(R.string.security_settings_fingerprint_enroll_enrolling_skip)
+                        .setListener(mOnSkipClickListener)
+                        .setButtonType(FooterButton.ButtonType.SKIP)
+                        .setTheme(R.style.SudGlifButton_Secondary)
+                        .build()
+        );
+
+        final LayerDrawable fingerprintDrawable = mProgressBar != null
+                ? (LayerDrawable) mProgressBar.getBackground() : null;
+        if (fingerprintDrawable != null) {
+            mIconAnimationDrawable = (AnimatedVectorDrawable)
+                    fingerprintDrawable.findDrawableByLayerId(R.id.fingerprint_animation);
+            mIconBackgroundBlinksDrawable = (AnimatedVectorDrawable)
+                    fingerprintDrawable.findDrawableByLayerId(R.id.fingerprint_background);
+            mIconAnimationDrawable.registerAnimationCallback(mIconAnimationCallback);
+        }
+
+        mFastOutSlowInInterpolator = AnimationUtils.loadInterpolator(
+                activity, android.R.interpolator.fast_out_slow_in);
+        mLinearOutSlowInInterpolator = AnimationUtils.loadInterpolator(
+                activity, android.R.interpolator.linear_out_slow_in);
+        mFastOutLinearInInterpolator = AnimationUtils.loadInterpolator(
+                activity, android.R.interpolator.fast_out_linear_in);
+
+        if (mProgressBar != null) {
+            mProgressBar.setProgressBackgroundTintMode(PorterDuff.Mode.SRC);
+            mProgressBar.setOnTouchListener((v, event) -> {
+                if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
+                    mIconTouchCount++;
+                    if (mIconTouchCount == ICON_TOUCH_COUNT_SHOW_UNTIL_DIALOG_SHOWN) {
+                        showIconTouchDialog();
+                    } else {
+                        mProgressBar.postDelayed(mShowDialogRunnable,
+                                ICON_TOUCH_DURATION_UNTIL_DIALOG_SHOWN);
+                    }
+                } else if (event.getActionMasked() == MotionEvent.ACTION_CANCEL
+                        || event.getActionMasked() == MotionEvent.ACTION_UP) {
+                    mProgressBar.removeCallbacks(mShowDialogRunnable);
+                }
+                return true;
+            });
+        }
+
+        return containView;
+    }
+
+    private void updateOrientation(int orientation) {
+        switch (orientation) {
+            case Configuration.ORIENTATION_LANDSCAPE: {
+                mIllustrationLottie = null;
+                break;
+            }
+            case Configuration.ORIENTATION_PORTRAIT: {
+                if (mShouldShowLottie) {
+                    mIllustrationLottie = mView.findViewById(R.id.illustration_lottie);
+                }
+                break;
+            }
+            default:
+                Log.e(TAG, "Error unhandled configuration change");
+                break;
+        }
+    }
+
+    private void updateTitleAndDescription() {
+        final Activity activity = getActivity();
+        final GlifLayoutHelper glifLayoutHelper = new GlifLayoutHelper(activity,
+                (GlifLayout) mView);
+
+        EnrollmentProgress progressLiveData = mProgressViewModel.getProgressLiveData().getValue();
+        if (progressLiveData == null || progressLiveData.getSteps() == -1) {
+            glifLayoutHelper.setDescriptionText(
+                    R.string.security_settings_fingerprint_enroll_start_message);
+        } else {
+            glifLayoutHelper.setDescriptionText(
+                    R.string.security_settings_fingerprint_enroll_repeat_message);
+        }
+    }
+
+    private void startIconAnimation() {
+        if (mIconAnimationDrawable != null) {
+            mIconAnimationDrawable.start();
+        }
+    }
+
+    private void stopIconAnimation() {
+        mAnimationCancelled = true;
+        if (mIconAnimationDrawable != null) {
+            mIconAnimationDrawable.stop();
+        }
+    }
+
+    private void showIconTouchDialog() {
+        mIconTouchCount = 0;
+        //TODO EnrollingActivity should observe live data and add dialog fragment
+        mEnrollingViewModel.onIconTouchDialogShow();
+    }
+
+    private boolean shouldShowLottie() {
+        DisplayDensityUtils displayDensity = new DisplayDensityUtils(getContext());
+        int currentDensityIndex = displayDensity.getCurrentIndexForDefaultDisplay();
+        final int currentDensity = displayDensity.getDefaultDisplayDensityValues()
+                [currentDensityIndex];
+        final int defaultDensity = displayDensity.getDefaultDensityForDefaultDisplay();
+        return defaultDensity == currentDensity;
+    }
+
+    private final Runnable mShowDialogRunnable = new Runnable() {
+        @Override
+        public void run() {
+            showIconTouchDialog();
+        }
+    };
+
+    private final Animatable2.AnimationCallback mIconAnimationCallback =
+            new Animatable2.AnimationCallback() {
+                @Override
+                public void onAnimationEnd(Drawable d) {
+                    if (mAnimationCancelled) {
+                        return;
+                    }
+
+                    // Start animation after it has ended.
+                    mProgressBar.post(new Runnable() {
+                        @Override
+                        public void run() {
+                            startIconAnimation();
+                        }
+                    });
+                }
+            };
+}
diff --git a/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollEnrollingSfpsFragment.java b/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollEnrollingSfpsFragment.java
new file mode 100644
index 0000000..ddeb465
--- /dev/null
+++ b/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollEnrollingSfpsFragment.java
@@ -0,0 +1,410 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.biometrics2.ui.view;
+
+import android.annotation.RawRes;
+import android.app.Activity;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.graphics.PorterDuff;
+import android.graphics.drawable.Animatable2;
+import android.graphics.drawable.AnimatedVectorDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.LayerDrawable;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.animation.AnimationUtils;
+import android.view.animation.Interpolator;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentActivity;
+import androidx.lifecycle.ViewModelProvider;
+
+import com.android.settings.R;
+import com.android.settings.biometrics.BiometricUtils;
+import com.android.settings.biometrics2.ui.model.EnrollmentProgress;
+import com.android.settings.biometrics2.ui.viewmodel.DeviceRotationViewModel;
+import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollEnrollingViewModel;
+import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollProgressViewModel;
+import com.android.settingslib.display.DisplayDensityUtils;
+
+import com.airbnb.lottie.LottieAnimationView;
+import com.airbnb.lottie.LottieCompositionFactory;
+import com.google.android.setupcompat.template.FooterBarMixin;
+import com.google.android.setupcompat.template.FooterButton;
+import com.google.android.setupdesign.GlifLayout;
+import com.google.android.setupdesign.template.DescriptionMixin;
+import com.google.android.setupdesign.template.HeaderMixin;
+
+/**
+ * Fragment is used to handle enrolling process for sfps
+ */
+public class FingerprintEnrollEnrollingSfpsFragment extends Fragment {
+
+    private static final String TAG = FingerprintEnrollEnrollingSfpsFragment.class.getSimpleName();
+
+    private static final long ICON_TOUCH_DURATION_UNTIL_DIALOG_SHOWN = 500;
+    private static final int ICON_TOUCH_COUNT_SHOW_UNTIL_DIALOG_SHOWN = 3;
+    private static final int HINT_TIMEOUT_DURATION = 2500;
+
+    private static final int STAGE_UNKNOWN = -1;
+    private static final int SFPS_STAGE_NO_ANIMATION = 0;
+    private static final int SFPS_STAGE_CENTER = 1;
+    private static final int SFPS_STAGE_FINGERTIP = 2;
+    private static final int SFPS_STAGE_LEFT_EDGE = 3;
+    private static final int SFPS_STAGE_RIGHT_EDGE = 4;
+
+    private FingerprintEnrollEnrollingViewModel mEnrollingViewModel;
+    private DeviceRotationViewModel mRotationViewModel;
+    private FingerprintEnrollProgressViewModel mProgressViewModel;
+
+    private Interpolator mFastOutSlowInInterpolator;
+    private Interpolator mLinearOutSlowInInterpolator;
+    private Interpolator mFastOutLinearInInterpolator;
+    private boolean mAnimationCancelled;
+
+    private View mView;
+    private ProgressBar mProgressBar;
+    private TextView mErrorText;
+    private FooterBarMixin mFooterBarMixin;
+    private AnimatedVectorDrawable mIconAnimationDrawable;
+    private AnimatedVectorDrawable mIconBackgroundBlinksDrawable;
+
+    private LottieAnimationView mIllustrationLottie;
+    private boolean mShouldShowLottie;
+    private boolean mIsAccessibilityEnabled;
+
+    private boolean mHaveShownSfpsNoAnimationLottie;
+    private boolean mHaveShownSfpsCenterLottie;
+    private boolean mHaveShownSfpsTipLottie;
+    private boolean mHaveShownSfpsLeftEdgeLottie;
+    private boolean mHaveShownSfpsRightEdgeLottie;
+
+    private final View.OnClickListener mOnSkipClickListener =
+            (v) -> mEnrollingViewModel.onSkipButtonClick();
+
+    private int mIconTouchCount;
+
+    @Override
+    public void onAttach(@NonNull Context context) {
+        final FragmentActivity activity = getActivity();
+        final ViewModelProvider provider = new ViewModelProvider(activity);
+        mEnrollingViewModel = provider.get(FingerprintEnrollEnrollingViewModel.class);
+        mRotationViewModel = provider.get(DeviceRotationViewModel.class);
+        mProgressViewModel = provider.get(FingerprintEnrollProgressViewModel.class);
+        super.onAttach(context);
+    }
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        mIsAccessibilityEnabled = mEnrollingViewModel.isAccessibilityEnabled();
+    }
+
+    @Override
+    public View onCreateView(LayoutInflater inflater, ViewGroup container,
+            Bundle savedInstanceState) {
+        mView = initSfpsLayout(inflater, container);
+        final Configuration config = getActivity().getResources().getConfiguration();
+        maybeHideSfpsText(config);
+        return mView;
+    }
+
+    private View initSfpsLayout(LayoutInflater inflater, ViewGroup container) {
+        final View containView = inflater.inflate(R.layout.sfps_enroll_enrolling, container, false);
+        final Activity activity = getActivity();
+        final GlifLayoutHelper glifLayoutHelper = new GlifLayoutHelper(activity,
+                (GlifLayout) containView);
+        glifLayoutHelper.setDescriptionText(
+                R.string.security_settings_fingerprint_enroll_start_message);
+        updateTitleAndDescription();
+
+        mShouldShowLottie = shouldShowLottie();
+        boolean isLandscape = BiometricUtils.isReverseLandscape(activity)
+                || BiometricUtils.isLandscape(activity);
+        updateOrientation((isLandscape
+                ? Configuration.ORIENTATION_LANDSCAPE : Configuration.ORIENTATION_PORTRAIT));
+
+        mErrorText = containView.findViewById(R.id.error_text);
+        mProgressBar = containView.findViewById(R.id.fingerprint_progress_bar);
+        mFooterBarMixin = ((GlifLayout) containView).getMixin(FooterBarMixin.class);
+        mFooterBarMixin.setSecondaryButton(
+                new FooterButton.Builder(activity)
+                        .setText(R.string.security_settings_fingerprint_enroll_enrolling_skip)
+                        .setListener(mOnSkipClickListener)
+                        .setButtonType(FooterButton.ButtonType.SKIP)
+                        .setTheme(R.style.SudGlifButton_Secondary)
+                        .build()
+        );
+
+        final LayerDrawable fingerprintDrawable = mProgressBar != null
+                ? (LayerDrawable) mProgressBar.getBackground() : null;
+        if (fingerprintDrawable != null) {
+            mIconAnimationDrawable = (AnimatedVectorDrawable)
+                    fingerprintDrawable.findDrawableByLayerId(R.id.fingerprint_animation);
+            mIconBackgroundBlinksDrawable = (AnimatedVectorDrawable)
+                    fingerprintDrawable.findDrawableByLayerId(R.id.fingerprint_background);
+            mIconAnimationDrawable.registerAnimationCallback(mIconAnimationCallback);
+        }
+
+        mFastOutSlowInInterpolator = AnimationUtils.loadInterpolator(
+                activity, android.R.interpolator.fast_out_slow_in);
+        mLinearOutSlowInInterpolator = AnimationUtils.loadInterpolator(
+                activity, android.R.interpolator.linear_out_slow_in);
+        mFastOutLinearInInterpolator = AnimationUtils.loadInterpolator(
+                activity, android.R.interpolator.fast_out_linear_in);
+
+        if (mProgressBar != null) {
+            mProgressBar.setProgressBackgroundTintMode(PorterDuff.Mode.SRC);
+            mProgressBar.setOnTouchListener((v, event) -> {
+                if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
+                    mIconTouchCount++;
+                    if (mIconTouchCount == ICON_TOUCH_COUNT_SHOW_UNTIL_DIALOG_SHOWN) {
+                        showIconTouchDialog();
+                    } else {
+                        mProgressBar.postDelayed(mShowDialogRunnable,
+                                ICON_TOUCH_DURATION_UNTIL_DIALOG_SHOWN);
+                    }
+                } else if (event.getActionMasked() == MotionEvent.ACTION_CANCEL
+                        || event.getActionMasked() == MotionEvent.ACTION_UP) {
+                    mProgressBar.removeCallbacks(mShowDialogRunnable);
+                }
+                return true;
+            });
+        }
+
+        return containView;
+    }
+
+    private void updateTitleAndDescription() {
+
+        final Activity activity = getActivity();
+        final GlifLayoutHelper glifLayoutHelper = new GlifLayoutHelper(activity,
+                (GlifLayout) mView);
+
+        if (mIsAccessibilityEnabled) {
+            mEnrollingViewModel.clearTalkback();
+            ((GlifLayout) mView).getDescriptionTextView().setAccessibilityLiveRegion(
+                    View.ACCESSIBILITY_LIVE_REGION_POLITE);
+        }
+        switch (getCurrentSfpsStage()) {
+            case SFPS_STAGE_NO_ANIMATION:
+                glifLayoutHelper.setHeaderText(
+                        R.string.security_settings_fingerprint_enroll_repeat_title);
+                if (!mHaveShownSfpsNoAnimationLottie && mIllustrationLottie != null) {
+                    mHaveShownSfpsNoAnimationLottie = true;
+                    mIllustrationLottie.setContentDescription(
+                            getString(
+                                    R.string.security_settings_sfps_animation_a11y_label,
+                                    0
+                            )
+                    );
+                    configureEnrollmentStage(
+                            getString(R.string.security_settings_sfps_enroll_start_message),
+                            R.raw.sfps_lottie_no_animation
+                    );
+                }
+                break;
+
+            case SFPS_STAGE_CENTER:
+                glifLayoutHelper.setHeaderText(
+                        R.string.security_settings_sfps_enroll_finger_center_title);
+                if (!mHaveShownSfpsCenterLottie && mIllustrationLottie != null) {
+                    mHaveShownSfpsCenterLottie = true;
+                    configureEnrollmentStage(
+                            getString(R.string.security_settings_sfps_enroll_start_message),
+                            R.raw.sfps_lottie_pad_center
+                    );
+                }
+                break;
+
+            case SFPS_STAGE_FINGERTIP:
+                glifLayoutHelper.setHeaderText(
+                        R.string.security_settings_sfps_enroll_fingertip_title);
+                if (!mHaveShownSfpsTipLottie && mIllustrationLottie != null) {
+                    mHaveShownSfpsTipLottie = true;
+                    configureEnrollmentStage("", R.raw.sfps_lottie_tip);
+                }
+                break;
+
+            case SFPS_STAGE_LEFT_EDGE:
+                glifLayoutHelper.setHeaderText(
+                        R.string.security_settings_sfps_enroll_left_edge_title);
+                if (!mHaveShownSfpsLeftEdgeLottie && mIllustrationLottie != null) {
+                    mHaveShownSfpsLeftEdgeLottie = true;
+                    configureEnrollmentStage("", R.raw.sfps_lottie_left_edge);
+                }
+                break;
+
+            case SFPS_STAGE_RIGHT_EDGE:
+                glifLayoutHelper.setHeaderText(
+                        R.string.security_settings_sfps_enroll_right_edge_title);
+                if (!mHaveShownSfpsRightEdgeLottie && mIllustrationLottie != null) {
+                    mHaveShownSfpsRightEdgeLottie = true;
+                    configureEnrollmentStage("", R.raw.sfps_lottie_right_edge);
+                }
+                break;
+
+            case STAGE_UNKNOWN:
+            default:
+                // Don't use BiometricEnrollBase#setHeaderText, since that invokes setTitle,
+                // which gets announced for a11y upon entering the page. For SFPS, we want to
+                // announce a different string for a11y upon entering the page.
+                glifLayoutHelper.setHeaderText(
+                        R.string.security_settings_sfps_enroll_find_sensor_title);
+                glifLayoutHelper.setDescriptionText(
+                        R.string.security_settings_sfps_enroll_start_message);
+                final CharSequence description = getString(
+                        R.string.security_settings_sfps_enroll_find_sensor_message);
+                ((GlifLayout) mView).getHeaderTextView().setContentDescription(description);
+                activity.setTitle(description);
+                break;
+
+        }
+    }
+
+    private void maybeHideSfpsText(@android.annotation.NonNull Configuration newConfig) {
+        final HeaderMixin headerMixin = ((GlifLayout) mView).getMixin(HeaderMixin.class);
+        final DescriptionMixin descriptionMixin = ((GlifLayout) mView).getMixin(
+                DescriptionMixin.class);
+        final boolean isLandscape = newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE;
+
+        if (isLandscape) {
+            headerMixin.setAutoTextSizeEnabled(true);
+            headerMixin.getTextView().setMinLines(0);
+            headerMixin.getTextView().setMaxLines(10);
+            descriptionMixin.getTextView().setMinLines(0);
+            descriptionMixin.getTextView().setMaxLines(10);
+        } else {
+            headerMixin.setAutoTextSizeEnabled(false);
+            headerMixin.getTextView().setLines(4);
+            // hide the description
+            descriptionMixin.getTextView().setLines(0);
+        }
+
+    }
+
+    private int getCurrentSfpsStage() {
+        EnrollmentProgress progressLiveData = mProgressViewModel.getProgressLiveData().getValue();
+
+        if (progressLiveData == null || progressLiveData.getSteps() == -1) {
+            return STAGE_UNKNOWN;
+        }
+
+        final int progressSteps = progressLiveData.getSteps() - progressLiveData.getRemaining();
+        if (progressSteps < getStageThresholdSteps(0)) {
+            return SFPS_STAGE_NO_ANIMATION;
+        } else if (progressSteps < getStageThresholdSteps(1)) {
+            return SFPS_STAGE_CENTER;
+        } else if (progressSteps < getStageThresholdSteps(2)) {
+            return SFPS_STAGE_FINGERTIP;
+        } else if (progressSteps < getStageThresholdSteps(3)) {
+            return SFPS_STAGE_LEFT_EDGE;
+        } else {
+            return SFPS_STAGE_RIGHT_EDGE;
+        }
+    }
+
+    private int getStageThresholdSteps(int index) {
+
+        EnrollmentProgress progressLiveData = mProgressViewModel.getProgressLiveData().getValue();
+
+        if (progressLiveData == null || progressLiveData.getSteps() == -1) {
+            Log.w(TAG, "getStageThresholdSteps: Enrollment not started yet");
+            return 1;
+        }
+        return Math.round(progressLiveData.getSteps()
+                * mEnrollingViewModel.getEnrollStageThreshold(index));
+    }
+
+    private void updateOrientation(int orientation) {
+        mIllustrationLottie = mView.findViewById(R.id.illustration_lottie);
+    }
+
+    private boolean shouldShowLottie() {
+        DisplayDensityUtils displayDensity = new DisplayDensityUtils(getContext());
+        int currentDensityIndex = displayDensity.getCurrentIndexForDefaultDisplay();
+        final int currentDensity = displayDensity.getDefaultDisplayDensityValues()
+                [currentDensityIndex];
+        final int defaultDensity = displayDensity.getDefaultDensityForDefaultDisplay();
+        return defaultDensity == currentDensity;
+    }
+
+
+    private void startIconAnimation() {
+        if (mIconAnimationDrawable != null) {
+            mIconAnimationDrawable.start();
+        }
+    }
+
+    private void stopIconAnimation() {
+        mAnimationCancelled = true;
+        if (mIconAnimationDrawable != null) {
+            mIconAnimationDrawable.stop();
+        }
+    }
+
+    private void showIconTouchDialog() {
+        mIconTouchCount = 0;
+        //TODO EnrollingActivity should observe live data and add dialog fragment
+        mEnrollingViewModel.onIconTouchDialogShow();
+    }
+
+    private void configureEnrollmentStage(CharSequence description, @RawRes int lottie) {
+        final GlifLayoutHelper glifLayoutHelper = new GlifLayoutHelper(getActivity(),
+                (GlifLayout) mView);
+        glifLayoutHelper.setDescriptionText(description);
+        LottieCompositionFactory.fromRawRes(getActivity(), lottie)
+                .addListener((c) -> {
+                    mIllustrationLottie.setComposition(c);
+                    mIllustrationLottie.setVisibility(View.VISIBLE);
+                    mIllustrationLottie.playAnimation();
+                });
+    }
+
+    private final Runnable mShowDialogRunnable = new Runnable() {
+        @Override
+        public void run() {
+            showIconTouchDialog();
+        }
+    };
+
+    private final Animatable2.AnimationCallback mIconAnimationCallback =
+            new Animatable2.AnimationCallback() {
+                @Override
+                public void onAnimationEnd(Drawable d) {
+                    if (mAnimationCancelled) {
+                        return;
+                    }
+
+                    // Start animation after it has ended.
+                    mProgressBar.post(new Runnable() {
+                        @Override
+                        public void run() {
+                            startIconAnimation();
+                        }
+                    });
+                }
+            };
+}
diff --git a/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollEnrollingUdfpsFragment.java b/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollEnrollingUdfpsFragment.java
new file mode 100644
index 0000000..89b061f
--- /dev/null
+++ b/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollEnrollingUdfpsFragment.java
@@ -0,0 +1,470 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.biometrics2.ui.view;
+
+import android.annotation.RawRes;
+import android.app.Activity;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.graphics.PorterDuff;
+import android.graphics.drawable.Animatable2;
+import android.graphics.drawable.AnimatedVectorDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.LayerDrawable;
+import android.os.Bundle;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.Surface;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.animation.AnimationUtils;
+import android.view.animation.Interpolator;
+import android.widget.LinearLayout;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentActivity;
+import androidx.lifecycle.ViewModelProvider;
+
+import com.android.settings.R;
+import com.android.settings.biometrics.BiometricUtils;
+import com.android.settings.biometrics2.ui.model.EnrollmentProgress;
+import com.android.settings.biometrics2.ui.viewmodel.DeviceRotationViewModel;
+import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollEnrollingViewModel;
+import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollProgressViewModel;
+import com.android.settingslib.display.DisplayDensityUtils;
+
+import com.airbnb.lottie.LottieAnimationView;
+import com.airbnb.lottie.LottieCompositionFactory;
+import com.google.android.setupcompat.template.FooterBarMixin;
+import com.google.android.setupcompat.template.FooterButton;
+import com.google.android.setupdesign.GlifLayout;
+
+import java.util.Locale;
+
+/**
+ * Fragment is used to handle enrolling process for udfps
+ */
+public class FingerprintEnrollEnrollingUdfpsFragment extends Fragment {
+
+    private static final String TAG = FingerprintEnrollEnrollingUdfpsFragment.class.getSimpleName();
+
+    private static final long ICON_TOUCH_DURATION_UNTIL_DIALOG_SHOWN = 500;
+    private static final int ICON_TOUCH_COUNT_SHOW_UNTIL_DIALOG_SHOWN = 3;
+    private static final int HINT_TIMEOUT_DURATION = 2500;
+
+    private static final int STAGE_UNKNOWN = -1;
+    private static final int STAGE_CENTER = 0;
+    private static final int STAGE_GUIDED = 1;
+    private static final int STAGE_FINGERTIP = 2;
+    private static final int STAGE_LEFT_EDGE = 3;
+    private static final int STAGE_RIGHT_EDGE = 4;
+
+    private FingerprintEnrollEnrollingViewModel mEnrollingViewModel;
+    private DeviceRotationViewModel mRotationViewModel;
+    private FingerprintEnrollProgressViewModel mProgressViewModel;
+
+    private Interpolator mFastOutSlowInInterpolator;
+    private Interpolator mLinearOutSlowInInterpolator;
+    private Interpolator mFastOutLinearInInterpolator;
+    private boolean mAnimationCancelled;
+
+    private LottieAnimationView mIllustrationLottie;
+    private boolean mHaveShownUdfpsTipLottie;
+    private boolean mHaveShownUdfpsLeftEdgeLottie;
+    private boolean mHaveShownUdfpsRightEdgeLottie;
+    private boolean mHaveShownUdfpsCenterLottie;
+    private boolean mHaveShownUdfpsGuideLottie;
+
+    private View mView;
+    private ProgressBar mProgressBar;
+    private TextView mErrorText;
+    private FooterBarMixin mFooterBarMixin;
+    private AnimatedVectorDrawable mIconAnimationDrawable;
+    private AnimatedVectorDrawable mIconBackgroundBlinksDrawable;
+
+    private boolean mShouldShowLottie;
+    private boolean mIsAccessibilityEnabled;
+
+    private final View.OnClickListener mOnSkipClickListener =
+            (v) -> mEnrollingViewModel.onSkipButtonClick();
+
+    private int mIconTouchCount;
+
+    @Override
+    public void onAttach(@NonNull Context context) {
+        final FragmentActivity activity = getActivity();
+        final ViewModelProvider provider = new ViewModelProvider(activity);
+        mEnrollingViewModel = provider.get(FingerprintEnrollEnrollingViewModel.class);
+        mRotationViewModel = provider.get(DeviceRotationViewModel.class);
+        mProgressViewModel = provider.get(FingerprintEnrollProgressViewModel.class);
+        super.onAttach(context);
+    }
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        mIsAccessibilityEnabled = mEnrollingViewModel.isAccessibilityEnabled();
+    }
+
+    @Override
+    public View onCreateView(LayoutInflater inflater, ViewGroup container,
+            Bundle savedInstanceState) {
+        mView = initUdfpsLayout(inflater, container);
+        return mView;
+    }
+
+    private View initUdfpsLayout(LayoutInflater inflater, ViewGroup container) {
+        final View containView = inflater.inflate(R.layout.udfps_enroll_enrolling, container,
+                false);
+
+        final Activity activity = getActivity();
+        final GlifLayoutHelper glifLayoutHelper = new GlifLayoutHelper(activity,
+                (GlifLayout) containView);
+        final int rotation = mRotationViewModel.getLiveData().getValue();
+        final boolean isLayoutRtl = (TextUtils.getLayoutDirectionFromLocale(
+                Locale.getDefault()) == View.LAYOUT_DIRECTION_RTL);
+
+
+        //TODO  implement b/20653554
+        if (rotation == Surface.ROTATION_90) {
+            final LinearLayout layoutContainer = containView.findViewById(
+                    R.id.layout_container);
+            final LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
+                    LinearLayout.LayoutParams.MATCH_PARENT,
+                    LinearLayout.LayoutParams.MATCH_PARENT);
+            lp.setMarginEnd((int) getResources().getDimension(
+                    R.dimen.rotation_90_enroll_margin_end));
+            layoutContainer.setPaddingRelative((int) getResources().getDimension(
+                    R.dimen.rotation_90_enroll_padding_start), 0, isLayoutRtl
+                    ? 0 : (int) getResources().getDimension(
+                    R.dimen.rotation_90_enroll_padding_end), 0);
+            layoutContainer.setLayoutParams(lp);
+            containView.setLayoutParams(lp);
+        }
+        glifLayoutHelper.setDescriptionText(R.string.security_settings_udfps_enroll_start_message);
+        updateTitleAndDescription();
+
+        mShouldShowLottie = shouldShowLottie();
+        boolean isLandscape = BiometricUtils.isReverseLandscape(activity)
+                || BiometricUtils.isLandscape(activity);
+        updateOrientation((isLandscape
+                ? Configuration.ORIENTATION_LANDSCAPE : Configuration.ORIENTATION_PORTRAIT));
+
+        mErrorText = containView.findViewById(R.id.error_text);
+        mProgressBar = containView.findViewById(R.id.fingerprint_progress_bar);
+        mFooterBarMixin = ((GlifLayout) containView).getMixin(FooterBarMixin.class);
+        mFooterBarMixin.setSecondaryButton(
+                new FooterButton.Builder(activity)
+                        .setText(R.string.security_settings_fingerprint_enroll_enrolling_skip)
+                        .setListener(mOnSkipClickListener)
+                        .setButtonType(FooterButton.ButtonType.SKIP)
+                        .setTheme(R.style.SudGlifButton_Secondary)
+                        .build()
+        );
+
+        final LayerDrawable fingerprintDrawable = mProgressBar != null
+                ? (LayerDrawable) mProgressBar.getBackground() : null;
+        if (fingerprintDrawable != null) {
+            mIconAnimationDrawable = (AnimatedVectorDrawable)
+                    fingerprintDrawable.findDrawableByLayerId(R.id.fingerprint_animation);
+            mIconBackgroundBlinksDrawable = (AnimatedVectorDrawable)
+                    fingerprintDrawable.findDrawableByLayerId(R.id.fingerprint_background);
+            mIconAnimationDrawable.registerAnimationCallback(mIconAnimationCallback);
+        }
+
+        mFastOutSlowInInterpolator = AnimationUtils.loadInterpolator(
+                activity, android.R.interpolator.fast_out_slow_in);
+        mLinearOutSlowInInterpolator = AnimationUtils.loadInterpolator(
+                activity, android.R.interpolator.linear_out_slow_in);
+        mFastOutLinearInInterpolator = AnimationUtils.loadInterpolator(
+                activity, android.R.interpolator.fast_out_linear_in);
+
+        if (mProgressBar != null) {
+            mProgressBar.setProgressBackgroundTintMode(PorterDuff.Mode.SRC);
+            mProgressBar.setOnTouchListener((v, event) -> {
+                if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
+                    mIconTouchCount++;
+                    if (mIconTouchCount == ICON_TOUCH_COUNT_SHOW_UNTIL_DIALOG_SHOWN) {
+                        showIconTouchDialog();
+                    } else {
+                        mProgressBar.postDelayed(mShowDialogRunnable,
+                                ICON_TOUCH_DURATION_UNTIL_DIALOG_SHOWN);
+                    }
+                } else if (event.getActionMasked() == MotionEvent.ACTION_CANCEL
+                        || event.getActionMasked() == MotionEvent.ACTION_UP) {
+                    mProgressBar.removeCallbacks(mShowDialogRunnable);
+                }
+                return true;
+            });
+        }
+
+        return containView;
+    }
+
+    private void updateTitleAndDescription() {
+
+        final Activity activity = getActivity();
+        final GlifLayoutHelper glifLayoutHelper = new GlifLayoutHelper(activity,
+                (GlifLayout) mView);
+
+        switch (getCurrentStage()) {
+            case STAGE_CENTER:
+                glifLayoutHelper.setHeaderText(
+                        R.string.security_settings_fingerprint_enroll_repeat_title);
+                if (mIsAccessibilityEnabled || mIllustrationLottie == null) {
+                    glifLayoutHelper.setDescriptionText(
+                            R.string.security_settings_udfps_enroll_start_message);
+                } else if (!mHaveShownUdfpsCenterLottie && mIllustrationLottie != null) {
+                    mHaveShownUdfpsCenterLottie = true;
+                    // Note: Update string reference when differentiate in between udfps & sfps
+                    mIllustrationLottie.setContentDescription(
+                            getString(R.string.security_settings_sfps_enroll_finger_center_title)
+                    );
+                    configureEnrollmentStage("", R.raw.udfps_center_hint_lottie);
+                }
+                break;
+
+            case STAGE_GUIDED:
+                glifLayoutHelper.setHeaderText(
+                        R.string.security_settings_fingerprint_enroll_repeat_title);
+                if (mIsAccessibilityEnabled || mIllustrationLottie == null) {
+                    glifLayoutHelper.setDescriptionText(
+                            R.string.security_settings_udfps_enroll_repeat_a11y_message);
+                } else if (!mHaveShownUdfpsGuideLottie && mIllustrationLottie != null) {
+                    mHaveShownUdfpsGuideLottie = true;
+                    mIllustrationLottie.setContentDescription(
+                            getString(R.string.security_settings_fingerprint_enroll_repeat_message)
+                    );
+                    // TODO(b/228100413) Could customize guided lottie animation
+                    configureEnrollmentStage("", R.raw.udfps_center_hint_lottie);
+                }
+                break;
+            case STAGE_FINGERTIP:
+                glifLayoutHelper.setHeaderText(
+                        R.string.security_settings_udfps_enroll_fingertip_title);
+                if (!mHaveShownUdfpsTipLottie && mIllustrationLottie != null) {
+                    mHaveShownUdfpsTipLottie = true;
+                    mIllustrationLottie.setContentDescription(
+                            getString(R.string.security_settings_udfps_tip_fingerprint_help)
+                    );
+                    configureEnrollmentStage("", R.raw.udfps_tip_hint_lottie);
+                }
+                break;
+            case STAGE_LEFT_EDGE:
+                glifLayoutHelper.setHeaderText(
+                        R.string.security_settings_udfps_enroll_left_edge_title);
+                if (!mHaveShownUdfpsLeftEdgeLottie && mIllustrationLottie != null) {
+                    mHaveShownUdfpsLeftEdgeLottie = true;
+                    mIllustrationLottie.setContentDescription(
+                            getString(R.string.security_settings_udfps_side_fingerprint_help)
+                    );
+                    configureEnrollmentStage("", R.raw.udfps_left_edge_hint_lottie);
+                } else if (mIllustrationLottie == null) {
+                    if (isStageHalfCompleted()) {
+                        glifLayoutHelper.setDescriptionText(
+                                R.string.security_settings_fingerprint_enroll_repeat_message);
+                    } else {
+                        glifLayoutHelper.setDescriptionText(
+                                R.string.security_settings_udfps_enroll_edge_message);
+                    }
+                }
+                break;
+            case STAGE_RIGHT_EDGE:
+                glifLayoutHelper.setHeaderText(
+                        R.string.security_settings_udfps_enroll_right_edge_title);
+                if (!mHaveShownUdfpsRightEdgeLottie && mIllustrationLottie != null) {
+                    mHaveShownUdfpsRightEdgeLottie = true;
+                    mIllustrationLottie.setContentDescription(
+                            getString(R.string.security_settings_udfps_side_fingerprint_help)
+                    );
+                    configureEnrollmentStage("", R.raw.udfps_right_edge_hint_lottie);
+
+                } else if (mIllustrationLottie == null) {
+                    if (isStageHalfCompleted()) {
+                        glifLayoutHelper.setDescriptionText(
+                                R.string.security_settings_fingerprint_enroll_repeat_message);
+                    } else {
+                        glifLayoutHelper.setDescriptionText(
+                                R.string.security_settings_udfps_enroll_edge_message);
+                    }
+                }
+                break;
+
+            case STAGE_UNKNOWN:
+            default:
+                // setHeaderText(R.string.security_settings_fingerprint_enroll_udfps_title);
+                // Don't use BiometricEnrollBase#setHeaderText, since that invokes setTitle,
+                // which gets announced for a11y upon entering the page. For UDFPS, we want to
+                // announce a different string for a11y upon entering the page.
+                glifLayoutHelper.setHeaderText(
+                        R.string.security_settings_fingerprint_enroll_udfps_title);
+                glifLayoutHelper.setDescriptionText(
+                        R.string.security_settings_udfps_enroll_start_message);
+                final CharSequence description = getString(
+                        R.string.security_settings_udfps_enroll_a11y);
+                ((GlifLayout) mView).getHeaderTextView().setContentDescription(description);
+                activity.setTitle(description);
+                break;
+
+        }
+    }
+
+    private boolean shouldShowLottie() {
+        DisplayDensityUtils displayDensity = new DisplayDensityUtils(getContext());
+        int currentDensityIndex = displayDensity.getCurrentIndexForDefaultDisplay();
+        final int currentDensity = displayDensity.getDefaultDisplayDensityValues()
+                [currentDensityIndex];
+        final int defaultDensity = displayDensity.getDefaultDensityForDefaultDisplay();
+        return defaultDensity == currentDensity;
+    }
+
+    private void updateOrientation(int orientation) {
+        switch (orientation) {
+            case Configuration.ORIENTATION_LANDSCAPE: {
+                mIllustrationLottie = null;
+                break;
+            }
+            case Configuration.ORIENTATION_PORTRAIT: {
+                if (mShouldShowLottie) {
+                    mIllustrationLottie = mView.findViewById(R.id.illustration_lottie);
+                }
+                break;
+            }
+            default:
+                Log.e(TAG, "Error unhandled configuration change");
+                break;
+        }
+    }
+
+    private void startIconAnimation() {
+        if (mIconAnimationDrawable != null) {
+            mIconAnimationDrawable.start();
+        }
+    }
+
+    private void stopIconAnimation() {
+        mAnimationCancelled = true;
+        if (mIconAnimationDrawable != null) {
+            mIconAnimationDrawable.stop();
+        }
+    }
+
+    private int getCurrentStage() {
+        EnrollmentProgress progressLiveData = mProgressViewModel.getProgressLiveData().getValue();
+
+        if (progressLiveData == null || progressLiveData.getSteps() == -1) {
+            return STAGE_UNKNOWN;
+        }
+
+        final int progressSteps = progressLiveData.getSteps() - progressLiveData.getRemaining();
+        if (progressSteps < getStageThresholdSteps(0)) {
+            return STAGE_CENTER;
+        } else if (progressSteps < getStageThresholdSteps(1)) {
+            return STAGE_GUIDED;
+        } else if (progressSteps < getStageThresholdSteps(2)) {
+            return STAGE_FINGERTIP;
+        } else if (progressSteps < getStageThresholdSteps(3)) {
+            return STAGE_LEFT_EDGE;
+        } else {
+            return STAGE_RIGHT_EDGE;
+        }
+    }
+
+    private boolean isStageHalfCompleted() {
+
+        EnrollmentProgress progressLiveData = mProgressViewModel.getProgressLiveData().getValue();
+        if (progressLiveData == null || progressLiveData.getSteps() == -1) {
+            return false;
+        }
+
+        final int progressSteps = progressLiveData.getSteps() - progressLiveData.getRemaining();
+        int prevThresholdSteps = 0;
+        for (int i = 0; i < mEnrollingViewModel.getEnrollStageCount(); i++) {
+            final int thresholdSteps = getStageThresholdSteps(i);
+            if (progressSteps >= prevThresholdSteps && progressSteps < thresholdSteps) {
+                final int adjustedProgress = progressSteps - prevThresholdSteps;
+                final int adjustedThreshold = thresholdSteps - prevThresholdSteps;
+                return adjustedProgress >= adjustedThreshold / 2;
+            }
+            prevThresholdSteps = thresholdSteps;
+        }
+
+        // After last enrollment step.
+        return true;
+    }
+
+    private int getStageThresholdSteps(int index) {
+
+        EnrollmentProgress progressLiveData = mProgressViewModel.getProgressLiveData().getValue();
+
+        if (progressLiveData == null || progressLiveData.getSteps() == -1) {
+            Log.w(TAG, "getStageThresholdSteps: Enrollment not started yet");
+            return 1;
+        }
+        return Math.round(progressLiveData.getSteps()
+                * mEnrollingViewModel.getEnrollStageThreshold(index));
+    }
+
+    private void showIconTouchDialog() {
+        mIconTouchCount = 0;
+        //TODO EnrollingActivity should observe live data and add dialog fragment
+        mEnrollingViewModel.onIconTouchDialogShow();
+    }
+
+    private void configureEnrollmentStage(CharSequence description, @RawRes int lottie) {
+        final GlifLayoutHelper glifLayoutHelper = new GlifLayoutHelper(getActivity(),
+                (GlifLayout) mView);
+        glifLayoutHelper.setDescriptionText(description);
+        LottieCompositionFactory.fromRawRes(getActivity(), lottie)
+                .addListener((c) -> {
+                    mIllustrationLottie.setComposition(c);
+                    mIllustrationLottie.setVisibility(View.VISIBLE);
+                    mIllustrationLottie.playAnimation();
+                });
+    }
+
+    private final Runnable mShowDialogRunnable = new Runnable() {
+        @Override
+        public void run() {
+            showIconTouchDialog();
+        }
+    };
+
+    private final Animatable2.AnimationCallback mIconAnimationCallback =
+            new Animatable2.AnimationCallback() {
+                @Override
+                public void onAnimationEnd(Drawable d) {
+                    if (mAnimationCancelled) {
+                        return;
+                    }
+
+                    // Start animation after it has ended.
+                    mProgressBar.post(new Runnable() {
+                        @Override
+                        public void run() {
+                            startIconAnimation();
+                        }
+                    });
+                }
+            };
+
+}
diff --git a/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollFinishFragment.java b/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollFinishFragment.java
new file mode 100644
index 0000000..02aa5f2
--- /dev/null
+++ b/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollFinishFragment.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.biometrics2.ui.view;
+
+import android.app.Activity;
+import android.content.Context;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentActivity;
+import androidx.lifecycle.ViewModelProvider;
+
+import com.android.settings.R;
+import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollFinishViewModel;
+
+import com.google.android.setupcompat.template.FooterBarMixin;
+import com.google.android.setupcompat.template.FooterButton;
+import com.google.android.setupdesign.GlifLayout;
+
+
+/**
+ * Fragment which concludes fingerprint enrollment.
+ */
+public class FingerprintEnrollFinishFragment extends Fragment {
+
+    private static final String TAG = FingerprintEnrollFinishFragment.class.getSimpleName();
+
+    private FingerprintEnrollFinishViewModel mFingerprintEnrollFinishViewModel;
+    private boolean mCanAssumeSfps;
+
+    private View mView;
+    private FooterBarMixin mFooterBarMixin;
+
+    private final View.OnClickListener mAddButtonClickListener =
+            (v) -> mFingerprintEnrollFinishViewModel.onAddButtonClick();
+    private final View.OnClickListener mNextButtonClickListener =
+            (v) -> mFingerprintEnrollFinishViewModel.onNextButtonClick();
+
+    @Override
+    public void onAttach(@NonNull Context context) {
+        super.onAttach(context);
+        final FragmentActivity activity = getActivity();
+        final ViewModelProvider provider = new ViewModelProvider(activity);
+        mFingerprintEnrollFinishViewModel = provider.get(FingerprintEnrollFinishViewModel.class);
+    }
+
+    @Override
+    public void onCreate(@Nullable Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        mCanAssumeSfps = mFingerprintEnrollFinishViewModel.canAssumeSfps();
+    }
+
+    @Nullable
+    @Override
+    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
+            @Nullable Bundle savedInstanceState) {
+
+        if (mCanAssumeSfps) {
+            mView = inflater.inflate(R.layout.sfps_enroll_finish, container, false);
+        } else {
+            mView = inflater.inflate(R.layout.fingerprint_enroll_finish, container, false);
+        }
+
+        final Activity activity = getActivity();
+        final GlifLayoutHelper glifLayoutHelper = new GlifLayoutHelper(activity,
+                (GlifLayout) mView);
+
+        glifLayoutHelper.setHeaderText(R.string.security_settings_fingerprint_enroll_finish_title);
+        glifLayoutHelper.setDescriptionText(
+                R.string.security_settings_fingerprint_enroll_finish_v2_message);
+
+        final int maxEnrollments = mFingerprintEnrollFinishViewModel.getMaxFingerprints();
+        final int enrolled = mFingerprintEnrollFinishViewModel.getNumOfEnrolledFingerprintsSize();
+        if (mCanAssumeSfps) {
+            if (enrolled < maxEnrollments) {
+                glifLayoutHelper.setDescriptionText(R.string
+                        .security_settings_fingerprint_enroll_finish_v2_add_fingerprint_message);
+            }
+        }
+
+        mFooterBarMixin = ((GlifLayout) mView).getMixin(FooterBarMixin.class);
+        mFooterBarMixin.setSecondaryButton(
+                new FooterButton.Builder(getActivity())
+                        .setText(R.string.fingerprint_enroll_button_add)
+                        .setButtonType(FooterButton.ButtonType.SKIP)
+                        .setTheme(R.style.SudGlifButton_Secondary)
+                        .build()
+        );
+
+        mFooterBarMixin.setPrimaryButton(
+                new FooterButton.Builder(getActivity())
+                        .setText(R.string.security_settings_fingerprint_enroll_done)
+                        .setListener(mNextButtonClickListener)
+                        .setButtonType(FooterButton.ButtonType.NEXT)
+                        .setTheme(R.style.SudGlifButton_Primary)
+                        .build()
+        );
+
+        FooterButton addButton = mFooterBarMixin.getSecondaryButton();
+        if (enrolled >= maxEnrollments) {
+            addButton.setVisibility(View.INVISIBLE);
+        } else {
+            addButton.setOnClickListener(mAddButtonClickListener);
+        }
+
+        return mView;
+    }
+
+}
diff --git a/src/com/android/settings/biometrics2/ui/view/GlifLayoutHelper.java b/src/com/android/settings/biometrics2/ui/view/GlifLayoutHelper.java
index a1645d2..814f579 100644
--- a/src/com/android/settings/biometrics2/ui/view/GlifLayoutHelper.java
+++ b/src/com/android/settings/biometrics2/ui/view/GlifLayoutHelper.java
@@ -67,4 +67,16 @@
             mGlifLayout.setDescriptionText(description);
         }
     }
+
+    /**
+     * Sets description resId to GlifLayout
+     */
+    public void setDescriptionText(int resId) {
+        CharSequence previousDescription = mGlifLayout.getDescriptionText();
+        CharSequence description = mActivity.getString(resId);
+        // Prevent a11y for re-reading the same string
+        if (!TextUtils.equals(previousDescription, description)) {
+            mGlifLayout.setDescriptionText(resId);
+        }
+    }
 }
diff --git a/src/com/android/settings/biometrics2/ui/view/IconTouchDialog.java b/src/com/android/settings/biometrics2/ui/view/IconTouchDialog.java
new file mode 100644
index 0000000..38d6a5b
--- /dev/null
+++ b/src/com/android/settings/biometrics2/ui/view/IconTouchDialog.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.biometrics2.ui.view;
+
+import android.app.Dialog;
+import android.app.settings.SettingsEnums;
+import android.content.DialogInterface;
+import android.os.Bundle;
+
+import androidx.appcompat.app.AlertDialog;
+
+import com.android.settings.R;
+import com.android.settings.core.instrumentation.InstrumentedDialogFragment;
+
+/**
+ * Icon Touch dialog
+ */
+public class IconTouchDialog extends InstrumentedDialogFragment {
+
+//    private FingerprintEnrollEnrollingViewModel mViewModel;
+//
+//    @Override
+//    public void onAttach(Context context) {
+//        mViewModel = new ViewModelProvider(getActivity()).get(
+//                FingerprintEnrollEnrollingViewModel.class);
+//        super.onAttach(context);
+//    }
+
+    @Override
+    public Dialog onCreateDialog(Bundle savedInstanceState) {
+        AlertDialog.Builder builder = new AlertDialog.Builder(getActivity(),
+                R.style.Theme_AlertDialog);
+        builder.setTitle(R.string.security_settings_fingerprint_enroll_touch_dialog_title)
+                .setMessage(R.string.security_settings_fingerprint_enroll_touch_dialog_message)
+                .setPositiveButton(R.string.security_settings_fingerprint_enroll_dialog_ok,
+                        new DialogInterface.OnClickListener() {
+                            @Override
+                            public void onClick(DialogInterface dialog, int which) {
+                                dialog.dismiss();
+                            }
+                        });
+        return builder.create();
+    }
+
+    @Override
+    public int getMetricsCategory() {
+        return SettingsEnums.DIALOG_FINGERPRINT_ICON_TOUCH;
+    }
+}
diff --git a/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollEnrollingViewModel.java b/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollEnrollingViewModel.java
new file mode 100644
index 0000000..058b50b
--- /dev/null
+++ b/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollEnrollingViewModel.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.biometrics2.ui.viewmodel;
+
+import android.app.Application;
+import android.os.VibrationAttributes;
+import android.os.VibrationEffect;
+import android.util.Log;
+import android.view.accessibility.AccessibilityManager;
+
+import androidx.lifecycle.AndroidViewModel;
+import androidx.lifecycle.DefaultLifecycleObserver;
+import androidx.lifecycle.MutableLiveData;
+
+import com.android.settings.biometrics2.data.repository.AccessibilityRepository;
+import com.android.settings.biometrics2.data.repository.FingerprintRepository;
+import com.android.settings.biometrics2.data.repository.VibratorRepository;
+import com.android.settings.biometrics2.ui.model.EnrollmentRequest;
+
+/**
+ * ViewModel explaining the fingerprint enrolling page
+ */
+public class FingerprintEnrollEnrollingViewModel extends AndroidViewModel
+        implements DefaultLifecycleObserver {
+
+    private static final String TAG = FingerprintEnrollEnrollingViewModel.class.getSimpleName();
+    private static final boolean DEBUG = false;
+
+    private static final VibrationEffect VIBRATE_EFFECT_ERROR =
+            VibrationEffect.createWaveform(new long[]{0, 5, 55, 60}, -1);
+    private static final VibrationAttributes FINGERPRINT_ENROLLING_SONFICATION_ATTRIBUTES =
+            VibrationAttributes.createForUsage(VibrationAttributes.USAGE_ACCESSIBILITY);
+
+    //Enrolling skip
+    public static final int FINGERPRINT_ENROLL_ENROLLING_ACTION_SKIP = 0;
+
+    //Icon touch dialog show
+    public static final int FINGERPRINT_ENROLL_ENROLLING_ACTION_SHOW_DIALOG = 0;
+
+    //Icon touch dialog dismiss
+    public static final int FINGERPRINT_ENROLL_ENROLLING_ACTION_DISMISS_DIALOG = 1;
+
+    private final FingerprintRepository mFingerprintRepository;
+    private final AccessibilityRepository mAccessibilityRepository;
+    private final VibratorRepository mVibratorRepository;
+
+    private EnrollmentRequest mEnrollmentRequest = null;
+    private final MutableLiveData<Integer> mEnrollingLiveData = new MutableLiveData<>();
+    private final MutableLiveData<Integer> mIconTouchDialogLiveData = new MutableLiveData<>();
+
+
+    public FingerprintEnrollEnrollingViewModel(Application application,
+            FingerprintRepository fingerprintRepository,
+            AccessibilityRepository accessibilityRepository,
+            VibratorRepository vibratorRepository) {
+        super(application);
+        mFingerprintRepository = fingerprintRepository;
+        mAccessibilityRepository = accessibilityRepository;
+        mVibratorRepository = vibratorRepository;
+    }
+
+    /**
+     * User clicks skip button
+     */
+    public void onSkipButtonClick() {
+        final int action = FINGERPRINT_ENROLL_ENROLLING_ACTION_SKIP;
+        if (DEBUG) {
+            Log.d(TAG, "onSkipButtonClick, post action " + action);
+        }
+        mEnrollingLiveData.postValue(action);
+    }
+
+    /**
+     * Icon touch dialog show
+     */
+    public void onIconTouchDialogShow() {
+        final int action = FINGERPRINT_ENROLL_ENROLLING_ACTION_SHOW_DIALOG;
+        if (DEBUG) {
+            Log.d(TAG, "onIconTouchDialogShow, post action " + action);
+        }
+        mIconTouchDialogLiveData.postValue(action);
+    }
+
+    /**
+     * Icon touch dialog dismiss
+     */
+    public void onIconTouchDialogDismiss() {
+        final int action = FINGERPRINT_ENROLL_ENROLLING_ACTION_DISMISS_DIALOG;
+        if (DEBUG) {
+            Log.d(TAG, "onIconTouchDialogDismiss, post action " + action);
+        }
+        mIconTouchDialogLiveData.postValue(action);
+    }
+
+    /**
+     * get enroll stage threshold
+     */
+    public float getEnrollStageThreshold(int index) {
+        return mFingerprintRepository.getEnrollStageThreshold(index);
+    }
+
+    /**
+     * Get enroll stage count
+     */
+    public int getEnrollStageCount() {
+        return mFingerprintRepository.getEnrollStageCount();
+    }
+
+    /**
+     * The first sensor type is UDFPS sensor or not
+     */
+    public boolean canAssumeUdfps() {
+        return mFingerprintRepository.canAssumeUdfps();
+    }
+
+    /**
+     * The first sensor type is SFPS sensor or not
+     */
+    public boolean canAssumeSfps() {
+        return mFingerprintRepository.canAssumeSfps();
+    }
+
+    /**
+     * Requests interruption of the accessibility feedback from all accessibility services.
+     */
+    public void clearTalkback() {
+        mAccessibilityRepository.interrupt();
+    }
+
+    /**
+     * Returns if the {@link AccessibilityManager} is enabled.
+     *
+     * @return True if this {@link AccessibilityManager} is enabled, false otherwise.
+     */
+    public boolean isAccessibilityEnabled() {
+        return mAccessibilityRepository.isEnabled();
+    }
+
+    /**
+     * Like {@link #vibrate(VibrationEffect, VibrationAttributes)}, but allows the
+     * caller to specify the vibration is owned by someone else and set a reason for vibration.
+     */
+    public void vibrateError(int uid, String opPkg, String reason) {
+        mVibratorRepository.vibrate(uid, opPkg, VIBRATE_EFFECT_ERROR, reason,
+                FINGERPRINT_ENROLLING_SONFICATION_ATTRIBUTES);
+    }
+}
diff --git a/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollFinishViewModel.java b/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollFinishViewModel.java
new file mode 100644
index 0000000..2cf0b47
--- /dev/null
+++ b/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollFinishViewModel.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.biometrics2.ui.viewmodel;
+
+
+import android.app.Application;
+import android.content.ComponentName;
+import android.content.pm.PackageManager;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.AndroidViewModel;
+import androidx.lifecycle.MutableLiveData;
+
+import com.android.settings.biometrics2.data.repository.FingerprintRepository;
+import com.android.settings.biometrics2.data.repository.PackageManagerRepository;
+
+/**
+ * Finish ViewModel handles the state of the fingerprint renroll final stage
+ */
+public class FingerprintEnrollFinishViewModel extends AndroidViewModel {
+
+    private static final String TAG = FingerprintEnrollFinishViewModel.class.getSimpleName();
+
+    private static final String FINGERPRINT_SUGGESTION_ACTIVITY =
+            "com.android.settings.SetupFingerprintSuggestionActivity";
+
+    private static final int ACTION_NONE = -1;
+    private static final int ACTION_ADD_BUTTON_CLICK = 0;
+    private static final int ACTION_NEXT_BUTTON_CLICK = 1;
+
+    private final FingerprintRepository mFingerprintRepository;
+    private final PackageManagerRepository mPackageManagerRepository;
+    private final int mUserId;
+
+    private final MutableLiveData<Integer> mActionLiveData = new MutableLiveData<>();
+
+    public FingerprintEnrollFinishViewModel(@NonNull Application application,
+            FingerprintRepository fingerprintRepository,
+            PackageManagerRepository packageManagerRepository,
+            int userId) {
+        super(application);
+        mFingerprintRepository = fingerprintRepository;
+        mPackageManagerRepository = packageManagerRepository;
+        mUserId = userId;
+        mActionLiveData.setValue(ACTION_NONE);
+    }
+
+    /**
+     * The first sensor type is Side fps sensor or not
+     */
+    public boolean canAssumeSfps() {
+        return mFingerprintRepository.canAssumeSfps();
+    }
+
+    /**
+     * Get number of fingerprints that this user enrolled.
+     */
+    public int getNumOfEnrolledFingerprintsSize() {
+        return mFingerprintRepository.getNumOfEnrolledFingerprintsSize(mUserId);
+    }
+
+    /**
+     * Get max possible number of fingerprints for a user
+     */
+    public int getMaxFingerprints() {
+        return mFingerprintRepository.getMaxFingerprints();
+    }
+
+    /**
+     * Clear life data
+     */
+    public void clearLiveData() {
+        mActionLiveData.setValue(ACTION_NONE);
+    }
+
+    /**
+     * Handle add button Click
+     */
+    public void onAddButtonClick() {
+        mActionLiveData.postValue(ACTION_ADD_BUTTON_CLICK);
+    }
+
+    /**
+     * Handle next button Click
+     */
+    public void onNextButtonClick() {
+        updateFingerprintSuggestionEnableState();
+        mActionLiveData.postValue(ACTION_NEXT_BUTTON_CLICK);
+    }
+
+    /**
+     * Handle back key pressed
+     */
+    public void onBackKeyPressed() {
+        updateFingerprintSuggestionEnableState();
+    }
+
+    private void updateFingerprintSuggestionEnableState() {
+        final int enrollNum = mFingerprintRepository.getNumOfEnrolledFingerprintsSize(mUserId);
+        final int flag = (enrollNum == 1) ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED
+                : PackageManager.COMPONENT_ENABLED_STATE_DISABLED;
+
+        ComponentName componentName = new ComponentName(getApplication(),
+                FINGERPRINT_SUGGESTION_ACTIVITY);
+
+        mPackageManagerRepository.setComponentEnabledSetting(componentName, flag,
+                PackageManager.DONT_KILL_APP);
+    }
+}
diff --git a/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollProgressViewModel.java b/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollProgressViewModel.java
index cbc74c0..532e2cc 100644
--- a/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollProgressViewModel.java
+++ b/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollProgressViewModel.java
@@ -56,6 +56,10 @@
     private final MutableLiveData<EnrollmentStatusMessage> mErrorMessageLiveData =
             new MutableLiveData<>();
 
+    private final MutableLiveData<Boolean> mAcquireLiveData = new MutableLiveData<>();
+    private final MutableLiveData<Integer> mPointerDownLiveData = new MutableLiveData<>();
+    private final MutableLiveData<Integer> mPointerUpLiveData = new MutableLiveData<>();
+
     private byte[] mToken = null;
     private final int mUserId;
 
@@ -86,6 +90,21 @@
         public void onEnrollmentError(int errMsgId, CharSequence errString) {
             mErrorMessageLiveData.postValue(new EnrollmentStatusMessage(errMsgId, errString));
         }
+
+        @Override
+        public void onAcquired(boolean isAcquiredGood) {
+            mAcquireLiveData.postValue(isAcquiredGood);
+        }
+
+        @Override
+        public void onPointerDown(int sensorId) {
+            mPointerDownLiveData.postValue(sensorId);
+        }
+
+        @Override
+        public void onPointerUp(int sensorId) {
+            mPointerUpLiveData.postValue(sensorId);
+        }
     };
 
     public FingerprintEnrollProgressViewModel(@NonNull Application application,
@@ -132,6 +151,19 @@
         return mErrorMessageLiveData;
     }
 
+    public MutableLiveData<Boolean> getAcquireLiveData() {
+        return mAcquireLiveData;
+    }
+
+    public MutableLiveData<Integer> getPointerDownLiveData() {
+        return mPointerDownLiveData;
+    }
+
+    public MutableLiveData<Integer> getPointerUpLiveData() {
+        return mPointerUpLiveData;
+    }
+
+
     /**
      * Starts enrollment and return latest isEnrolling() result
      */
diff --git a/src/com/android/settings/fuelgauge/BatteryBroadcastReceiver.java b/src/com/android/settings/fuelgauge/BatteryBroadcastReceiver.java
index 5e432cf..ecc4ea0 100644
--- a/src/com/android/settings/fuelgauge/BatteryBroadcastReceiver.java
+++ b/src/com/android/settings/fuelgauge/BatteryBroadcastReceiver.java
@@ -20,6 +20,7 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
+import android.hardware.usb.UsbManager;
 import android.os.BatteryManager;
 import android.os.PowerManager;
 import android.util.Log;
@@ -99,6 +100,7 @@
         intentFilter.addAction(Intent.ACTION_BATTERY_CHANGED);
         intentFilter.addAction(PowerManager.ACTION_POWER_SAVE_MODE_CHANGED);
         intentFilter.addAction(BatteryUtils.BYPASS_DOCK_DEFENDER_ACTION);
+        intentFilter.addAction(UsbManager.ACTION_USB_PORT_COMPLIANCE_CHANGED);
 
         final Intent intent = mContext.registerReceiver(this, intentFilter,
                 Context.RECEIVER_EXPORTED);
@@ -110,33 +112,36 @@
     }
 
     private void updateBatteryStatus(Intent intent, boolean forceUpdate) {
-        if (intent != null && mBatteryListener != null) {
-            if (Intent.ACTION_BATTERY_CHANGED.equals(intent.getAction())) {
-                final String batteryLevel = Utils.getBatteryPercentage(intent);
-                final String batteryStatus =
-                        Utils.getBatteryStatus(mContext, intent, /* compactStatus= */ false);
-                final int batteryHealth = intent.getIntExtra(
-                        BatteryManager.EXTRA_HEALTH, BatteryManager.BATTERY_HEALTH_UNKNOWN);
-                if (!Utils.isBatteryPresent(intent)) {
-                    Log.w(TAG, "Problem reading the battery meter.");
-                    mBatteryListener.onBatteryChanged(BatteryUpdateType.BATTERY_NOT_PRESENT);
-                } else if (forceUpdate) {
-                    mBatteryListener.onBatteryChanged(BatteryUpdateType.MANUAL);
-                } else if (batteryHealth != mBatteryHealth) {
-                    mBatteryListener.onBatteryChanged(BatteryUpdateType.BATTERY_HEALTH);
-                } else if(!batteryLevel.equals(mBatteryLevel)) {
-                    mBatteryListener.onBatteryChanged(BatteryUpdateType.BATTERY_LEVEL);
-                } else if (!batteryStatus.equals(mBatteryStatus)) {
-                    mBatteryListener.onBatteryChanged(BatteryUpdateType.BATTERY_STATUS);
-                }
-                mBatteryLevel = batteryLevel;
-                mBatteryStatus = batteryStatus;
-                mBatteryHealth = batteryHealth;
-            } else if (PowerManager.ACTION_POWER_SAVE_MODE_CHANGED.equals(intent.getAction())) {
-                mBatteryListener.onBatteryChanged(BatteryUpdateType.BATTERY_SAVER);
-            } else if (BatteryUtils.BYPASS_DOCK_DEFENDER_ACTION.equals(intent.getAction())) {
+        if (intent == null || mBatteryListener == null) {
+            return;
+        }
+        final String action = intent.getAction();
+        if (Intent.ACTION_BATTERY_CHANGED.equals(action)) {
+            final String batteryLevel = Utils.getBatteryPercentage(intent);
+            final String batteryStatus =
+                    Utils.getBatteryStatus(mContext, intent, /* compactStatus= */ false);
+            final int batteryHealth = intent.getIntExtra(
+                    BatteryManager.EXTRA_HEALTH, BatteryManager.BATTERY_HEALTH_UNKNOWN);
+            if (!Utils.isBatteryPresent(intent)) {
+                Log.w(TAG, "Problem reading the battery meter.");
+                mBatteryListener.onBatteryChanged(BatteryUpdateType.BATTERY_NOT_PRESENT);
+            } else if (forceUpdate) {
+                mBatteryListener.onBatteryChanged(BatteryUpdateType.MANUAL);
+            } else if (batteryHealth != mBatteryHealth) {
+                mBatteryListener.onBatteryChanged(BatteryUpdateType.BATTERY_HEALTH);
+            } else if(!batteryLevel.equals(mBatteryLevel)) {
+                mBatteryListener.onBatteryChanged(BatteryUpdateType.BATTERY_LEVEL);
+            } else if (!batteryStatus.equals(mBatteryStatus)) {
                 mBatteryListener.onBatteryChanged(BatteryUpdateType.BATTERY_STATUS);
             }
+            mBatteryLevel = batteryLevel;
+            mBatteryStatus = batteryStatus;
+            mBatteryHealth = batteryHealth;
+        } else if (PowerManager.ACTION_POWER_SAVE_MODE_CHANGED.equals(action)) {
+            mBatteryListener.onBatteryChanged(BatteryUpdateType.BATTERY_SAVER);
+        } else if (BatteryUtils.BYPASS_DOCK_DEFENDER_ACTION.equals(action)
+                || UsbManager.ACTION_USB_PORT_COMPLIANCE_CHANGED.equals(action)) {
+            mBatteryListener.onBatteryChanged(BatteryUpdateType.BATTERY_STATUS);
         }
     }
 }
diff --git a/src/com/android/settings/fuelgauge/batterytip/BatteryTipLoader.java b/src/com/android/settings/fuelgauge/batterytip/BatteryTipLoader.java
index da646cb..437ffda 100644
--- a/src/com/android/settings/fuelgauge/batterytip/BatteryTipLoader.java
+++ b/src/com/android/settings/fuelgauge/batterytip/BatteryTipLoader.java
@@ -26,6 +26,7 @@
 import com.android.settings.fuelgauge.batterytip.detectors.BatteryDefenderDetector;
 import com.android.settings.fuelgauge.batterytip.detectors.DockDefenderDetector;
 import com.android.settings.fuelgauge.batterytip.detectors.HighUsageDetector;
+import com.android.settings.fuelgauge.batterytip.detectors.IncompatibleChargerDetector;
 import com.android.settings.fuelgauge.batterytip.detectors.LowBatteryDetector;
 import com.android.settings.fuelgauge.batterytip.detectors.SmartBatteryDetector;
 import com.android.settings.fuelgauge.batterytip.tips.BatteryTip;
@@ -59,15 +60,15 @@
         final List<BatteryTip> tips = new ArrayList<>();
         final BatteryTipPolicy policy = new BatteryTipPolicy(getContext());
         final BatteryInfo batteryInfo = mBatteryUtils.getBatteryInfo(TAG);
-        final Context context = getContext();
+        final Context context = getContext().getApplicationContext();
 
         tips.add(new LowBatteryDetector(context, policy, batteryInfo).detect());
         tips.add(new HighUsageDetector(context, policy, mBatteryUsageStats, batteryInfo).detect());
         tips.add(new SmartBatteryDetector(
                 context, policy, batteryInfo, context.getContentResolver()).detect());
-        tips.add(new BatteryDefenderDetector(
-                batteryInfo, context.getApplicationContext()).detect());
-        tips.add(new DockDefenderDetector(batteryInfo, context.getApplicationContext()).detect());
+        tips.add(new BatteryDefenderDetector(batteryInfo, context).detect());
+        tips.add(new DockDefenderDetector(batteryInfo, context).detect());
+        tips.add(new IncompatibleChargerDetector(context, batteryInfo).detect());
         Collections.sort(tips);
         return tips;
     }
diff --git a/src/com/android/settings/fuelgauge/batterytip/detectors/IncompatibleChargerDetector.java b/src/com/android/settings/fuelgauge/batterytip/detectors/IncompatibleChargerDetector.java
new file mode 100644
index 0000000..483e37d
--- /dev/null
+++ b/src/com/android/settings/fuelgauge/batterytip/detectors/IncompatibleChargerDetector.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.settings.fuelgauge.batterytip.detectors;
+
+import android.content.Context;
+import android.util.Log;
+
+import com.android.settings.fuelgauge.BatteryInfo;
+import com.android.settings.fuelgauge.batterytip.tips.BatteryTip;
+import com.android.settings.fuelgauge.batterytip.tips.IncompatibleChargerTip;
+import com.android.settingslib.Utils;
+
+/** Detect whether it is in the incompatible charging state */
+public final class IncompatibleChargerDetector implements BatteryTipDetector {
+    private static final String TAG = "IncompatibleChargerDetector";
+
+    private final Context mContext;
+    private final BatteryInfo mBatteryInfo;
+
+    public IncompatibleChargerDetector(Context context, BatteryInfo batteryInfo) {
+        mContext = context;
+        mBatteryInfo = batteryInfo;
+    }
+
+    @Override
+    public BatteryTip detect() {
+        int state = BatteryTip.StateType.INVISIBLE;
+        boolean isIncompatibleCharging = false;
+
+        // Check incompatible charging state if the device is plugged.
+        if (mBatteryInfo.pluggedStatus != 0) {
+            isIncompatibleCharging = Utils.containsIncompatibleChargers(mContext, TAG);
+            if (isIncompatibleCharging) {
+                state = BatteryTip.StateType.NEW;
+            }
+        }
+        Log.d(TAG, "detect() state= " + state + " isIncompatibleCharging: "
+                + isIncompatibleCharging);
+        return new IncompatibleChargerTip(state);
+    }
+}
diff --git a/tests/robotests/src/com/android/settings/fuelgauge/BatteryBroadcastReceiverTest.java b/tests/robotests/src/com/android/settings/fuelgauge/BatteryBroadcastReceiverTest.java
index 4bfb15b..a829c40 100644
--- a/tests/robotests/src/com/android/settings/fuelgauge/BatteryBroadcastReceiverTest.java
+++ b/tests/robotests/src/com/android/settings/fuelgauge/BatteryBroadcastReceiverTest.java
@@ -21,6 +21,7 @@
 
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.spy;
@@ -29,6 +30,8 @@
 
 import android.content.Context;
 import android.content.Intent;
+import android.content.IntentFilter;
+import android.hardware.usb.UsbManager;
 import android.os.BatteryManager;
 import android.os.PowerManager;
 
@@ -37,6 +40,7 @@
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 import org.robolectric.RobolectricTestRunner;
@@ -55,7 +59,6 @@
     private BatteryBroadcastReceiver mBatteryBroadcastReceiver;
     private Context mContext;
     private Intent mChargingIntent;
-    private Intent mDockDefenderBypassIntent;
 
     @Before
     public void setUp() {
@@ -73,12 +76,10 @@
         mChargingIntent.putExtra(BatteryManager.EXTRA_SCALE, BATTERY_INTENT_SCALE);
         mChargingIntent
                 .putExtra(BatteryManager.EXTRA_STATUS, BatteryManager.BATTERY_STATUS_CHARGING);
-        mDockDefenderBypassIntent = new Intent(BatteryUtils.BYPASS_DOCK_DEFENDER_ACTION);
-
     }
 
     @Test
-    public void testOnReceive_batteryLevelChanged_dataUpdated() {
+    public void onReceive_batteryLevelChanged_dataUpdated() {
         mBatteryBroadcastReceiver.onReceive(mContext, mChargingIntent);
 
         assertThat(mBatteryBroadcastReceiver.mBatteryLevel)
@@ -89,7 +90,7 @@
     }
 
     @Test
-    public void testOnReceive_batteryHealthChanged_dataUpdated() {
+    public void onReceive_batteryHealthChanged_dataUpdated() {
         mChargingIntent
                 .putExtra(BatteryManager.EXTRA_HEALTH, BatteryManager.BATTERY_HEALTH_OVERHEAT);
         mBatteryBroadcastReceiver.onReceive(mContext, mChargingIntent);
@@ -109,7 +110,7 @@
     }
 
     @Test
-    public void testOnReceive_powerSaveModeChanged_listenerInvoked() {
+    public void onReceive_powerSaveModeChanged_listenerInvoked() {
         mBatteryBroadcastReceiver.onReceive(mContext,
                 new Intent(PowerManager.ACTION_POWER_SAVE_MODE_CHANGED));
 
@@ -117,7 +118,7 @@
     }
 
     @Test
-    public void testOnReceive_batteryDataNotChanged_listenerNotInvoked() {
+    public void onReceive_batteryDataNotChanged_listenerNotInvoked() {
         final String batteryLevel = Utils.getBatteryPercentage(mChargingIntent);
         final String batteryStatus =
                 Utils.getBatteryStatus(mContext, mChargingIntent, /* compactStatus= */ false);
@@ -134,14 +135,23 @@
     }
 
     @Test
-    public void testOnReceive_dockDefenderBypassed_listenerInvoked() {
-        mBatteryBroadcastReceiver.onReceive(mContext, mDockDefenderBypassIntent);
+    public void onReceive_dockDefenderBypassed_listenerInvoked() {
+        mBatteryBroadcastReceiver.onReceive(mContext,
+                new Intent(BatteryUtils.BYPASS_DOCK_DEFENDER_ACTION));
 
         verify(mBatteryListener).onBatteryChanged(BatteryUpdateType.BATTERY_STATUS);
     }
 
     @Test
-    public void testRegister_updateBatteryStatus() {
+    public void onReceive_usbPortComplianceChanged_listenerInvoked() {
+        mBatteryBroadcastReceiver.onReceive(mContext,
+                new Intent(UsbManager.ACTION_USB_PORT_COMPLIANCE_CHANGED));
+
+        verify(mBatteryListener).onBatteryChanged(BatteryUpdateType.BATTERY_STATUS);
+    }
+
+    @Test
+    public void register_updateBatteryStatus() {
         doReturn(mChargingIntent).when(mContext).registerReceiver(any(), any(), anyInt());
 
         mBatteryBroadcastReceiver.register();
@@ -156,4 +166,23 @@
         // 2 times because register will force update the battery
         verify(mBatteryListener, times(2)).onBatteryChanged(BatteryUpdateType.MANUAL);
     }
+
+    @Test
+    public void register_registerExpectedIntent() {
+        mBatteryBroadcastReceiver.register();
+
+        ArgumentCaptor<IntentFilter> captor = ArgumentCaptor.forClass(IntentFilter.class);
+        verify(mContext).registerReceiver(
+                eq(mBatteryBroadcastReceiver),
+                captor.capture(),
+                eq(Context.RECEIVER_EXPORTED));
+        assertAction(captor, Intent.ACTION_BATTERY_CHANGED);
+        assertAction(captor, PowerManager.ACTION_POWER_SAVE_MODE_CHANGED);
+        assertAction(captor, BatteryUtils.BYPASS_DOCK_DEFENDER_ACTION);
+        assertAction(captor, UsbManager.ACTION_USB_PORT_COMPLIANCE_CHANGED);
+    }
+
+    private void assertAction(ArgumentCaptor<IntentFilter> captor, String action) {
+        assertThat(captor.getValue().hasAction(action)).isTrue();
+    }
 }
diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batterytip/BatteryTipLoaderTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batterytip/BatteryTipLoaderTest.java
index 0e1c458..5b0ae04 100644
--- a/tests/robotests/src/com/android/settings/fuelgauge/batterytip/BatteryTipLoaderTest.java
+++ b/tests/robotests/src/com/android/settings/fuelgauge/batterytip/BatteryTipLoaderTest.java
@@ -53,6 +53,7 @@
             BatteryTip.TipType.LOW_BATTERY,
             BatteryTip.TipType.BATTERY_DEFENDER,
             BatteryTip.TipType.DOCK_DEFENDER,
+            BatteryTip.TipType.INCOMPATIBLE_CHARGER,
             BatteryTip.TipType.HIGH_DEVICE_USAGE,
             BatteryTip.TipType.SMART_BATTERY_MANAGER};
     @Mock(answer = Answers.RETURNS_DEEP_STUBS)
diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batterytip/detectors/IncompatibleChargerDetectorTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batterytip/detectors/IncompatibleChargerDetectorTest.java
new file mode 100644
index 0000000..1dfe6e2
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/fuelgauge/batterytip/detectors/IncompatibleChargerDetectorTest.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.settings.fuelgauge.batterytip.detectors;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.hardware.usb.UsbManager;
+import android.hardware.usb.UsbPort;
+import android.hardware.usb.UsbPortStatus;
+
+import com.android.settings.fuelgauge.BatteryInfo;
+import com.android.settings.fuelgauge.batterytip.tips.BatteryTip;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@RunWith(RobolectricTestRunner.class)
+public final class IncompatibleChargerDetectorTest {
+
+    @Mock private BatteryInfo mBatteryInfo;
+    @Mock private UsbPort mUsbPort;
+    @Mock private UsbManager mUsbManager;
+    @Mock private UsbPortStatus mUsbPortStatus;
+
+    private Context mContext;
+    private IncompatibleChargerDetector mIncompatibleChargerDetector;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        mContext = spy(RuntimeEnvironment.application);
+        when(mContext.getSystemService(UsbManager.class)).thenReturn(mUsbManager);
+        mIncompatibleChargerDetector =
+                new IncompatibleChargerDetector(mContext, mBatteryInfo);
+    }
+
+    @Test
+    public void detect_unplugDevice_shouldNotShowTip() {
+        mBatteryInfo.pluggedStatus = 0;
+
+        BatteryTip batteryTip = mIncompatibleChargerDetector.detect();
+
+        assertThat(batteryTip.isVisible()).isFalse();
+        assertThat(batteryTip.getState()).isEqualTo(BatteryTip.StateType.INVISIBLE);
+    }
+
+    @Test
+    public void detect_plugDeviceWithoutIncompatibleCharger_shouldNotShowTip() {
+        mBatteryInfo.pluggedStatus = 1;
+
+        BatteryTip batteryTip = mIncompatibleChargerDetector.detect();
+
+        assertThat(batteryTip.isVisible()).isFalse();
+        assertThat(batteryTip.getState()).isEqualTo(BatteryTip.StateType.INVISIBLE);
+    }
+
+    @Test
+    public void detect_plugDeviceWithIncompatibleCharger_showTip() {
+        mBatteryInfo.pluggedStatus = 1;
+        setupIncompatibleCharging();
+
+        BatteryTip batteryTip = mIncompatibleChargerDetector.detect();
+
+        assertThat(batteryTip.isVisible()).isTrue();
+        assertThat(batteryTip.getState()).isEqualTo(BatteryTip.StateType.NEW);
+    }
+
+    private void setupIncompatibleCharging() {
+        final List<UsbPort> usbPorts = new ArrayList<>();
+        usbPorts.add(mUsbPort);
+        when(mUsbManager.getPorts()).thenReturn(usbPorts);
+        when(mUsbPort.getStatus()).thenReturn(mUsbPortStatus);
+        when(mUsbPort.supportsComplianceWarnings()).thenReturn(true);
+        when(mUsbPortStatus.isConnected()).thenReturn(true);
+        when(mUsbPortStatus.getComplianceWarnings()).thenReturn(new int[]{1});
+    }
+}