Bypass cancel error code during "Add another"

1. When user chooses "Add another" on latest fingerprint enroll page, we
   shall bypass RESULT_CANCELED from FingerprintEnrollEnrolling. This is
   because user has successfully enrolled a fingerprint before enter
   FingerprintEnrollFinish page.

2. In FingerprintEnrollFindSensor
   A. Do not call startLookingForFingerprint() if mNextClick is true
      because it is wating the result back from
      FingerprintEnrollEnrolling, and let onActivityResult() mehtod to
      handle correct behavior.
   B. Add tests for following cases
      a. Sidecar existence
      b. Activity recycled and recreate in order to get activity result.

Bug: 243701933
Bug: 243762418

Test: ROBOTEST for SetupFingerprintEnrollFindSensorTest,
   FingerprintEnrollFindSensorTest, SetupFingerprintEnrollFinishTest

Test: Test scenarios w/ and w/o always_finish_activities
  1. Enroll a fingerprint but cancel it during enrolling in SuW
  2. Enroll a fingerprint in SuW
  3. Add another fingerprint in SuW
  4. Run "Add another" but cancel it during enrolling in SuW
  5. W/o enrolled fingerprint, add first fingerprint in settings
  6. W/o enrolled fingerprint, add first fingerprint but cancel it
     during enrolling in settings
  7. W/o enrolled fingerprint, add first fingerprint and choose "Add
     another" in settings
  8. W/o enrolled fingerprint, add first fingerprint and choose "Add
     another" then cancel it during enrolling in settings
  9. W/ 1 enrolled fingerprint, add fingerprint in settings
  10. W/ 1 enrolled fingerprint, add fingerprint but cancel it during
      enrolling in settings
  11. W/ 1 enrolled fingerprint, add fingerprint and choose "Add
      another" in settings
  12. W/ 1 enrolled fingerprint, add fingerprint and choose "Add
      another" then canel it during enrolling in settings

Change-Id: I03d8d8ebc39eb34f8fc28acb5cd267e37c7a0311
diff --git a/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollFindSensor.java b/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollFindSensor.java
index fd589f4..9abf38d 100644
--- a/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollFindSensor.java
+++ b/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollFindSensor.java
@@ -134,6 +134,9 @@
             setHeaderText(R.string.security_settings_fingerprint_enroll_find_sensor_title);
             setDescriptionText(R.string.security_settings_fingerprint_enroll_find_sensor_message);
         }
+        if (savedInstanceState != null) {
+            mNextClicked = savedInstanceState.getBoolean(SAVED_STATE_IS_NEXT_CLICKED, mNextClicked);
+        }
 
         // This is an entry point for SetNewPasswordController, e.g.
         // adb shell am start -a android.app.action.SET_NEW_PASSWORD
@@ -148,11 +151,19 @@
                 // it passed in.
                 getIntent().putExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN, mToken);
 
-                startLookingForFingerprint();
+                // Do not start looking for fingerprint if this activity is re-created because it is
+                // waiting for activity result from enrolling activity.
+                if (!mNextClicked) {
+                    startLookingForFingerprint();
+                }
             });
         } else if (mToken != null) {
-            // HAT passed in from somewhere else, such as FingerprintEnrollIntroduction
-            startLookingForFingerprint();
+            // Do not start looking for fingerprint if this activity is re-created because it is
+            // waiting for activity result from enrolling activity.
+            if (!mNextClicked) {
+                // HAT passed in from somewhere else, such as FingerprintEnrollIntroduction
+                startLookingForFingerprint();
+            }
         } else {
             // There's something wrong with the enrollment flow, this should never happen.
             throw new IllegalStateException("HAT and GkPwHandle both missing...");
@@ -173,9 +184,6 @@
                 mAnimation = (FingerprintFindSensorAnimation) animationView;
             }
         }
-        if (savedInstanceState != null) {
-            mNextClicked = savedInstanceState.getBoolean(SAVED_STATE_IS_NEXT_CLICKED, mNextClicked);
-        }
     }
 
     @Override
@@ -297,6 +305,7 @@
                     return;
                 }
             }
+            mSidecar.setListener(null);
             getSupportFragmentManager().beginTransaction().remove(mSidecar).
                     commitAllowingStateLoss();
             mSidecar = null;
diff --git a/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollFinish.java b/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollFinish.java
index b606b60..c24e9f0 100644
--- a/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollFinish.java
+++ b/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollFinish.java
@@ -50,6 +50,8 @@
     static final String FINGERPRINT_SUGGESTION_ACTIVITY =
             "com.android.settings.SetupFingerprintSuggestionActivity";
 
+    private boolean mIsAddAnotherOrFinish;
+
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
@@ -108,8 +110,22 @@
     }
 
     @Override
+    protected void onStart() {
+        super.onStart();
+
+        // Reset it to false every time activity back to fg because this flag is stateless between
+        // different life cycle.
+        mIsAddAnotherOrFinish = false;
+    }
+
+    @Override
     protected void onNextButtonClick(View view) {
         updateFingerprintSuggestionEnableState();
+        finishAndToNext();
+    }
+
+    private void finishAndToNext() {
+        mIsAddAnotherOrFinish = true;
         setResult(RESULT_FINISHED);
         if (WizardManagerHelper.isAnySetupWizard(getIntent())) {
             postEnroll();
@@ -145,20 +161,21 @@
     }
 
     private void onAddAnotherButtonClick(View view) {
+        mIsAddAnotherOrFinish = true;
         startActivityForResult(getFingerprintEnrollingIntent(), BiometricUtils.REQUEST_ADD_ANOTHER);
     }
 
     @Override
     protected boolean shouldFinishWhenBackgrounded() {
-        return !isFinishing() && super.shouldFinishWhenBackgrounded();
+        return !mIsAddAnotherOrFinish && super.shouldFinishWhenBackgrounded();
     }
 
     @Override
     protected void onActivityResult(int requestCode, int resultCode, Intent data) {
         updateFingerprintSuggestionEnableState();
         if (requestCode == BiometricUtils.REQUEST_ADD_ANOTHER && resultCode != RESULT_CANCELED) {
-            setResult(resultCode, data);
-            finish();
+            // If user cancel during "Add another", just use similar flow on "Next" button
+            finishAndToNext();
         } else {
             super.onActivityResult(requestCode, resultCode, data);
         }
diff --git a/tests/robotests/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollFindSensorTest.java b/tests/robotests/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollFindSensorTest.java
index 49a16cb..fad3abf 100644
--- a/tests/robotests/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollFindSensorTest.java
+++ b/tests/robotests/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollFindSensorTest.java
@@ -19,6 +19,11 @@
 import static android.hardware.fingerprint.FingerprintSensorProperties.TYPE_REAR;
 import static android.hardware.fingerprint.FingerprintSensorProperties.TYPE_UDFPS_OPTICAL;
 
+import static com.android.settings.biometrics.BiometricEnrollBase.RESULT_FINISHED;
+import static com.android.settings.biometrics.BiometricEnrollBase.RESULT_SKIP;
+import static com.android.settings.biometrics.BiometricEnrollBase.RESULT_TIMEOUT;
+import static com.android.settings.biometrics.fingerprint.FingerprintEnrollEnrolling.TAG_SIDECAR;
+
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assertWithMessage;
 
@@ -29,6 +34,7 @@
 import static org.mockito.Mockito.verify;
 import static org.robolectric.RuntimeEnvironment.application;
 
+import android.annotation.NonNull;
 import android.app.Activity;
 import android.content.ComponentName;
 import android.content.Intent;
@@ -38,9 +44,12 @@
 import android.hardware.fingerprint.FingerprintManager.EnrollmentCallback;
 import android.hardware.fingerprint.FingerprintSensorProperties;
 import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
+import android.os.Bundle;
 import android.os.CancellationSignal;
 import android.view.View;
 
+import androidx.fragment.app.Fragment;
+
 import com.android.settings.R;
 import com.android.settings.biometrics.BiometricEnrollBase;
 import com.android.settings.password.ChooseLockSettingsHelper;
@@ -82,12 +91,7 @@
 
     private FingerprintEnrollFindSensor mActivity;
 
-    @Before
-    public void setUp() {
-        MockitoAnnotations.initMocks(this);
-        ShadowUtils.setFingerprintManager(mFingerprintManager);
-        FakeFeatureFactory.setupForTest();
-
+    private void buildActivity() {
         mActivityController = Robolectric.buildActivity(
                 FingerprintEnrollFindSensor.class,
                 new Intent()
@@ -97,6 +101,14 @@
         mActivity = mActivityController.get();
     }
 
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        ShadowUtils.setFingerprintManager(mFingerprintManager);
+        FakeFeatureFactory.setupForTest();
+        buildActivity();
+    }
+
     private void setupActivity_onRearDevice() {
         final ArrayList<FingerprintSensorPropertiesInternal> props = new ArrayList<>();
         props.add(newFingerprintSensorPropertiesInternal(TYPE_REAR));
@@ -187,7 +199,7 @@
 
         ShadowActivity shadowActivity = Shadows.shadowOf(mActivity);
         assertWithMessage("result code").that(shadowActivity.getResultCode())
-                .isEqualTo(BiometricEnrollBase.RESULT_SKIP);
+                .isEqualTo(RESULT_SKIP);
     }
 
     private EnrollmentCallback verifyAndCaptureEnrollmentCallback() {
@@ -214,123 +226,320 @@
     }
 
     @Test
-    public void enrollingFinishResultShallSentBack_onRearDevice() {
+    public void resultFinishShallForward_onRearDevice() {
         setupActivity_onRearDevice();
         triggerEnrollProgressAndError_onRearDevice();
         verifyStartEnrollingActivity();
 
-        // onStop shall not change default activity result
+        // pause activity
         mActivityController.pause().stop();
+
+        // onStop shall not change default activity result
         assertThat(Shadows.shadowOf(mActivity).getResultCode()).isEqualTo(DEFAULT_ACTIVITY_RESULT);
 
-        gotEnrollingResult_verifyResultSentBack(BiometricEnrollBase.RESULT_FINISHED);
+        gotEnrollingResult_resumeActivityAndVerifyResultThenForward(RESULT_FINISHED);
     }
 
     @Test
-    public void enrollingSkipResultShallSentBack_onRearDevice() {
+    public void resultFinishShallForward_onRearDevice_recreate() {
         setupActivity_onRearDevice();
         triggerEnrollProgressAndError_onRearDevice();
         verifyStartEnrollingActivity();
 
+        // recycle activity
+        final Bundle bundle = new Bundle();
+        mActivityController.pause().stop().saveInstanceState(bundle).destroy();
+
         // onStop shall not change default activity result
-        mActivityController.pause().stop();
         assertThat(Shadows.shadowOf(mActivity).getResultCode()).isEqualTo(DEFAULT_ACTIVITY_RESULT);
 
-        gotEnrollingResult_verifyResultSentBack(BiometricEnrollBase.RESULT_SKIP);
+        gotEnrollingResult_recreateActivityAndVerifyResultThenForward(RESULT_FINISHED, bundle);
     }
 
     @Test
-    public void enrollingTimeoutResultShallSentBack_onRearDevice() {
+    public void resultSkipShallForward_onRearDevice() {
         setupActivity_onRearDevice();
+        verifySidecar_onRearOrSfpsDevice();
+
         triggerEnrollProgressAndError_onRearDevice();
         verifyStartEnrollingActivity();
 
-        // onStop shall not change default activity result
+        // pause activity
         mActivityController.pause().stop();
+
+        // onStop shall not change default activity result
         assertThat(Shadows.shadowOf(mActivity).getResultCode()).isEqualTo(DEFAULT_ACTIVITY_RESULT);
 
-        gotEnrollingResult_verifyResultSentBack(BiometricEnrollBase.RESULT_TIMEOUT);
+        gotEnrollingResult_resumeActivityAndVerifyResultThenForward(RESULT_SKIP);
     }
 
     @Test
-    public void enrollingFinishResultShallSentBack_onUdfpsDevice_triggeredByLottieClick() {
+    public void resultSkipShallForward_onRearDevice_recreate() {
+        setupActivity_onRearDevice();
+        verifySidecar_onRearOrSfpsDevice();
+
+        triggerEnrollProgressAndError_onRearDevice();
+        verifyStartEnrollingActivity();
+
+        // recycle activity
+        final Bundle bundle = new Bundle();
+        mActivityController.pause().stop().saveInstanceState(bundle).destroy();
+
+        // onStop shall not change default activity result
+        assertThat(Shadows.shadowOf(mActivity).getResultCode()).isEqualTo(DEFAULT_ACTIVITY_RESULT);
+
+        gotEnrollingResult_recreateActivityAndVerifyResultThenForward(RESULT_SKIP, bundle);
+    }
+
+    @Test
+    public void resultTimeoutShallForward_onRearDevice() {
+        setupActivity_onRearDevice();
+        verifySidecar_onRearOrSfpsDevice();
+
+        triggerEnrollProgressAndError_onRearDevice();
+        verifyStartEnrollingActivity();
+
+        // pause activity
+        mActivityController.pause().stop();
+
+        // onStop shall not change default activity result
+        assertThat(Shadows.shadowOf(mActivity).getResultCode()).isEqualTo(DEFAULT_ACTIVITY_RESULT);
+
+        gotEnrollingResult_resumeActivityAndVerifyResultThenForward(RESULT_TIMEOUT);
+    }
+
+    @Test
+    public void resultTimeoutShallForward_onRearDevice_recreate() {
+        setupActivity_onRearDevice();
+        verifySidecar_onRearOrSfpsDevice();
+
+        triggerEnrollProgressAndError_onRearDevice();
+        verifyStartEnrollingActivity();
+
+        // recycle activity
+        final Bundle bundle = new Bundle();
+        mActivityController.pause().stop().saveInstanceState(bundle).destroy();
+
+        // onStop shall not change default activity result
+        assertThat(Shadows.shadowOf(mActivity).getResultCode()).isEqualTo(DEFAULT_ACTIVITY_RESULT);
+
+        gotEnrollingResult_recreateActivityAndVerifyResultThenForward(RESULT_TIMEOUT, bundle);
+    }
+
+    @Test
+    public void clickLottieResultFinishShallForward_onUdfpsDevice() {
         setupActivity_onUdfpsDevice();
+        verifyNoSidecar();
+
         clickLottieView_onUdfpsDevice();
         verifyStartEnrollingActivity();
         assertThat(Shadows.shadowOf(mActivity).getResultCode()).isEqualTo(DEFAULT_ACTIVITY_RESULT);
 
-        // onStop shall not change default activity result
+        // pause activity
         mActivityController.pause().stop();
+
+        // onStop shall not change default activity result
         assertThat(Shadows.shadowOf(mActivity).getResultCode()).isEqualTo(DEFAULT_ACTIVITY_RESULT);
 
-        gotEnrollingResult_verifyResultSentBack(BiometricEnrollBase.RESULT_FINISHED);
+        gotEnrollingResult_resumeActivityAndVerifyResultThenForward(RESULT_FINISHED);
     }
 
     @Test
-    public void enrollingSkipResultShallSentBack_onUdfpsDevice_triggeredByLottieClick() {
+    public void clickLottieResultFinishShallForward_onUdfpsDevice_ifActivityRecycled() {
         setupActivity_onUdfpsDevice();
+        verifyNoSidecar();
+
         clickLottieView_onUdfpsDevice();
         verifyStartEnrollingActivity();
         assertThat(Shadows.shadowOf(mActivity).getResultCode()).isEqualTo(DEFAULT_ACTIVITY_RESULT);
 
+        // recycle activity
+        final Bundle bundle = new Bundle();
+        mActivityController.pause().stop().saveInstanceState(bundle).destroy();
+
         // onStop shall not change default activity result
-        mActivityController.pause().stop();
         assertThat(Shadows.shadowOf(mActivity).getResultCode()).isEqualTo(DEFAULT_ACTIVITY_RESULT);
 
-        gotEnrollingResult_verifyResultSentBack(BiometricEnrollBase.RESULT_SKIP);
+        gotEnrollingResult_recreateActivityAndVerifyResultThenForward(RESULT_FINISHED, bundle);
     }
 
     @Test
-    public void enrollingTimeoutResultShallSentBack_onUdfpsDevice_triggeredByLottieClick() {
+    public void clickLottieResultSkipShallForward_onUdfpsDevice() {
         setupActivity_onUdfpsDevice();
+        verifyNoSidecar();
+
         clickLottieView_onUdfpsDevice();
         verifyStartEnrollingActivity();
         assertThat(Shadows.shadowOf(mActivity).getResultCode()).isEqualTo(DEFAULT_ACTIVITY_RESULT);
 
-        // onStop shall not change default activity result
+        // pause activity
         mActivityController.pause().stop();
+
+        // onStop shall not change default activity result
         assertThat(Shadows.shadowOf(mActivity).getResultCode()).isEqualTo(DEFAULT_ACTIVITY_RESULT);
 
-        gotEnrollingResult_verifyResultSentBack(BiometricEnrollBase.RESULT_TIMEOUT);
+        gotEnrollingResult_resumeActivityAndVerifyResultThenForward(RESULT_SKIP);
     }
 
     @Test
-    public void enrollingFinishResultShallSentBack_onUdfpsDevice_triggeredByPrimaryButtonClick() {
+    public void clickLottieResultSkipShallForward_onUdfpsDevice_ifActivityRecycled() {
         setupActivity_onUdfpsDevice();
-        clickPrimaryButton_onUdfpsDevice();
-        verifyStartEnrollingActivity();
+        verifyNoSidecar();
 
-        // onStop shall not change default activity result
-        mActivityController.pause().stop();
+        clickLottieView_onUdfpsDevice();
+        verifyStartEnrollingActivity();
         assertThat(Shadows.shadowOf(mActivity).getResultCode()).isEqualTo(DEFAULT_ACTIVITY_RESULT);
 
-        gotEnrollingResult_verifyResultSentBack(BiometricEnrollBase.RESULT_FINISHED);
+        // recycle activity
+        final Bundle bundle = new Bundle();
+        mActivityController.pause().stop().saveInstanceState(bundle).destroy();
+
+        // onStop shall not change default activity result
+        assertThat(Shadows.shadowOf(mActivity).getResultCode()).isEqualTo(DEFAULT_ACTIVITY_RESULT);
+
+        gotEnrollingResult_recreateActivityAndVerifyResultThenForward(RESULT_SKIP, bundle);
     }
 
     @Test
-    public void enrollingSkipResultShallSentBack_onUdfpsDevice_triggeredByPrimaryButtonClick() {
+    public void clickLottieResultTimeoutShallForward_onUdfpsDevice() {
         setupActivity_onUdfpsDevice();
-        clickPrimaryButton_onUdfpsDevice();
-        verifyStartEnrollingActivity();
+        verifyNoSidecar();
 
-        // onStop shall not change default activity result
-        mActivityController.pause().stop();
+        clickLottieView_onUdfpsDevice();
+        verifyStartEnrollingActivity();
         assertThat(Shadows.shadowOf(mActivity).getResultCode()).isEqualTo(DEFAULT_ACTIVITY_RESULT);
 
-        gotEnrollingResult_verifyResultSentBack(BiometricEnrollBase.RESULT_SKIP);
+        // pause activity
+        mActivityController.pause().stop();
+
+        // onStop shall not change default activity result
+        assertThat(Shadows.shadowOf(mActivity).getResultCode()).isEqualTo(DEFAULT_ACTIVITY_RESULT);
+
+        gotEnrollingResult_resumeActivityAndVerifyResultThenForward(RESULT_TIMEOUT);
     }
 
     @Test
-    public void enrollingTimeoutResultShallSentBack_onUdfpsDevice_triggeredByPrimaryButtonClick() {
+    public void clickLottieResultTimeoutShallForward_onUdfpsDevice_ifActivityRecycled() {
         setupActivity_onUdfpsDevice();
+        verifyNoSidecar();
+
+        clickLottieView_onUdfpsDevice();
+        verifyStartEnrollingActivity();
+        assertThat(Shadows.shadowOf(mActivity).getResultCode()).isEqualTo(DEFAULT_ACTIVITY_RESULT);
+
+        // recycle activity
+        final Bundle bundle = new Bundle();
+        mActivityController.pause().stop().saveInstanceState(bundle).destroy();
+
+        // onStop shall not change default activity result
+        assertThat(Shadows.shadowOf(mActivity).getResultCode()).isEqualTo(DEFAULT_ACTIVITY_RESULT);
+
+        gotEnrollingResult_recreateActivityAndVerifyResultThenForward(RESULT_TIMEOUT, bundle);
+    }
+
+    @Test
+    public void clickPrimiaryButtonResultFinishShallForward_onUdfpsDevice() {
+        setupActivity_onUdfpsDevice();
+        verifyNoSidecar();
+
         clickPrimaryButton_onUdfpsDevice();
         verifyStartEnrollingActivity();
 
-        // onStop shall not change default activity result
+        // pause activity
         mActivityController.pause().stop();
+
+        // onStop shall not change default activity result
         assertThat(Shadows.shadowOf(mActivity).getResultCode()).isEqualTo(DEFAULT_ACTIVITY_RESULT);
 
-        gotEnrollingResult_verifyResultSentBack(BiometricEnrollBase.RESULT_TIMEOUT);
+        gotEnrollingResult_resumeActivityAndVerifyResultThenForward(RESULT_FINISHED);
+    }
+
+    @Test
+    public void clickPrimiaryButtonResultFinishShallForward_onUdfpsDevice_ifActivityRecycled() {
+        setupActivity_onUdfpsDevice();
+        verifyNoSidecar();
+
+        clickPrimaryButton_onUdfpsDevice();
+        verifyStartEnrollingActivity();
+
+        // recycle activity
+        final Bundle bundle = new Bundle();
+        mActivityController.pause().stop().saveInstanceState(bundle).destroy();
+
+        // onStop shall not change default activity result
+        assertThat(Shadows.shadowOf(mActivity).getResultCode()).isEqualTo(DEFAULT_ACTIVITY_RESULT);
+
+        gotEnrollingResult_recreateActivityAndVerifyResultThenForward(RESULT_FINISHED, bundle);
+    }
+
+    @Test
+    public void clickPrimiaryButtonResultSkipShallForward_onUdfpsDevice() {
+        setupActivity_onUdfpsDevice();
+        verifyNoSidecar();
+
+        clickPrimaryButton_onUdfpsDevice();
+        verifyStartEnrollingActivity();
+
+        // pause activity
+        mActivityController.pause().stop();
+
+        // onStop shall not change default activity result
+        assertThat(Shadows.shadowOf(mActivity).getResultCode()).isEqualTo(DEFAULT_ACTIVITY_RESULT);
+
+        gotEnrollingResult_resumeActivityAndVerifyResultThenForward(RESULT_SKIP);
+    }
+
+    @Test
+    public void clickPrimaryButtonResultSkipShallForward_onUdfpsDevice_ifActivityRecycled() {
+        setupActivity_onUdfpsDevice();
+        verifyNoSidecar();
+
+        clickPrimaryButton_onUdfpsDevice();
+        verifyStartEnrollingActivity();
+
+        // recycle activity
+        final Bundle bundle = new Bundle();
+        mActivityController.pause().stop().saveInstanceState(bundle).destroy();
+
+        // onStop shall not change default activity result
+        assertThat(Shadows.shadowOf(mActivity).getResultCode()).isEqualTo(DEFAULT_ACTIVITY_RESULT);
+
+        gotEnrollingResult_recreateActivityAndVerifyResultThenForward(RESULT_SKIP, bundle);
+    }
+
+    @Test
+    public void clickPrimaryButtonResultTimeoutShallForward_onUdfpsDevice() {
+        setupActivity_onUdfpsDevice();
+        verifyNoSidecar();
+
+        clickPrimaryButton_onUdfpsDevice();
+        verifyStartEnrollingActivity();
+
+        // pause activity
+        mActivityController.pause().stop();
+
+        // onStop shall not change default activity result
+        assertThat(Shadows.shadowOf(mActivity).getResultCode()).isEqualTo(DEFAULT_ACTIVITY_RESULT);
+
+        gotEnrollingResult_resumeActivityAndVerifyResultThenForward(RESULT_TIMEOUT);
+    }
+
+    @Test
+    public void clickPrimaryButtonResultTimeoutShallForward_onUdfpsDevice_ifActivityRecycled() {
+        setupActivity_onUdfpsDevice();
+        verifyNoSidecar();
+
+        clickPrimaryButton_onUdfpsDevice();
+        verifyStartEnrollingActivity();
+
+        // recycle activity
+        final Bundle bundle = new Bundle();
+        mActivityController.pause().stop().saveInstanceState(bundle).destroy();
+
+        // onStop shall not change default activity result
+        assertThat(Shadows.shadowOf(mActivity).getResultCode()).isEqualTo(DEFAULT_ACTIVITY_RESULT);
+
+        gotEnrollingResult_recreateActivityAndVerifyResultThenForward(RESULT_TIMEOUT, bundle);
     }
 
     private void triggerEnrollProgressAndError_onRearDevice() {
@@ -355,9 +564,13 @@
         lottieView.performClick();
     }
 
-    private void gotEnrollingResult_verifyResultSentBack(int testActivityResult) {
-        // onActivityResult from Enrolling activity shall be sent back
+    private void gotEnrollingResult_resumeActivityAndVerifyResultThenForward(
+            int testActivityResult) {
+        // resume activity
         mActivityController.start().resume().visible();
+        verifyNoSidecar();
+
+        // onActivityResult from Enrolling activity shall be forward back
         Shadows.shadowOf(mActivity).receiveResult(
                 new Intent(mActivity, FingerprintEnrollEnrolling.class),
                 testActivityResult,
@@ -366,7 +579,42 @@
         assertThat(mActivity.isFinishing()).isEqualTo(true);
 
         // onStop shall not change last activity result
-        mActivityController.pause().stop();
+        mActivityController.pause().stop().destroy();
         assertThat(Shadows.shadowOf(mActivity).getResultCode()).isEqualTo(testActivityResult);
     }
+
+    private void gotEnrollingResult_recreateActivityAndVerifyResultThenForward(
+            int testActivityResult, @NonNull Bundle savedInstance) {
+        // Rebuild activity and use savedInstance to restore.
+        buildActivity();
+        mActivityController.setup(savedInstance);
+        verifyNoSidecar();
+
+        // onActivityResult from Enrolling activity shall be forward back
+        Shadows.shadowOf(mActivity).receiveResult(
+                new Intent(mActivity, FingerprintEnrollEnrolling.class),
+                testActivityResult,
+                null);
+        assertThat(Shadows.shadowOf(mActivity).getResultCode()).isEqualTo(testActivityResult);
+        assertThat(mActivity.isFinishing()).isEqualTo(true);
+
+        // onStop shall not change last activity result
+        mActivityController.pause().stop().destroy();
+        assertThat(Shadows.shadowOf(mActivity).getResultCode()).isEqualTo(testActivityResult);
+    }
+
+    private void verifySidecar_onRearOrSfpsDevice() {
+        final Fragment sidecar = mActivity.getSupportFragmentManager().findFragmentByTag(
+                TAG_SIDECAR);
+        assertThat(sidecar).isNotNull();
+        assertThat(sidecar.isAdded()).isTrue();
+    }
+
+    private void verifyNoSidecar() {
+        final Fragment sidecar = mActivity.getSupportFragmentManager().findFragmentByTag(
+                TAG_SIDECAR);
+        if (sidecar != null) {
+            assertThat(sidecar.isAdded()).isFalse();
+        }
+    }
 }