chromium/chrome/android/javatests/src/org/chromium/chrome/browser/password_manager/settings/PasswordSettingsExportTest.java

// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.chrome.browser.password_manager.settings;

import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.assertion.ViewAssertions.doesNotExist;
import static androidx.test.espresso.assertion.ViewAssertions.matches;
import static androidx.test.espresso.intent.Intents.intended;
import static androidx.test.espresso.intent.Intents.intending;
import static androidx.test.espresso.intent.matcher.BundleMatchers.hasEntry;
import static androidx.test.espresso.intent.matcher.IntentMatchers.hasAction;
import static androidx.test.espresso.intent.matcher.IntentMatchers.hasCategories;
import static androidx.test.espresso.intent.matcher.IntentMatchers.hasData;
import static androidx.test.espresso.intent.matcher.IntentMatchers.hasExtras;
import static androidx.test.espresso.intent.matcher.IntentMatchers.hasType;
import static androidx.test.espresso.intent.matcher.UriMatchers.hasHost;
import static androidx.test.espresso.matcher.RootMatchers.isDialog;
import static androidx.test.espresso.matcher.RootMatchers.withDecorView;
import static androidx.test.espresso.matcher.ViewMatchers.isCompletelyDisplayed;
import static androidx.test.espresso.matcher.ViewMatchers.isDescendantOfA;
import static androidx.test.espresso.matcher.ViewMatchers.isEnabled;
import static androidx.test.espresso.matcher.ViewMatchers.withText;

import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasItem;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;

import static org.chromium.chrome.browser.flags.ChromeFeatureList.UNIFIED_PASSWORD_MANAGER_LOCAL_PWD_MIGRATION_WARNING;
import static org.chromium.chrome.browser.password_manager.PasswordMetricsUtil.PASSWORD_SETTINGS_EXPORT_METRICS_ID;
import static org.chromium.ui.test.util.ViewUtils.onViewWaiting;

import android.app.Activity;
import android.app.Instrumentation;
import android.app.Instrumentation.ActivityResult;
import android.content.Intent;
import android.view.View;

import androidx.test.espresso.Espresso;
import androidx.test.espresso.intent.Intents;
import androidx.test.filters.SmallTest;
import androidx.test.platform.app.InstrumentationRegistry;

import org.hamcrest.Matcher;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

import org.chromium.base.FileUtils;
import org.chromium.base.ThreadUtils;
import org.chromium.base.test.util.CommandLineFlags;
import org.chromium.base.test.util.DoNotBatch;
import org.chromium.base.test.util.Feature;
import org.chromium.base.test.util.Features.DisableFeatures;
import org.chromium.base.test.util.Features.EnableFeatures;
import org.chromium.base.test.util.HistogramWatcher;
import org.chromium.base.test.util.Matchers;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.flags.ChromeSwitches;
import org.chromium.chrome.browser.password_check.PasswordCheck;
import org.chromium.chrome.browser.password_check.PasswordCheckFactory;
import org.chromium.chrome.browser.password_manager.PasswordMetricsUtil;
import org.chromium.chrome.browser.password_manager.PasswordMetricsUtil.HistogramExportResult;
import org.chromium.chrome.browser.settings.SettingsActivity;
import org.chromium.chrome.browser.settings.SettingsActivityTestRule;
import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
import org.chromium.chrome.test.R;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;

/** Tests for exports started at the "Passwords" settings screen. */
@RunWith(ChromeJUnit4ClassRunner.class)
@CommandLineFlags.Add({ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE})
@DoNotBatch(reason = "Tests are flaky on API Q+ with batching. This might be fixable. b/40926377")
public class PasswordSettingsExportTest {
    @Rule
    public SettingsActivityTestRule<PasswordSettings> mSettingsActivityTestRule =
            new SettingsActivityTestRule<>(PasswordSettings.class);

    @Mock private PasswordCheck mPasswordCheck;

    private final PasswordSettingsTestHelper mTestHelper = new PasswordSettingsTestHelper();

    @Before
    public void setUp() {
        MockitoAnnotations.initMocks(this);
        PasswordCheckFactory.setPasswordCheckForTesting(mPasswordCheck);
    }

    @After
    public void tearDown() {
        mTestHelper.tearDown();
    }

    /** Check that if there are no saved passwords, the export menu item is disabled. */
    @Test
    @SmallTest
    @Feature({"Preferences"})
    public void testExportMenuDisabled() {
        // Ensure there are no saved passwords reported to settings.
        mTestHelper.setPasswordSource(null);

        ReauthenticationManager.setApiOverride(ReauthenticationManager.OverrideState.AVAILABLE);

        mTestHelper.startPasswordSettingsFromMainSettings(mSettingsActivityTestRule);

        checkExportMenuItemState(false);
    }

    /** Check that if there are saved passwords, the export menu item is enabled. */
    @Test
    @SmallTest
    @Feature({"Preferences"})
    public void testExportMenuEnabled() {
        mTestHelper.setPasswordSource(
                new SavedPasswordEntry("https://example.com", "test user", "password"));

        ReauthenticationManager.setApiOverride(ReauthenticationManager.OverrideState.AVAILABLE);

        mTestHelper.startPasswordSettingsFromMainSettings(mSettingsActivityTestRule);

        checkExportMenuItemState(true);
    }

    /**
     * Check that tapping the export menu requests the passwords to be serialised in the background.
     */
    @Test
    @SmallTest
    @Feature({"Preferences"})
    public void testExportTriggersSerialization() {
        mTestHelper.setPasswordSource(
                new SavedPasswordEntry("https://example.com", "test user", "password"));

        ReauthenticationManager.setApiOverride(ReauthenticationManager.OverrideState.AVAILABLE);
        ReauthenticationManager.setScreenLockSetUpOverride(
                ReauthenticationManager.OverrideState.AVAILABLE);

        mTestHelper.startPasswordSettingsFromMainSettings(mSettingsActivityTestRule);

        var histogram =
                HistogramWatcher.newSingleRecordWatcher(
                        mSettingsActivityTestRule
                                .getFragment()
                                .getExportFlowForTesting()
                                .getExportEventHistogramName(),
                        ExportFlow.PasswordExportEvent.EXPORT_OPTION_SELECTED);

        openActionBarOverflowOrOptionsMenu(
                InstrumentationRegistry.getInstrumentation().getTargetContext());
        // Before tapping the menu item for export, pretend that the last successful
        // reauthentication just happened. This will allow the export flow to continue.
        ReauthenticationManager.recordLastReauth(
                System.currentTimeMillis(), ReauthenticationManager.ReauthScope.BULK);
        onViewWaiting(
                        allOf(
                                withText(R.string.password_settings_export_action_title),
                                isCompletelyDisplayed()))
                .perform(click());

        Assert.assertNotNull(mTestHelper.getHandler().getExportTargetPath());
        Assert.assertFalse(mTestHelper.getHandler().getExportTargetPath().isEmpty());
        histogram.assertExpected();
    }

    /**
     * Check that the export menu item is included and hidden behind the overflow menu. Check that
     * the menu displays the warning before letting the user export passwords.
     */
    @Test
    @SmallTest
    @Feature({"Preferences"})
    public void testExportMenuItem() {
        mTestHelper.setPasswordSource(
                new SavedPasswordEntry("https://example.com", "test user", "password"));

        ReauthenticationManager.setApiOverride(ReauthenticationManager.OverrideState.AVAILABLE);
        ReauthenticationManager.setScreenLockSetUpOverride(
                ReauthenticationManager.OverrideState.AVAILABLE);

        final SettingsActivity settingsActivity =
                mTestHelper.startPasswordSettingsFromMainSettings(mSettingsActivityTestRule);

        reauthenticateAndRequestExport(settingsActivity);

        // Check that the warning dialog is displayed.
        onView(withText(R.string.settings_passwords_export_description))
                .inRoot(isDialog())
                .check(matches(isCompletelyDisplayed()));
    }

    /**
     * Check that if export is canceled by the user after a successful reauthentication, then
     * re-triggering the export and failing the second reauthentication aborts the export as well.
     */
    @Test
    @SmallTest
    @Feature({"Preferences"})
    public void testExportReauthAfterCancel() {
        mTestHelper.setPasswordSource(
                new SavedPasswordEntry("https://example.com", "test user", "password"));

        ReauthenticationManager.setApiOverride(ReauthenticationManager.OverrideState.AVAILABLE);
        ReauthenticationManager.setScreenLockSetUpOverride(
                ReauthenticationManager.OverrideState.AVAILABLE);

        final SettingsActivity settingsActivity =
                mTestHelper.startPasswordSettingsFromMainSettings(mSettingsActivityTestRule);

        reauthenticateAndRequestExport(settingsActivity);

        // Hit the Cancel button on the warning dialog to cancel the flow.
        onView(withText(R.string.cancel)).inRoot(isDialog()).perform(click());

        // Now repeat the steps almost like in |reauthenticateAndRequestExport| but simulate failing
        // the reauthentication challenge.
        openActionBarOverflowOrOptionsMenu(
                InstrumentationRegistry.getInstrumentation().getTargetContext());

        // Avoid launching the Android-provided reauthentication challenge, which cannot be
        // completed in the test.
        ReauthenticationManager.setSkipSystemReauth(true);
        onViewWaiting(
                        allOf(
                                withText(R.string.password_settings_export_action_title),
                                isCompletelyDisplayed()))
                .perform(click());

        // Now Chrome thinks it triggered the challenge and is waiting to be resumed. Once resumed
        // it will check the reauthentication result. First, update the reauth timestamp to indicate
        // a cancelled reauth:
        ReauthenticationManager.resetLastReauth();

        // Now call onResume to nudge Chrome into continuing the export flow.
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    settingsActivity.getMainFragment().onResume();
                });

        // Check that the warning dialog is not displayed.
        onView(withText(R.string.settings_passwords_export_description)).check(doesNotExist());

        // Check that the export menu item is enabled, because the current export was cancelled.
        checkExportMenuItemState(true);
    }

    /**
     * Check that metrics are recorded when export flow is aborted because the screen lock is not
     * set up.
     */
    @Test
    @SmallTest
    @Feature({"Preferences"})
    @EnableFeatures(ChromeFeatureList.UNIFIED_PASSWORD_MANAGER_LOCAL_PWD_MIGRATION_WARNING)
    public void testExportFlowWithNoScreenLockRecordsMetrics() {
        mTestHelper.setPasswordSource(
                new SavedPasswordEntry("https://example.com", "test user", "password"));

        ReauthenticationManager.setApiOverride(ReauthenticationManager.OverrideState.AVAILABLE);
        ReauthenticationManager.setScreenLockSetUpOverride(
                ReauthenticationManager.OverrideState.UNAVAILABLE);

        final SettingsActivity settingsActivity =
                mTestHelper.startPasswordSettingsFromMainSettings(mSettingsActivityTestRule);

        var exportResultHistogram =
                HistogramWatcher.newBuilder()
                        .expectIntRecords(
                                PASSWORD_SETTINGS_EXPORT_METRICS_ID
                                        + PasswordMetricsUtil.EXPORT_RESULT_HISTOGRAM_SUFFIX,
                                PasswordMetricsUtil.HistogramExportResult.NO_SCREEN_LOCK_SET_UP)
                        .build();

        reauthenticateAndRequestExport(settingsActivity);

        exportResultHistogram.assertExpected();
    }

    /**
     * Check whether the user is asked to set up a screen lock if attempting to export passwords.
     */
    @Test
    @SmallTest
    @Feature({"Preferences"})
    public void testExportMenuItemNoLock() {
        mTestHelper.setPasswordSource(
                new SavedPasswordEntry("https://example.com", "test user", "password"));

        ReauthenticationManager.setApiOverride(ReauthenticationManager.OverrideState.AVAILABLE);
        ReauthenticationManager.setScreenLockSetUpOverride(
                ReauthenticationManager.OverrideState.UNAVAILABLE);

        final SettingsActivity settingsActivity =
                mTestHelper.startPasswordSettingsFromMainSettings(mSettingsActivityTestRule);

        View mainDecorView = settingsActivity.getWindow().getDecorView();
        openActionBarOverflowOrOptionsMenu(
                InstrumentationRegistry.getInstrumentation().getTargetContext());
        onViewWaiting(
                        allOf(
                                withText(R.string.password_settings_export_action_title),
                                isCompletelyDisplayed()))
                .perform(click());
        onView(withText(R.string.password_export_set_lock_screen))
                .inRoot(withDecorView(not(is(mainDecorView))))
                .check(matches(isCompletelyDisplayed()));
    }

    /**
     * Check that if exporting is cancelled for the absence of the screen lock, the menu item is
     * enabled for a retry.
     */
    @Test
    @SmallTest
    @Feature({"Preferences"})
    public void testExportMenuItemReenabledNoLock() {
        mTestHelper.setPasswordSource(
                new SavedPasswordEntry("https://example.com", "test user", "password"));

        ReauthenticationManager.setApiOverride(ReauthenticationManager.OverrideState.AVAILABLE);
        ReauthenticationManager.setScreenLockSetUpOverride(
                ReauthenticationManager.OverrideState.UNAVAILABLE);

        mTestHelper.startPasswordSettingsFromMainSettings(mSettingsActivityTestRule);

        // Trigger exporting and let it fail on the unavailable lock.
        openActionBarOverflowOrOptionsMenu(
                InstrumentationRegistry.getInstrumentation().getTargetContext());
        onViewWaiting(
                        allOf(
                                withText(R.string.password_settings_export_action_title),
                                isCompletelyDisplayed()))
                .perform(click());

        // Check that for re-triggering, the export menu item is enabled.
        checkExportMenuItemState(true);
    }

    /**
     * Check that if exporting is cancelled for the user's failure to reauthenticate, the menu item
     * is enabled for a retry.
     */
    @Test
    @SmallTest
    @Feature({"Preferences"})
    public void testExportMenuItemReenabledReauthFailure() {
        mTestHelper.setPasswordSource(
                new SavedPasswordEntry("https://example.com", "test user", "password"));

        ReauthenticationManager.setApiOverride(ReauthenticationManager.OverrideState.AVAILABLE);
        ReauthenticationManager.setSkipSystemReauth(true);

        final SettingsActivity settingsActivity =
                mTestHelper.startPasswordSettingsFromMainSettings(mSettingsActivityTestRule);

        openActionBarOverflowOrOptionsMenu(
                InstrumentationRegistry.getInstrumentation().getTargetContext());
        onViewWaiting(
                        allOf(
                                withText(R.string.password_settings_export_action_title),
                                isCompletelyDisplayed()))
                .perform(click());
        // The reauthentication dialog is skipped and the last reauthentication timestamp is not
        // reset. This looks like a failed reauthentication to PasswordSettings' onResume.
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    settingsActivity.getMainFragment().onResume();
                });
        checkExportMenuItemState(true);
    }

    /**
     * Check that the export always requires a reauthentication, even if the last one happened
     * recently.
     */
    @Test
    @SmallTest
    @Feature({"Preferences"})
    public void testExportRequiresReauth() {
        mTestHelper.setPasswordSource(
                new SavedPasswordEntry("https://example.com", "test user", "password"));

        ReauthenticationManager.setApiOverride(ReauthenticationManager.OverrideState.AVAILABLE);
        ReauthenticationManager.setScreenLockSetUpOverride(
                ReauthenticationManager.OverrideState.AVAILABLE);

        mTestHelper.startPasswordSettingsFromMainSettings(mSettingsActivityTestRule);

        // Ensure that the last reauthentication time stamp is recent enough.
        ReauthenticationManager.recordLastReauth(
                System.currentTimeMillis(), ReauthenticationManager.ReauthScope.BULK);

        // Start export.
        openActionBarOverflowOrOptionsMenu(
                InstrumentationRegistry.getInstrumentation().getTargetContext());

        // Avoid launching the Android-provided reauthentication challenge, which cannot be
        // completed in the test.
        ReauthenticationManager.setSkipSystemReauth(true);
        onViewWaiting(
                        allOf(
                                withText(R.string.password_settings_export_action_title),
                                isCompletelyDisplayed()))
                .perform(click());

        // Check that Chrome indeed issued an (ignored) request to reauthenticate the user rather
        // than re-using the recent reauthentication, by observing that the next step in the flow
        // (progress bar) is not shown.
        onView(withText(R.string.settings_passwords_preparing_export)).check(doesNotExist());
    }

    /**
     * Check that the export flow ends up with sending off a share intent with the exported
     * passwords.
     */
    @Test
    @SmallTest
    @DisableFeatures(UNIFIED_PASSWORD_MANAGER_LOCAL_PWD_MIGRATION_WARNING)
    @Feature({"Preferences"})
    public void testExportIntent() throws Exception {
        mTestHelper.setPasswordSource(
                new SavedPasswordEntry("https://example.com", "test user", "password"));

        ReauthenticationManager.setApiOverride(ReauthenticationManager.OverrideState.AVAILABLE);
        ReauthenticationManager.setScreenLockSetUpOverride(
                ReauthenticationManager.OverrideState.AVAILABLE);

        final SettingsActivity settingsActivity =
                mTestHelper.startPasswordSettingsFromMainSettings(mSettingsActivityTestRule);

        Intents.init();

        var exportEventHistogram =
                HistogramWatcher.newBuilder()
                        .expectIntRecords(
                                mSettingsActivityTestRule
                                        .getFragment()
                                        .getExportFlowForTesting()
                                        .getExportEventHistogramName(),
                                ExportFlow.PasswordExportEvent.EXPORT_OPTION_SELECTED,
                                ExportFlow.PasswordExportEvent.EXPORT_CONFIRMED)
                        .build();

        reauthenticateAndRequestExport(settingsActivity);
        File tempFile = createFakeExportedPasswordsFile();
        // Pretend that passwords have been serialized to go directly to the intent.
        mTestHelper.getHandler().getExportSuccessCallback().onResult(123, tempFile.getPath());

        // Before triggering the sharing intent chooser, stub it out to avoid leaving system UI open
        // after the test is finished.
        intending(hasAction(equalTo(Intent.ACTION_CHOOSER)))
                .respondWith(new Instrumentation.ActivityResult(Activity.RESULT_OK, null));

        // Confirm the export warning to fire the sharing intent.
        onViewWaiting(
                        allOf(
                                withText(R.string.password_settings_export_action_title),
                                isCompletelyDisplayed()),
                        /* checkRootDialog= */ true)
                .perform(click());
        exportEventHistogram.assertExpected();

        intended(
                allOf(
                        hasAction(equalTo(Intent.ACTION_CHOOSER)),
                        hasExtras(
                                hasEntry(
                                        equalTo(Intent.EXTRA_INTENT),
                                        allOf(
                                                hasAction(equalTo(Intent.ACTION_SEND)),
                                                hasType("text/csv"))))));

        Intents.release();

        tempFile.delete();
    }

    /** Check that the export flow ends with saving the file with passwords to the file system. */
    @Test
    @SmallTest
    @EnableFeatures(UNIFIED_PASSWORD_MANAGER_LOCAL_PWD_MIGRATION_WARNING)
    @Feature({"Preferences"})
    public void testExportToDownloadsIntent() throws Exception {
        mTestHelper.setPasswordSource(
                new SavedPasswordEntry("https://example.com", "test user", "password"));

        ReauthenticationManager.setApiOverride(ReauthenticationManager.OverrideState.AVAILABLE);
        ReauthenticationManager.setScreenLockSetUpOverride(
                ReauthenticationManager.OverrideState.AVAILABLE);

        final SettingsActivity settingsActivity =
                mTestHelper.startPasswordSettingsFromMainSettings(mSettingsActivityTestRule);

        var histogram =
                HistogramWatcher.newBuilder()
                        .expectIntRecords(
                                mSettingsActivityTestRule
                                        .getFragment()
                                        .getExportFlowForTesting()
                                        .getExportEventHistogramName(),
                                ExportFlow.PasswordExportEvent.EXPORT_OPTION_SELECTED,
                                ExportFlow.PasswordExportEvent.EXPORT_CONFIRMED)
                        .build();

        Intents.init();

        reauthenticateAndRequestExport(settingsActivity);
        File tempFile = createFakeExportedPasswordsFile();
        // Pretend that passwords have been serialized to go directly to the intent.
        mTestHelper.getHandler().getExportSuccessCallback().onResult(123, tempFile.getPath());

        // Simulate that the intent would return a newly created file.
        Intent result = new Intent();
        File outputFile = createFakeSavedPasswordsFile();
        result.setData(FileUtils.getUriForFile(outputFile));
        // Pretend that user has chosen to save the passwords in the file system.
        intending(hasAction(Intent.ACTION_CREATE_DOCUMENT))
                .respondWith(new ActivityResult(Activity.RESULT_OK, result));

        // Confirm the export warning to fire the sharing intent.
        onViewWaiting(
                        allOf(
                                withText(R.string.password_settings_export_action_title),
                                isCompletelyDisplayed()),
                        /* checkRootDialog= */ true)
                .perform(click());
        histogram.assertExpected();

        intended(
                allOf(
                        hasAction(equalTo(Intent.ACTION_CREATE_DOCUMENT)),
                        hasCategories(hasItem(Intent.CATEGORY_OPENABLE)),
                        hasExtras(hasEntry(Intent.EXTRA_TITLE, Matchers.notNullValue())),
                        hasType("text/csv")));
        // Assert that the output file was written.
        Assert.assertTrue(outputFile.length() > 0);

        Intents.release();

        tempFile.delete();
        outputFile.delete();
    }

    /**
     * Check that the export flow ends up with sending off a share intent with the exported
     * passwords, even if the flow gets interrupted by pausing Chrome.
     */
    @Test
    @SmallTest
    @DisableFeatures(UNIFIED_PASSWORD_MANAGER_LOCAL_PWD_MIGRATION_WARNING)
    @Feature({"Preferences"})
    public void testExportIntentPaused() throws Exception {
        mTestHelper.setPasswordSource(
                new SavedPasswordEntry("https://example.com", "test user", "password"));

        ReauthenticationManager.setApiOverride(ReauthenticationManager.OverrideState.AVAILABLE);
        ReauthenticationManager.setScreenLockSetUpOverride(
                ReauthenticationManager.OverrideState.AVAILABLE);

        final SettingsActivity settingsActivity =
                mTestHelper.startPasswordSettingsFromMainSettings(mSettingsActivityTestRule);

        Intents.init();

        var histogram =
                HistogramWatcher.newBuilder()
                        .expectIntRecords(
                                mSettingsActivityTestRule
                                        .getFragment()
                                        .getExportFlowForTesting()
                                        .getExportEventHistogramName(),
                                ExportFlow.PasswordExportEvent.EXPORT_OPTION_SELECTED,
                                ExportFlow.PasswordExportEvent.EXPORT_CONFIRMED)
                        .build();

        reauthenticateAndRequestExport(settingsActivity);

        // Call onResume to simulate that the user put Chrome into background by opening "recent
        // apps" and then restored Chrome by choosing it from the list.
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    settingsActivity.getMainFragment().onResume();
                });

        File tempFile = createFakeExportedPasswordsFile();
        // Pretend that passwords have been serialized to go directly to the intent.
        mTestHelper.getHandler().getExportSuccessCallback().onResult(56, tempFile.getPath());

        // Before triggering the sharing intent chooser, stub it out to avoid leaving system UI open
        // after the test is finished.
        intending(hasAction(equalTo(Intent.ACTION_CHOOSER)))
                .respondWith(new Instrumentation.ActivityResult(Activity.RESULT_OK, null));

        // Confirm the export warning to fire the sharing intent.
        onViewWaiting(
                        allOf(
                                withText(R.string.password_settings_export_action_title),
                                isCompletelyDisplayed()),
                        /* checkRootDialog= */ true)
                .perform(click());
        histogram.assertExpected();

        intended(
                allOf(
                        hasAction(equalTo(Intent.ACTION_CHOOSER)),
                        hasExtras(
                                hasEntry(
                                        equalTo(Intent.EXTRA_INTENT),
                                        allOf(
                                                hasAction(equalTo(Intent.ACTION_SEND)),
                                                hasType("text/csv"))))));

        Intents.release();

        tempFile.delete();
    }

    /**
     * Check that the export flow can be canceled in the warning dialogue and that upon cancellation
     * the export menu item gets re-enabled.
     */
    @Test
    @SmallTest
    @Feature({"Preferences"})
    public void testExportCancelOnWarning() {
        mTestHelper.setPasswordSource(
                new SavedPasswordEntry("https://example.com", "test user", "password"));

        ReauthenticationManager.setApiOverride(ReauthenticationManager.OverrideState.AVAILABLE);
        ReauthenticationManager.setScreenLockSetUpOverride(
                ReauthenticationManager.OverrideState.AVAILABLE);

        final SettingsActivity settingsActivity =
                mTestHelper.startPasswordSettingsFromMainSettings(mSettingsActivityTestRule);

        var histogram =
                HistogramWatcher.newBuilder()
                        .expectIntRecords(
                                mSettingsActivityTestRule
                                        .getFragment()
                                        .getExportFlowForTesting()
                                        .getExportEventHistogramName(),
                                ExportFlow.PasswordExportEvent.EXPORT_OPTION_SELECTED,
                                ExportFlow.PasswordExportEvent.EXPORT_DISMISSED)
                        .build();

        reauthenticateAndRequestExport(settingsActivity);

        // Cancel the export warning.
        onView(withText(R.string.cancel)).inRoot(isDialog()).perform(click());

        // Check that the cancellation succeeded by checking that the export menu is available and
        // enabled.
        checkExportMenuItemState(true);
        histogram.assertExpected();
    }

    /** Check that the export warning is not duplicated when onResume is called on the settings. */
    @Test
    @SmallTest
    @Feature({"Preferences"})
    public void testExportWarningOnResume() {
        mTestHelper.setPasswordSource(
                new SavedPasswordEntry("https://example.com", "test user", "password"));

        ReauthenticationManager.setApiOverride(ReauthenticationManager.OverrideState.AVAILABLE);
        ReauthenticationManager.setScreenLockSetUpOverride(
                ReauthenticationManager.OverrideState.AVAILABLE);

        final SettingsActivity settingsActivity =
                mTestHelper.startPasswordSettingsFromMainSettings(mSettingsActivityTestRule);

        reauthenticateAndRequestExport(settingsActivity);

        // Call onResume to simulate that the user put Chrome into background by opening "recent
        // apps" and then restored Chrome by choosing it from the list.
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    settingsActivity.getMainFragment().onResume();
                });

        // Cancel the export warning.
        onView(withText(R.string.cancel)).inRoot(isDialog()).perform(click());

        // Check that export warning is not visible again.
        onView(withText(R.string.cancel)).check(doesNotExist());

        // Check that the cancellation succeeded by checking that the export menu is available and
        // enabled.
        checkExportMenuItemState(true);
    }

    /**
     * Check that the export warning is dismissed after onResume if the last reauthentication
     * happened too long ago.
     */
    @Test
    @SmallTest
    @Feature({"Preferences"})
    public void testExportWarningTimeoutOnResume() {
        mTestHelper.setPasswordSource(
                new SavedPasswordEntry("https://example.com", "test user", "password"));

        ReauthenticationManager.setApiOverride(ReauthenticationManager.OverrideState.AVAILABLE);
        ReauthenticationManager.setScreenLockSetUpOverride(
                ReauthenticationManager.OverrideState.AVAILABLE);

        final SettingsActivity settingsActivity =
                mTestHelper.startPasswordSettingsFromMainSettings(mSettingsActivityTestRule);

        openActionBarOverflowOrOptionsMenu(
                InstrumentationRegistry.getInstrumentation().getTargetContext());

        // Before exporting, pretend that the last successful reauthentication happened too long
        // ago.
        ReauthenticationManager.recordLastReauth(
                System.currentTimeMillis()
                        - ReauthenticationManager.VALID_REAUTHENTICATION_TIME_INTERVAL_MILLIS
                        - 1,
                ReauthenticationManager.ReauthScope.BULK);

        onViewWaiting(
                        allOf(
                                withText(R.string.password_settings_export_action_title),
                                isCompletelyDisplayed()))
                .perform(click());

        // Call onResume to simulate that the user put Chrome into background by opening "recent
        // apps" and then restored Chrome by choosing it from the list.
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    settingsActivity.getMainFragment().onResume();
                });

        // Check that export warning is not visible again.
        onView(withText(R.string.cancel)).check(doesNotExist());

        // Check that the export flow was cancelled automatically by checking that the export menu
        // is available and enabled.
        checkExportMenuItemState(true);
    }

    /**
     * Check that the export flow can be canceled by dismissing the warning dialogue (tapping
     * outside of it, as opposed to tapping "Cancel") and that upon cancellation the export menu
     * item gets re-enabled.
     */
    @Test
    @SmallTest
    @Feature({"Preferences"})
    public void testExportCancelOnWarningDismissal() {
        mTestHelper.setPasswordSource(
                new SavedPasswordEntry("https://example.com", "test user", "password"));

        ReauthenticationManager.setApiOverride(ReauthenticationManager.OverrideState.AVAILABLE);
        ReauthenticationManager.setScreenLockSetUpOverride(
                ReauthenticationManager.OverrideState.AVAILABLE);

        final SettingsActivity settingsActivity =
                mTestHelper.startPasswordSettingsFromMainSettings(mSettingsActivityTestRule);

        reauthenticateAndRequestExport(settingsActivity);

        // Verify that the warning dialog is shown and then dismiss it through pressing back (as
        // opposed to the cancel button).
        onView(withText(R.string.password_settings_export_action_title))
                .inRoot(isDialog())
                .check(matches(isCompletelyDisplayed()));
        Espresso.pressBack();

        // Check that the cancellation succeeded by checking that the export menu is available and
        // enabled.
        checkExportMenuItemState(true);
    }

    /** Check that a progressbar is displayed for a minimal time duration to avoid flickering. */
    @Test
    @SmallTest
    @DisableFeatures(UNIFIED_PASSWORD_MANAGER_LOCAL_PWD_MIGRATION_WARNING)
    @Feature({"Preferences"})
    public void testExportProgressMinimalTime() throws Exception {
        mTestHelper.setPasswordSource(
                new SavedPasswordEntry("https://example.com", "test user", "password"));

        ReauthenticationManager.setApiOverride(ReauthenticationManager.OverrideState.AVAILABLE);
        ReauthenticationManager.setScreenLockSetUpOverride(
                ReauthenticationManager.OverrideState.AVAILABLE);

        final SettingsActivity settingsActivity =
                mTestHelper.startPasswordSettingsFromMainSettings(mSettingsActivityTestRule);

        Intents.init();

        // This also disables the timer for keeping the progress bar up. The test can thus emulate
        // that timer going off by calling {@link allowProgressBarToBeHidden}.
        reauthenticateAndRequestExport(settingsActivity);

        // Before triggering the sharing intent chooser, stub it out to avoid leaving system UI open
        // after the test is finished.
        intending(hasAction(equalTo(Intent.ACTION_CHOOSER)))
                .respondWith(new Instrumentation.ActivityResult(Activity.RESULT_OK, null));

        // Confirm the export warning to fire the sharing intent.
        onViewWaiting(
                        allOf(
                                withText(R.string.password_settings_export_action_title),
                                isCompletelyDisplayed()),
                        /* checkRootDialog= */ true)
                .perform(click());

        // Before simulating the serialized passwords being received, check that the progress bar is
        // shown.
        onView(withText(R.string.settings_passwords_preparing_export))
                .inRoot(isDialog())
                .check(matches(isCompletelyDisplayed()));

        File tempFile = createFakeExportedPasswordsFile();
        // Now pretend that passwords have been serialized.
        mTestHelper.getHandler().getExportSuccessCallback().onResult(12, tempFile.getPath());

        // Check that the progress bar is still shown, though, because the timer has not gone off
        // yet.
        onView(withText(R.string.settings_passwords_preparing_export))
                .inRoot(isDialog())
                .check(matches(isCompletelyDisplayed()));

        // Now mark the timer as gone off and check that the progress bar is hidden.
        allowProgressBarToBeHidden();
        onView(withText(R.string.settings_passwords_preparing_export)).check(doesNotExist());

        intended(
                allOf(
                        hasAction(equalTo(Intent.ACTION_CHOOSER)),
                        hasExtras(
                                hasEntry(
                                        equalTo(Intent.EXTRA_INTENT),
                                        allOf(
                                                hasAction(equalTo(Intent.ACTION_SEND)),
                                                hasType("text/csv"))))));

        Intents.release();

        tempFile.delete();
    }

    /**
     * Check that a progress bar is displayed when the user confirms the export and the serialized
     * passwords are not ready yet.
     */
    @Test
    @SmallTest
    @DisableFeatures(UNIFIED_PASSWORD_MANAGER_LOCAL_PWD_MIGRATION_WARNING)
    @Feature({"Preferences"})
    public void testExportProgress() throws Exception {
        mTestHelper.setPasswordSource(
                new SavedPasswordEntry("https://example.com", "test user", "password"));

        ReauthenticationManager.setApiOverride(ReauthenticationManager.OverrideState.AVAILABLE);
        ReauthenticationManager.setScreenLockSetUpOverride(
                ReauthenticationManager.OverrideState.AVAILABLE);

        final SettingsActivity settingsActivity =
                mTestHelper.startPasswordSettingsFromMainSettings(mSettingsActivityTestRule);

        Intents.init();

        reauthenticateAndRequestExport(settingsActivity);

        // Before triggering the sharing intent chooser, stub it out to avoid leaving system UI open
        // after the test is finished.
        intending(hasAction(equalTo(Intent.ACTION_CHOOSER)))
                .respondWith(new Instrumentation.ActivityResult(Activity.RESULT_OK, null));

        // Confirm the export warning to fire the sharing intent.
        onViewWaiting(
                        allOf(
                                withText(R.string.password_settings_export_action_title),
                                isCompletelyDisplayed()),
                        /* checkRootDialog= */ true)
                .perform(click());

        // Before simulating the serialized passwords being received, check that the progress bar is
        // shown.
        onViewWaiting(
                        withText(R.string.settings_passwords_preparing_export),
                        /* checkRootDialog= */ true)
                .check(matches(isCompletelyDisplayed()));

        File tempFile = createFakeExportedPasswordsFile();

        // Now pretend that passwords have been serialized.
        allowProgressBarToBeHidden();
        mTestHelper.getHandler().getExportSuccessCallback().onResult(12, tempFile.getPath());

        // After simulating the serialized passwords being received, check that the progress bar is
        // hidden.
        onView(withText(R.string.settings_passwords_preparing_export)).check(doesNotExist());

        intended(
                allOf(
                        hasAction(equalTo(Intent.ACTION_CHOOSER)),
                        hasExtras(
                                hasEntry(
                                        equalTo(Intent.EXTRA_INTENT),
                                        allOf(
                                                hasAction(equalTo(Intent.ACTION_SEND)),
                                                hasType("text/csv"))))));

        Intents.release();

        tempFile.delete();
    }

    /** Check that the user can cancel exporting with the "Cancel" button on the progressbar. */
    @Test
    @SmallTest
    @Feature({"Preferences"})
    public void testExportCancelOnProgress() {
        mTestHelper.setPasswordSource(
                new SavedPasswordEntry("https://example.com", "test user", "password"));

        ReauthenticationManager.setApiOverride(ReauthenticationManager.OverrideState.AVAILABLE);
        ReauthenticationManager.setScreenLockSetUpOverride(
                ReauthenticationManager.OverrideState.AVAILABLE);

        final SettingsActivity settingsActivity =
                mTestHelper.startPasswordSettingsFromMainSettings(mSettingsActivityTestRule);

        reauthenticateAndRequestExport(settingsActivity);

        // Confirm the export warning to fire the sharing intent.
        onViewWaiting(
                        allOf(
                                withText(R.string.password_settings_export_action_title),
                                isCompletelyDisplayed()),
                        /* checkRootDialog= */ true)
                .perform(click());

        // Simulate the minimal time for showing the progress bar to have passed, to ensure that it
        // is kept live because of the pending serialization.
        allowProgressBarToBeHidden();

        // Check that the progress bar is shown.
        onView(withText(R.string.settings_passwords_preparing_export))
                .inRoot(isDialog())
                .check(matches(isCompletelyDisplayed()));

        // Hit the Cancel button.
        onView(withText(R.string.cancel)).inRoot(isDialog()).perform(click());

        // Check that the cancellation succeeded by checking that the export menu is available and
        // enabled.
        checkExportMenuItemState(true);
    }

    /** Check that the user can cancel exporting with the negative button on the error message. */
    @Test
    @SmallTest
    @Feature({"Preferences"})
    public void testExportCancelOnError() {
        mTestHelper.setPasswordSource(
                new SavedPasswordEntry("https://example.com", "test user", "password"));

        ReauthenticationManager.setApiOverride(ReauthenticationManager.OverrideState.AVAILABLE);
        ReauthenticationManager.setScreenLockSetUpOverride(
                ReauthenticationManager.OverrideState.AVAILABLE);

        final SettingsActivity settingsActivity =
                mTestHelper.startPasswordSettingsFromMainSettings(mSettingsActivityTestRule);

        reauthenticateAndRequestExport(settingsActivity);

        // Confirm the export warning.
        onViewWaiting(
                        allOf(
                                withText(R.string.password_settings_export_action_title),
                                isCompletelyDisplayed()),
                        /* checkRootDialog= */ true)
                .perform(click());

        // Show an arbitrary error. This should replace the progress bar if that has been shown in
        // the meantime.
        allowProgressBarToBeHidden();
        requestShowingExportError();

        // Check that the error prompt is showing.
        onView(withText(R.string.password_settings_export_error_title))
                .inRoot(isDialog())
                .check(matches(isCompletelyDisplayed()));

        // Hit the negative button on the error prompt.
        onView(withText(R.string.close)).inRoot(isDialog()).perform(click());

        // Check that the cancellation succeeded by checking that the export menu is available and
        // enabled.
        checkExportMenuItemState(true);
    }

    /**
     * Check that the user can re-trigger the export from an error dialog which has a "retry"
     * button.
     */
    @Test
    @SmallTest
    @Feature({"Preferences"})
    public void testExportRetry() {
        mTestHelper.setPasswordSource(
                new SavedPasswordEntry("https://example.com", "test user", "password"));

        ReauthenticationManager.setApiOverride(ReauthenticationManager.OverrideState.AVAILABLE);
        ReauthenticationManager.setScreenLockSetUpOverride(
                ReauthenticationManager.OverrideState.AVAILABLE);

        final SettingsActivity settingsActivity =
                mTestHelper.startPasswordSettingsFromMainSettings(mSettingsActivityTestRule);

        reauthenticateAndRequestExport(settingsActivity);

        // Confirm the export warning.
        onViewWaiting(
                        allOf(
                                withText(R.string.password_settings_export_action_title),
                                isCompletelyDisplayed()),
                        /* checkRootDialog= */ true)
                .perform(click());

        // Show an arbitrary error but ensure that the positive button label is the one for "try
        // again".
        allowProgressBarToBeHidden();
        requestShowingExportErrorWithButton(R.string.try_again);

        // Hit the positive button to try again.
        onView(withText(R.string.try_again)).inRoot(isDialog()).perform(click());

        // Check that there is again the export warning.
        onView(withText(R.string.password_settings_export_action_title))
                .inRoot(isDialog())
                .check(matches(isCompletelyDisplayed()));
    }

    /**
     * Check that the error dialog lets the user visit a help page to install Google Drive if they
     * need an app to consume the exported passwords.
     */
    @Test
    @SmallTest
    @Feature({"Preferences"})
    public void testExportHelpSite() {
        mTestHelper.setPasswordSource(
                new SavedPasswordEntry("https://example.com", "test user", "password"));

        ReauthenticationManager.setApiOverride(ReauthenticationManager.OverrideState.AVAILABLE);
        ReauthenticationManager.setScreenLockSetUpOverride(
                ReauthenticationManager.OverrideState.AVAILABLE);

        final SettingsActivity settingsActivity =
                mTestHelper.startPasswordSettingsFromMainSettings(mSettingsActivityTestRule);

        reauthenticateAndRequestExport(settingsActivity);

        // Confirm the export warning.
        onViewWaiting(
                        allOf(
                                withText(R.string.password_settings_export_action_title),
                                isCompletelyDisplayed()),
                        true)
                .perform(click());

        // Show an arbitrary error but ensure that the positive button label is the one for the
        // Google Drive help site.
        allowProgressBarToBeHidden();
        requestShowingExportErrorWithButton(R.string.password_settings_export_learn_google_drive);

        Intents.init();

        // Before triggering the viewing intent, stub it out to avoid cascading that into further
        // intents and opening the web browser.
        intending(hasAction(equalTo(Intent.ACTION_VIEW)))
                .respondWith(new Instrumentation.ActivityResult(Activity.RESULT_OK, null));

        // Hit the positive button to navigate to the help site.
        onView(withText(R.string.password_settings_export_learn_google_drive))
                .inRoot(isDialog())
                .perform(click());

        intended(
                allOf(
                        hasAction(equalTo(Intent.ACTION_VIEW)),
                        hasData(hasHost(equalTo("support.google.com")))));

        Intents.release();
    }

    /**
     * Check that if errors are encountered when user is busy confirming the export, the error UI is
     * shown after the confirmation is completed, so that the user does not see UI changing
     * unexpectedly under their fingers.
     */
    @Test
    @SmallTest
    @Feature({"Preferences"})
    public void testExportErrorUiAfterConfirmation() {
        mTestHelper.setPasswordSource(
                new SavedPasswordEntry("https://example.com", "test user", "password"));

        ReauthenticationManager.setApiOverride(ReauthenticationManager.OverrideState.AVAILABLE);
        ReauthenticationManager.setScreenLockSetUpOverride(
                ReauthenticationManager.OverrideState.AVAILABLE);

        final SettingsActivity settingsActivity =
                mTestHelper.startPasswordSettingsFromMainSettings(mSettingsActivityTestRule);

        reauthenticateAndRequestExport(settingsActivity);

        // Request showing an arbitrary error while the confirmation dialog is still up.
        requestShowingExportError();

        // Check that the confirmation dialog is showing and dismiss it.
        onViewWaiting(
                        allOf(
                                withText(R.string.password_settings_export_action_title),
                                isCompletelyDisplayed()),
                        /* checkRootDialog= */ true)
                .perform(click());

        // Check that now the error is displayed, instead of the progress bar.
        allowProgressBarToBeHidden();
        onView(withText(R.string.settings_passwords_preparing_export)).check(doesNotExist());
        onView(withText(R.string.password_settings_export_error_title))
                .check(matches(isCompletelyDisplayed()));

        // Close the error dialog and abort the export.
        onView(withText(R.string.close)).perform(click());

        // Ensure that there is still no progress bar.
        onView(withText(R.string.settings_passwords_preparing_export)).check(doesNotExist());
    }

    @Test
    @SmallTest
    public void testDontRepeatedlySerialisePasswords() {
        mTestHelper.setPasswordSource(
                new SavedPasswordEntry("https://example.com", "test user", "password"));

        ReauthenticationManager.setApiOverride(ReauthenticationManager.OverrideState.AVAILABLE);
        ReauthenticationManager.setScreenLockSetUpOverride(
                ReauthenticationManager.OverrideState.AVAILABLE);

        final SettingsActivity settingsActivity =
                mTestHelper.startPasswordSettingsFromMainSettings(mSettingsActivityTestRule);

        PasswordSettings fragment = mSettingsActivityTestRule.getFragment();
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    ExportFlow exportFlow = fragment.getExportFlowForTesting();
                    exportFlow.startExporting();
                    exportFlow.passwordsAvailable();
                    exportFlow.passwordsAvailable();
                });

        Assert.assertEquals(1, mTestHelper.getHandler().getSerializationInvocationCount());
    }

    /**
     * Taps the menu item to trigger exporting and ensures that reauthentication passes. It also
     * disables the timer in {@link DialogManager} which is used to allow hiding the progress bar
     * after an initial period. Hiding can be later allowed manually in tests with {@link
     * #allowProgressBarToBeHidden}, to avoid time-dependent flakiness.
     */
    private void reauthenticateAndRequestExport(SettingsActivity settingsActivity) {
        openActionBarOverflowOrOptionsMenu(
                InstrumentationRegistry.getInstrumentation().getTargetContext());

        // Avoid launching the Android-provided reauthentication challenge, which cannot be
        // completed in the test.
        ReauthenticationManager.setSkipSystemReauth(true);
        onViewWaiting(
                        allOf(
                                withText(R.string.password_settings_export_action_title),
                                isCompletelyDisplayed()))
                .perform(click());

        // Now Chrome thinks it triggered the challenge and is waiting to be resumed. Once resumed
        // it will check the reauthentication result. First, update the reauth timestamp to indicate
        // a successful reauth:
        ReauthenticationManager.recordLastReauth(
                System.currentTimeMillis(), ReauthenticationManager.ReauthScope.BULK);

        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    // Disable the timer for progress bar.
                    PasswordSettings fragment = mSettingsActivityTestRule.getFragment();
                    fragment.getExportFlowForTesting()
                            .getDialogManagerForTesting()
                            .replaceCallbackDelayerForTesting(mTestHelper.getManualDelayer());
                    // Now call onResume to nudge Chrome into continuing the export flow.
                    settingsActivity.getMainFragment().onResume();
                });
    }

    /**
     * Checks that the menu item for exporting passwords is enabled or disabled as expected.
     *
     * @param isMenuItemStateEnabled The expected state of the menu item.
     */
    private void checkExportMenuItemState(boolean isMenuItemStateEnabled) {
        openActionBarOverflowOrOptionsMenu(
                InstrumentationRegistry.getInstrumentation().getTargetContext());
        // Matches a TextView, but the disabled entity is a wrapper higher in the menu's hierarchy.
        final Matcher<View> isDescendantOfDisabledParent = isDescendantOfA(not(isEnabled()));
        onViewWaiting(withText(R.string.password_settings_export_action_title))
                .check(
                        matches(
                                isMenuItemStateEnabled
                                        ? not(isDescendantOfDisabledParent)
                                        : isDescendantOfDisabledParent));
    }

    /** Requests showing an arbitrary password export error. */
    private void requestShowingExportError() {
        ThreadUtils.runOnUiThreadBlocking(
                mTestHelper.getHandler().getExportErrorCallback().bind("Arbitrary error"));
    }

    /**
     * Requests showing an arbitrary password export error with a particular positive button to be
     * shown. If you don't care about the button, just call {@link #requestShowingExportError}.
     *
     * @param positiveButtonLabelId controls which label the positive button ends up having.
     */
    private void requestShowingExportErrorWithButton(int positiveButtonLabelId) {
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    PasswordSettings fragment = mSettingsActivityTestRule.getFragment();
                    // To show an error, the error type for UMA needs to be specified. Because it is
                    // not relevant for cases when the error is forcibly displayed in tests,
                    // HistogramExportResult.NO_CONSUMER is passed as an arbitrarily chosen value.
                    fragment.getExportFlowForTesting()
                            .showExportErrorAndAbort(
                                    R.string.password_settings_export_no_app,
                                    null,
                                    positiveButtonLabelId,
                                    HistogramExportResult.NO_CONSUMER);
                });
    }

    /**
     * Sends the signal to {@link DialogManager} that the minimal time for showing the progress bar
     * has passed. This results in the progress bar getting hidden as soon as requested.
     */
    private void allowProgressBarToBeHidden() {
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mTestHelper.getManualDelayer().runCallbacksSynchronously();
                });
    }

    /**
     * Create a temporary file in the cache sub-directory for exported passwords, which the test can
     * try to use for sharing.
     *
     * @return The {@link File} handle for such temporary file.
     */
    private static File createFakeExportedPasswordsFile() throws IOException {
        File passwordsDir = new File(ExportFlow.getTargetDirectory());
        // Ensure that the directory exists.
        passwordsDir.mkdir();
        File tempFile = File.createTempFile("test", ".csv", passwordsDir);
        FileWriter writer = new FileWriter(tempFile);
        writer.write("Fake serialized passwords");

        writer.close();
        return tempFile;
    }

    /**
     * Creates an empty file, which can be used as the result of ACTION_CREATE_DOCUMENT intent.
     *
     * @return The newly created empty file.
     */
    private File createFakeSavedPasswordsFile() throws IOException {
        File passwordsDir = new File(ExportFlow.getTargetDirectory());
        // Ensure that the directory exists.
        passwordsDir.mkdir();
        File outputFile = new File(ExportFlow.getTargetDirectory(), "test_saved_passwords.csv");
        outputFile.createNewFile();
        return outputFile;
    }
}