Commit b76be05f authored by Vaclav Brozek's avatar Vaclav Brozek Committed by Commit Bot

[Android passwords settings] Add a progress bar

This CL adds a progress bar to the password export flow.

The progress bar is shown if the user already completed
reauthentication and export confirmation but the passwords
are still being serialized in the background. The user
can cancel the whole export with a Cancel button next
to the progress bar. The screenshot is at
https://crbug.com/788701#c22, the mocks at
go chrome-pwd-export-mocks-android (Google only).

This CL also merges the existing flags mExportOptionSuspended,
mExportRequested and mExportConfirmed into a single mExportState
to improve the clarity of the whole process.

Bug: 788701
Change-Id: I1857b135df255124a1507139095f36eca7c3dc3d
Reviewed-on: https://chromium-review.googlesource.com/881019Reviewed-by: default avatarTheresa <twellington@chromium.org>
Commit-Queue: Vaclav Brozek <vabr@chromium.org>
Cr-Commit-Position: refs/heads/master@{#533335}
parent 0d46f955
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2018 The Chromium Authors. All rights reserved.
Use of this source code is governed by a BSD-style license that can be
found in the LICENSE file. -->
<!-- Used by ProgressBarDialogFragment of passwords preferences. -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
style="@style/AlertDialogContent">
<org.chromium.chrome.browser.widget.MaterialProgressBar
android:id="@+id/passwords_progress_bar"
android:layout_marginTop="14dp"
android:layout_width="match_parent"
android:layout_height="4dp"/>
</LinearLayout>
// Copyright 2018 The Chromium Authors. All rights reserved.
// 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.preferences.password;
import android.app.Dialog;
import android.app.DialogFragment;
import android.content.DialogInterface;
import android.os.Bundle;
import android.support.v7.app.AlertDialog;
import android.view.View;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.widget.MaterialProgressBar;
/**
* Shows the dialog that informs the user about the progress of preparing passwords for export and
* allows the user to cancel that operation.
*/
public class ProgressBarDialogFragment extends DialogFragment {
// This handler is used to perform the user-triggered cancellation of the password preparation.
private DialogInterface.OnClickListener mHandler;
public void setCancelProgressHandler(DialogInterface.OnClickListener handler) {
mHandler = handler;
}
/**
* Opens the dialog with the progress bar, hooks up the cancel button handler and sets the
* progress indicator to being indeterminate, because the background operation does not easily
* allow to signal its own progress.
*/
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
View dialog =
getActivity().getLayoutInflater().inflate(R.layout.passwords_progress_dialog, null);
MaterialProgressBar bar =
(MaterialProgressBar) dialog.findViewById(R.id.passwords_progress_bar);
bar.setIndeterminate(true);
return new AlertDialog.Builder(getActivity(), R.style.SimpleDialog)
.setView(dialog)
.setNegativeButton(R.string.cancel, mHandler)
.setTitle(getActivity().getResources().getString(
R.string.settings_passwords_preparing_export))
.create();
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// If there is a |savedInstanceState|, then the dialog is being recreated
// by Android and will lack the necessary click handler. Dismiss
// immediately, the settings page will recreate it with the appropriate
// click handler.
if (savedInstanceState != null) {
dismiss();
return;
}
}
}
...@@ -493,7 +493,7 @@ CHAR-LIMIT guidelines: ...@@ -493,7 +493,7 @@ CHAR-LIMIT guidelines:
Showing password generation popup Showing password generation popup
</message> </message>
<message name="IDS_SAVE_PASSWORD_PREFERENCES_EXPORT_ACTION_TITLE" desc="The title of a menu item to trigger exporting passwords from the password settings."> <message name="IDS_SAVE_PASSWORD_PREFERENCES_EXPORT_ACTION_TITLE" desc="The title of a menu item to trigger exporting passwords from the password settings.">
Export passwords... Export passwords
</message> </message>
<message name="IDS_SAVE_PASSWORD_PREFERENCES_EXPORT_ACTION_DESCRIPTION" desc="The description of a menu item to trigger exporting passwords from the password settings."> <message name="IDS_SAVE_PASSWORD_PREFERENCES_EXPORT_ACTION_DESCRIPTION" desc="The description of a menu item to trigger exporting passwords from the password settings.">
Export passwords stored with Chrome Export passwords stored with Chrome
...@@ -501,6 +501,9 @@ CHAR-LIMIT guidelines: ...@@ -501,6 +501,9 @@ CHAR-LIMIT guidelines:
<message name="IDS_SETTINGS_PASSWORDS_EXPORT_DESCRIPTION" desc="Text shown to the user who initiated exporting passwords, as a warning before any passwords have been exported."> <message name="IDS_SETTINGS_PASSWORDS_EXPORT_DESCRIPTION" desc="Text shown to the user who initiated exporting passwords, as a warning before any passwords have been exported.">
Your passwords will be visible to anyone who can see the exported file. Your passwords will be visible to anyone who can see the exported file.
</message> </message>
<message name="IDS_SETTINGS_PASSWORDS_PREPARING_EXPORT" desc="Text shown to the user above a progress bar, informing the user that passwords are being prepared for export.">
Preparing passwords…
</message>
<!-- Lock Screen Fragment --> <!-- Lock Screen Fragment -->
<message name="IDS_LOCKSCREEN_DESCRIPTION_COPY" desc="When a user attempts to copy a password for a particular website into clipboard in Chrome's settings, Chrome launches a lock screen to verify the user's identity and displays the following explanation."> <message name="IDS_LOCKSCREEN_DESCRIPTION_COPY" desc="When a user attempts to copy a password for a particular website into clipboard in Chrome's settings, Chrome launches a lock screen to verify the user's identity and displays the following explanation.">
......
...@@ -987,6 +987,7 @@ chrome_java_sources = [ ...@@ -987,6 +987,7 @@ chrome_java_sources = [
"java/src/org/chromium/chrome/browser/preferences/password/PasswordManagerHandlerProvider.java", "java/src/org/chromium/chrome/browser/preferences/password/PasswordManagerHandlerProvider.java",
"java/src/org/chromium/chrome/browser/preferences/password/PasswordReauthenticationFragment.java", "java/src/org/chromium/chrome/browser/preferences/password/PasswordReauthenticationFragment.java",
"java/src/org/chromium/chrome/browser/preferences/password/PasswordUIView.java", "java/src/org/chromium/chrome/browser/preferences/password/PasswordUIView.java",
"java/src/org/chromium/chrome/browser/preferences/password/ProgressBarDialogFragment.java",
"java/src/org/chromium/chrome/browser/preferences/password/ReauthenticationManager.java", "java/src/org/chromium/chrome/browser/preferences/password/ReauthenticationManager.java",
"java/src/org/chromium/chrome/browser/preferences/password/SavedPasswordEntry.java", "java/src/org/chromium/chrome/browser/preferences/password/SavedPasswordEntry.java",
"java/src/org/chromium/chrome/browser/preferences/password/SavePasswordsPreferences.java", "java/src/org/chromium/chrome/browser/preferences/password/SavePasswordsPreferences.java",
......
...@@ -35,6 +35,7 @@ import static org.hamcrest.Matchers.startsWith; ...@@ -35,6 +35,7 @@ import static org.hamcrest.Matchers.startsWith;
import android.app.Activity; import android.app.Activity;
import android.app.Instrumentation; import android.app.Instrumentation;
import android.content.Intent; import android.content.Intent;
import android.support.annotation.Nullable;
import android.support.test.InstrumentationRegistry; import android.support.test.InstrumentationRegistry;
import android.support.test.espresso.Espresso; import android.support.test.espresso.Espresso;
import android.support.test.espresso.intent.Intents; import android.support.test.espresso.intent.Intents;
...@@ -95,8 +96,9 @@ public class SavePasswordsPreferencesTest { ...@@ -95,8 +96,9 @@ public class SavePasswordsPreferencesTest {
// The faked contents of the saves password exceptions to be displayed. // The faked contents of the saves password exceptions to be displayed.
private ArrayList<String> mSavedPasswordExeptions = new ArrayList<>(); private ArrayList<String> mSavedPasswordExeptions = new ArrayList<>();
// This is set to true when serializePasswords is called. // This is set once {@link #serializePasswords()} is called.
private boolean mSerializePasswordsCalled; @Nullable
private Callback<String> mExportCallback;
public void setSavedPasswords(ArrayList<SavedPasswordEntry> savedPasswords) { public void setSavedPasswords(ArrayList<SavedPasswordEntry> savedPasswords) {
mSavedPasswords = savedPasswords; mSavedPasswords = savedPasswords;
...@@ -106,8 +108,8 @@ public class SavePasswordsPreferencesTest { ...@@ -106,8 +108,8 @@ public class SavePasswordsPreferencesTest {
mSavedPasswordExeptions = savedPasswordExceptions; mSavedPasswordExeptions = savedPasswordExceptions;
} }
public boolean getSerializePasswordsCalled() { public Callback<String> getExportCallback() {
return mSerializePasswordsCalled; return mExportCallback;
} }
/** /**
...@@ -152,8 +154,7 @@ public class SavePasswordsPreferencesTest { ...@@ -152,8 +154,7 @@ public class SavePasswordsPreferencesTest {
@Override @Override
public void serializePasswords(Callback<String> callback) { public void serializePasswords(Callback<String> callback) {
callback.onResult("serialized passwords"); mExportCallback = callback;
mSerializePasswordsCalled = true;
} }
} }
...@@ -236,6 +237,20 @@ public class SavePasswordsPreferencesTest { ...@@ -236,6 +237,20 @@ public class SavePasswordsPreferencesTest {
} }
} }
/**
* Taps the menu item to trigger exporting and ensures that reauthentication passes.
*/
private void reauthenticateAndRequestExport() {
Espresso.openActionBarOverflowOrOptionsMenu(
InstrumentationRegistry.getInstrumentation().getTargetContext());
// Before exporting, pretend that the last successful reauthentication just
// happened. This will allow the export flow to continue.
ReauthenticationManager.recordLastReauth(
System.currentTimeMillis(), ReauthenticationManager.REAUTH_SCOPE_BULK);
Espresso.onView(withText(R.string.save_password_preferences_export_action_title))
.perform(click());
}
/** /**
* Ensure that resetting of empty passwords list works. * Ensure that resetting of empty passwords list works.
*/ */
...@@ -476,7 +491,7 @@ public class SavePasswordsPreferencesTest { ...@@ -476,7 +491,7 @@ public class SavePasswordsPreferencesTest {
Espresso.onView(withText(R.string.save_password_preferences_export_action_title)) Espresso.onView(withText(R.string.save_password_preferences_export_action_title))
.perform(click()); .perform(click());
Assert.assertTrue(mHandler.getSerializePasswordsCalled()); Assert.assertTrue(mHandler.getExportCallback() != null);
} }
/** /**
...@@ -631,15 +646,50 @@ public class SavePasswordsPreferencesTest { ...@@ -631,15 +646,50 @@ public class SavePasswordsPreferencesTest {
Intents.init(); Intents.init();
Espresso.openActionBarOverflowOrOptionsMenu( reauthenticateAndRequestExport();
InstrumentationRegistry.getInstrumentation().getTargetContext());
// Before exporting, pretend that the last successful reauthentication just // Pretend that passwords have been serialized to go directly to the intent.
// happened. This will allow the export flow to continue. mHandler.getExportCallback().onResult("serialized passwords");
ReauthenticationManager.recordLastReauth(
System.currentTimeMillis(), ReauthenticationManager.REAUTH_SCOPE_BULK); // 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.
Espresso.onView(withText(R.string.save_password_preferences_export_action_title)) Espresso.onView(withText(R.string.save_password_preferences_export_action_title))
.perform(click()); .perform(click());
intended(allOf(hasAction(equalTo(Intent.ACTION_CHOOSER)),
hasExtras(hasEntry(equalTo(Intent.EXTRA_INTENT),
allOf(hasAction(equalTo(Intent.ACTION_SEND)), hasType("text/csv"))))));
Intents.release();
}
/**
* Check that a progressbar is displayed when the user confirms the export and the serialized
* passwords are not ready yet.
*/
@Test
@SmallTest
@Feature({"Preferences"})
@EnableFeatures("PasswordExport")
public void testExportProgress() throws Exception {
setPasswordSource(new SavedPasswordEntry("https://example.com", "test user", "password"));
ReauthenticationManager.setApiOverride(ReauthenticationManager.OVERRIDE_STATE_AVAILABLE);
ReauthenticationManager.setScreenLockSetUpOverride(
ReauthenticationManager.OVERRIDE_STATE_AVAILABLE);
final Preferences preferences =
PreferencesTest.startPreferences(InstrumentationRegistry.getInstrumentation(),
SavePasswordsPreferences.class.getName());
Intents.init();
reauthenticateAndRequestExport();
// Before triggering the sharing intent chooser, stub it out to avoid leaving system UI open // Before triggering the sharing intent chooser, stub it out to avoid leaving system UI open
// after the test is finished. // after the test is finished.
intending(hasAction(equalTo(Intent.ACTION_CHOOSER))) intending(hasAction(equalTo(Intent.ACTION_CHOOSER)))
...@@ -649,6 +699,19 @@ public class SavePasswordsPreferencesTest { ...@@ -649,6 +699,19 @@ public class SavePasswordsPreferencesTest {
Espresso.onView(withText(R.string.save_password_preferences_export_action_title)) Espresso.onView(withText(R.string.save_password_preferences_export_action_title))
.perform(click()); .perform(click());
// Before simulating the serialized passwords being received, check that the progress bar is
// shown.
Espresso.onView(withText(R.string.settings_passwords_preparing_export))
.check(matches(isDisplayed()));
// Now pretend that passwords have been serialized.
mHandler.getExportCallback().onResult("serialized passwords");
// Before simulating the serialized passwords being received, check that the progress bar is
// hidden.
Espresso.onView(withText(R.string.settings_passwords_preparing_export))
.check(doesNotExist());
intended(allOf(hasAction(equalTo(Intent.ACTION_CHOOSER)), intended(allOf(hasAction(equalTo(Intent.ACTION_CHOOSER)),
hasExtras(hasEntry(equalTo(Intent.EXTRA_INTENT), hasExtras(hasEntry(equalTo(Intent.EXTRA_INTENT),
allOf(hasAction(equalTo(Intent.ACTION_SEND)), hasType("text/csv")))))); allOf(hasAction(equalTo(Intent.ACTION_SEND)), hasType("text/csv"))))));
...@@ -656,6 +719,48 @@ public class SavePasswordsPreferencesTest { ...@@ -656,6 +719,48 @@ public class SavePasswordsPreferencesTest {
Intents.release(); Intents.release();
} }
/**
* Check that the user can cancel exporting with the "Cancel" button on the progressbar.
*/
@Test
@SmallTest
@Feature({"Preferences"})
@EnableFeatures("PasswordExport")
public void testExportCancelOnProgress() throws Exception {
setPasswordSource(new SavedPasswordEntry("https://example.com", "test user", "password"));
ReauthenticationManager.setApiOverride(ReauthenticationManager.OVERRIDE_STATE_AVAILABLE);
ReauthenticationManager.setScreenLockSetUpOverride(
ReauthenticationManager.OVERRIDE_STATE_AVAILABLE);
final Preferences preferences =
PreferencesTest.startPreferences(InstrumentationRegistry.getInstrumentation(),
SavePasswordsPreferences.class.getName());
reauthenticateAndRequestExport();
// Confirm the export warning to fire the sharing intent.
Espresso.onView(withText(R.string.save_password_preferences_export_action_title))
.perform(click());
// Check that the progress bar is shown.
Espresso.onView(withText(R.string.settings_passwords_preparing_export))
.check(matches(isDisplayed()));
// Hit the Cancel button.
Espresso.onView(withText(R.string.cancel)).perform(click());
// Check that the cancellation succeeded by checking that the export menu is available and
// enabled.
openActionBarOverflowOrOptionsMenu(
InstrumentationRegistry.getInstrumentation().getTargetContext());
// The text matches a text view, but the potentially disabled entity is some wrapper two
// levels up in the view hierarchy, hence the two withParent matchers.
Espresso.onView(allOf(withText(R.string.save_password_preferences_export_action_title),
withParent(withParent(isEnabled()))))
.check(matches(isDisplayed()));
}
/** /**
* Check whether the user is asked to set up a screen lock if attempting to view passwords. * Check whether the user is asked to set up a screen lock if attempting to view passwords.
*/ */
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment