Commit 699b380d authored by Ioana Pandele's avatar Ioana Pandele Committed by Commit Bot

Wire automatic generation via the keyboard accessory

Automatic password generation is offered for password fields that
Chrome perceives as being eligible for generation.

When such a field is focused, a button to trigger generation is
presented in the keyboard accessory.Tapping on this button brings up
a modal dialog that displays a generated password with confirm and
cancel buttons. Once the user taps the "Use password" button in the
dialog, the generated password is filled in the field and can be
edited in place.

Bug: 835234
Change-Id: I2f0e5dc3a0f9153109207a4fd8624cd4eb6e03c3
Reviewed-on: https://chromium-review.googlesource.com/1097478Reviewed-by: default avatarVasilii Sukhanov <vasilii@chromium.org>
Reviewed-by: default avatarBernhard Bauer <bauerb@chromium.org>
Reviewed-by: default avatarFriedrich Horschig <fhorschig@chromium.org>
Commit-Queue: Ioana Pandele <ioanap@chromium.org>
Cr-Commit-Position: refs/heads/master@{#569672}
parent d85d2145
......@@ -4,22 +4,31 @@
package org.chromium.chrome.browser.autofill.keyboard_accessory;
import android.support.annotation.Nullable;
import org.chromium.base.annotations.CalledByNative;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.ChromeActivity;
import org.chromium.chrome.browser.autofill.keyboard_accessory.KeyboardAccessoryData.Action;
import org.chromium.chrome.browser.autofill.keyboard_accessory.KeyboardAccessoryData.Item;
import org.chromium.ui.base.WindowAndroid;
class PasswordAccessoryBridge {
private final KeyboardAccessoryData.PropertyProvider<Item> mItemProvider =
new KeyboardAccessoryData.PropertyProvider<>();
private final KeyboardAccessoryData.PropertyProvider<Action> mActionProvider =
new KeyboardAccessoryData.PropertyProvider<>();
private final ManualFillingCoordinator mManualFillingCoordinator;
private final ChromeActivity mActivity;
private @Nullable Action mGenerationAction;
private long mNativeView;
private PasswordAccessoryBridge(long nativeView, WindowAndroid windowAndroid) {
mNativeView = nativeView;
ChromeActivity activity = (ChromeActivity) windowAndroid.getActivity().get();
mManualFillingCoordinator = activity.getManualFillingController();
mActivity = (ChromeActivity) windowAndroid.getActivity().get();
mManualFillingCoordinator = mActivity.getManualFillingController();
mManualFillingCoordinator.registerPasswordProvider(mItemProvider);
mManualFillingCoordinator.registerActionProvider(mActionProvider);
}
@CalledByNative
......@@ -33,6 +42,35 @@ class PasswordAccessoryBridge {
mItemProvider.notifyObservers(convertToItems(text, description, isPassword, itemType));
}
@CalledByNative
private void onAutomaticGenerationStatusChanged(boolean available) {
final Action[] generationAction;
if (available) {
if (mGenerationAction != null) {
return;
}
// This is meant to suppress the warning that the short string is not used.
// TODO(crbug.com/855581): Switch between strings based on whether they fit on the
// screen or not.
boolean useLongString = true;
String caption = useLongString
? mActivity.getString(R.string.password_generation_accessory_button)
: mActivity.getString(R.string.password_generation_accessory_button_short);
mGenerationAction = new Action(caption, (action) -> {
assert mNativeView
!= 0 : "Controller has been destroyed but the bridge wasn't cleaned up!";
nativeOnGenerationRequested(mNativeView);
});
generationAction = new Action[] {mGenerationAction};
} else {
if (mGenerationAction == null) return;
mGenerationAction = null;
generationAction = new Action[0];
}
mActionProvider.notifyObservers(generationAction);
}
@CalledByNative
private void destroy() {
mItemProvider.notifyObservers(new Item[] {}); // There are no more items available!
......@@ -79,4 +117,5 @@ class PasswordAccessoryBridge {
long nativePasswordAccessoryViewAndroid, boolean isPassword, String textToFill);
private native void nativeOnOptionSelected(
long nativePasswordAccessoryViewAndroid, String selectedOption);
private native void nativeOnGenerationRequested(long nativePasswordAccessoryViewAndroid);
}
\ No newline at end of file
// 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.password_manager;
import org.chromium.base.annotations.CalledByNative;
import org.chromium.chrome.browser.ChromeActivity;
import org.chromium.ui.base.WindowAndroid;
/**
* JNI call glue between native password generation and Java objects.
*/
public class PasswordGenerationDialogBridge {
private long mNativePasswordGenerationDialogViewAndroid;
private final PasswordGenerationDialogCoordinator mPasswordGenerationDialog;
// TODO(ioanap): Get the generated password from the model once editing is in place.
private String mGeneratedPassword;
private PasswordGenerationDialogBridge(
WindowAndroid windowAndroid, long nativePasswordGenerationDialogViewAndroid) {
mNativePasswordGenerationDialogViewAndroid = nativePasswordGenerationDialogViewAndroid;
ChromeActivity activity = (ChromeActivity) windowAndroid.getActivity().get();
mPasswordGenerationDialog = new PasswordGenerationDialogCoordinator(activity);
}
@CalledByNative
public static PasswordGenerationDialogBridge create(
WindowAndroid windowAndroid, long nativeDialog) {
return new PasswordGenerationDialogBridge(windowAndroid, nativeDialog);
}
@CalledByNative
public void showDialog(String generatedPassword) {
mGeneratedPassword = generatedPassword;
mPasswordGenerationDialog.showDialog(
generatedPassword, this::onPasswordAcceptedOrRejected);
}
@CalledByNative
private void destroy() {
mNativePasswordGenerationDialogViewAndroid = 0;
mPasswordGenerationDialog.dismissDialog();
}
private void onPasswordAcceptedOrRejected(boolean accepted) {
if (mNativePasswordGenerationDialogViewAndroid == 0) {
return;
}
if (accepted) {
nativePasswordAccepted(mNativePasswordGenerationDialogViewAndroid, mGeneratedPassword);
} else {
nativePasswordRejected(mNativePasswordGenerationDialogViewAndroid);
}
mPasswordGenerationDialog.dismissDialog();
}
private native void nativePasswordAccepted(
long nativePasswordGenerationDialogViewAndroid, String generatedPassword);
private native void nativePasswordRejected(long nativePasswordGenerationDialogViewAndroid);
}
......@@ -3449,6 +3449,15 @@ However, you aren’t invisible. Going incognito doesn’t hide your browsing fr
Tap the home button to get to the new tab page in your current tab
</message>
<!-- Password Accessory Strings -->
<message name="IDS_PASSWORD_GENERATION_ACCESSORY_BUTTON" desc="Text for the button used to generate a password.">
Suggest strong password...
</message>
<message name="IDS_PASSWORD_GENERATION_ACCESSORY_BUTTON_SHORT" desc="Shortened text for the button used to generate a password. This is used for devices with small screen.">
Suggest password...
</message>
<!-- Launcher Shortcuts -->
<message name="IDS_DISABLED_INCOGNITO_LAUNCHER_SHORTCUT_MESSAGE" desc="Text for a toast displayed prompting the user to remove the disabled 'New incognito tab' app shortcut and recreate it.">
Remove and recreate this shortcut
......
......@@ -953,6 +953,7 @@ chrome_java_sources = [
"java/src/org/chromium/chrome/browser/partnercustomizations/PartnerBrowserCustomizations.java",
"java/src/org/chromium/chrome/browser/password_manager/AccountChooserDialog.java",
"java/src/org/chromium/chrome/browser/password_manager/AutoSigninFirstRunDialog.java",
"java/src/org/chromium/chrome/browser/password_manager/PasswordGenerationDialogBridge.java",
"java/src/org/chromium/chrome/browser/password_manager/PasswordGenerationDialogCoordinator.java",
"java/src/org/chromium/chrome/browser/password_manager/PasswordGenerationDialogMediator.java",
"java/src/org/chromium/chrome/browser/password_manager/PasswordGenerationDialogModel.java",
......
......@@ -2176,6 +2176,8 @@ jumbo_split_static_library("browser") {
"android/partner_browser_customizations.h",
"android/password_manager/password_accessory_view_android.cc",
"android/password_manager/password_accessory_view_android.h",
"android/password_manager/password_generation_dialog_view_android.cc",
"android/password_manager/password_generation_dialog_view_android.h",
"android/password_ui_view_android.cc",
"android/password_ui_view_android.h",
"android/payments/service_worker_payment_app_bridge.cc",
......@@ -2358,6 +2360,7 @@ jumbo_split_static_library("browser") {
"password_manager/password_accessory_controller.cc",
"password_manager/password_accessory_controller.h",
"password_manager/password_accessory_view_interface.h",
"password_manager/password_generation_dialog_view_interface.h",
"password_manager/password_manager_infobar_delegate_android.cc",
"password_manager/password_manager_infobar_delegate_android.h",
"password_manager/save_password_infobar_delegate_android.cc",
......@@ -4475,6 +4478,7 @@ if (is_android) {
"../android/java/src/org/chromium/chrome/browser/password_manager/AccountChooserDialog.java",
"../android/java/src/org/chromium/chrome/browser/password_manager/AutoSigninFirstRunDialog.java",
"../android/java/src/org/chromium/chrome/browser/password_manager/Credential.java",
"../android/java/src/org/chromium/chrome/browser/password_manager/PasswordGenerationDialogBridge.java",
"../android/java/src/org/chromium/chrome/browser/payments/JourneyLogger.java",
"../android/java/src/org/chromium/chrome/browser/payments/PaymentManifestWebDataService.java",
"../android/java/src/org/chromium/chrome/browser/payments/ServiceWorkerPaymentAppBridge.java",
......
......@@ -62,6 +62,16 @@ void PasswordAccessoryViewAndroid::OnItemsAvailable(
base::android::ToJavaIntArray(env, item_types));
}
void PasswordAccessoryViewAndroid::OnAutomaticGenerationStatusChanged(
bool available) {
if (!available && java_object_.is_null())
return;
JNIEnv* env = base::android::AttachCurrentThread();
Java_PasswordAccessoryBridge_onAutomaticGenerationStatusChanged(
env, java_object_, available /* available */);
}
void PasswordAccessoryViewAndroid::OnFillingTriggered(
JNIEnv* env,
const base::android::JavaParamRef<jobject>& obj,
......@@ -79,6 +89,12 @@ void PasswordAccessoryViewAndroid::OnOptionSelected(
base::android::ConvertJavaStringToUTF16(selectedOption));
}
void PasswordAccessoryViewAndroid::OnGenerationRequested(
JNIEnv* env,
const base::android::JavaParamRef<jobject>& obj) {
controller_->OnGenerationRequested();
}
// static
std::unique_ptr<PasswordAccessoryViewInterface>
PasswordAccessoryViewInterface::Create(
......
......@@ -25,6 +25,7 @@ class PasswordAccessoryViewAndroid : public PasswordAccessoryViewInterface {
// PasswordAccessoryViewInterface:
void OnItemsAvailable(const GURL& origin,
const std::vector<AccessoryItem>& items) override;
void OnAutomaticGenerationStatusChanged(bool available) override;
// Called from Java via JNI:
void OnFillingTriggered(
......@@ -36,6 +37,8 @@ class PasswordAccessoryViewAndroid : public PasswordAccessoryViewInterface {
JNIEnv* env,
const base::android::JavaParamRef<jobject>& obj,
const base::android::JavaParamRef<_jstring*>& selectedOption);
void OnGenerationRequested(JNIEnv* env,
const base::android::JavaParamRef<jobject>& obj);
private:
// The controller provides data for this view and owns it.
......
// 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.
#include "chrome/browser/android/password_manager/password_generation_dialog_view_android.h"
#include "base/android/jni_android.h"
#include "base/android/jni_string.h"
#include "chrome/browser/password_manager/password_accessory_controller.h"
#include "jni/PasswordGenerationDialogBridge_jni.h"
#include "ui/android/window_android.h"
PasswordGenerationDialogViewAndroid::PasswordGenerationDialogViewAndroid(
PasswordAccessoryController* controller)
: controller_(controller) {
ui::WindowAndroid* window_android = controller_->native_window();
DCHECK(window_android);
java_object_.Reset(Java_PasswordGenerationDialogBridge_create(
base::android::AttachCurrentThread(), window_android->GetJavaObject(),
reinterpret_cast<long>(this)));
}
PasswordGenerationDialogViewAndroid::~PasswordGenerationDialogViewAndroid() {
DCHECK(!java_object_.is_null());
Java_PasswordGenerationDialogBridge_destroy(
base::android::AttachCurrentThread(), java_object_);
}
void PasswordGenerationDialogViewAndroid::Show(base::string16& password) {
JNIEnv* env = base::android::AttachCurrentThread();
Java_PasswordGenerationDialogBridge_showDialog(
env, java_object_,
base::android::ConvertUTF16ToJavaString(env, password));
}
void PasswordGenerationDialogViewAndroid::PasswordAccepted(
JNIEnv* env,
const base::android::JavaParamRef<jobject>& obj,
const base::android::JavaParamRef<jstring>& password) {
controller_->GeneratedPasswordAccepted(
base::android::ConvertJavaStringToUTF16(env, password));
}
void PasswordGenerationDialogViewAndroid::PasswordRejected(
JNIEnv* env,
const base::android::JavaParamRef<jobject>& obj) {
// TODO(ioanap): Send message to controller. This will probably be used
// mainly for metrics.
}
// static
std::unique_ptr<PasswordGenerationDialogViewInterface>
PasswordGenerationDialogViewInterface::Create(
PasswordAccessoryController* controller) {
return std::make_unique<PasswordGenerationDialogViewAndroid>(controller);
}
// 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.
#ifndef CHROME_BROWSER_ANDROID_PASSWORD_MANAGER_PASSWORD_GENERATION_DIALOG_VIEW_ANDROID_H_
#define CHROME_BROWSER_ANDROID_PASSWORD_MANAGER_PASSWORD_GENERATION_DIALOG_VIEW_ANDROID_H_
#include <jni.h>
#include "base/android/scoped_java_ref.h"
#include "base/strings/string16.h"
#include "chrome/browser/password_manager/password_generation_dialog_view_interface.h"
class PasswordAccessoryController;
// Modal dialog displaying a generated password with options to accept or
// reject it. Communicates events to its Java counterpart and passes responses
// back to the |PasswordAccessoryController|.
// TODO(crbug.com/835234): Add a specialized dialog controller.
class PasswordGenerationDialogViewAndroid
: public PasswordGenerationDialogViewInterface {
public:
// Builds the UI for the |controller|
explicit PasswordGenerationDialogViewAndroid(
PasswordAccessoryController* controller);
~PasswordGenerationDialogViewAndroid() override;
// Called to show the dialog. |password| is the generated password.
void Show(base::string16& password) override;
// Called from Java via JNI.
void PasswordAccepted(JNIEnv* env,
const base::android::JavaParamRef<jobject>& obj,
const base::android::JavaParamRef<jstring>& password);
// Called from Java via JNI.
void PasswordRejected(JNIEnv* env,
const base::android::JavaParamRef<jobject>& obj);
private:
// The controller provides data for this view and owns it.
PasswordAccessoryController* controller_;
// The corresponding java object.
base::android::ScopedJavaGlobalRef<jobject> java_object_;
DISALLOW_COPY_AND_ASSIGN(PasswordGenerationDialogViewAndroid);
};
#endif // CHROME_BROWSER_ANDROID_PASSWORD_MANAGER_PASSWORD_GENERATION_DIALOG_VIEW_ANDROID_H_
......@@ -688,15 +688,41 @@ gfx::RectF ChromePasswordManagerClient::GetBoundsInScreenSpace(
void ChromePasswordManagerClient::AutomaticGenerationStatusChanged(
bool available,
const base::Optional<
autofill::password_generation::PasswordGenerationUIData>&
password_generation_ui_data) {
// TODO(crbug.com/835234): Use the case when available is false to
// remove the option from the keyboard accessory on Android and maybe
// to hide the popup on desktop.
autofill::password_generation::PasswordGenerationUIData>& ui_data) {
#if defined(OS_ANDROID)
// Either #passwords-keyboards-accessory or #experimental-ui must be enabled.
if (!base::FeatureList::IsEnabled(
password_manager::features::kPasswordsKeyboardAccessory) &&
!base::FeatureList::IsEnabled(features::kExperimentalUi)) {
if (available) {
ShowPasswordGenerationPopup(ui_data.value(),
false /* is_manually_triggered */);
}
} else {
PasswordAccessoryController::CreateForWebContents(web_contents());
if (available) {
password_manager::PasswordManagerDriver* driver =
driver_factory_->GetDriverForFrame(
password_manager_client_bindings_.GetCurrentTargetFrame());
DCHECK(driver);
password_manager_.SetGenerationElementAndReasonForForm(
driver, ui_data.value().password_form,
ui_data.value().generation_element,
false /* is_manually_triggered */);
PasswordAccessoryController::FromWebContents(web_contents())
->OnAutomaticGenerationStatusChanged(true, ui_data,
driver->AsWeakPtr());
} else {
PasswordAccessoryController::FromWebContents(web_contents())
->OnAutomaticGenerationStatusChanged(false, base::nullopt, nullptr);
}
}
#else
if (available) {
ShowPasswordGenerationPopup(password_generation_ui_data.value(),
ShowPasswordGenerationPopup(ui_data.value(),
false /* is_manually_triggered */);
}
#endif // defined(OS_ANDROID)
}
void ChromePasswordManagerClient::ShowManualPasswordGenerationPopup(
......
......@@ -8,6 +8,7 @@
#include "base/strings/utf_string_conversions.h"
#include "chrome/browser/password_manager/password_accessory_view_interface.h"
#include "chrome/browser/password_manager/password_generation_dialog_view_interface.h"
#include "chrome/browser/ui/passwords/manage_passwords_view_utils.h"
#include "chrome/grit/generated_resources.h"
#include "components/autofill/content/browser/content_autofill_driver.h"
......@@ -15,7 +16,9 @@
#include "components/autofill/core/common/password_form.h"
#include "components/password_manager/content/browser/content_password_manager_driver.h"
#include "components/password_manager/content/browser/content_password_manager_driver_factory.h"
#include "components/password_manager/core/browser/password_manager_driver.h"
#include "components/strings/grit/components_strings.h"
#include "content/public/browser/web_contents.h"
#include "ui/base/l10n/l10n_util.h"
#include "chrome/browser/android/preferences/preferences_launcher.h"
......@@ -25,28 +28,54 @@ using Item = PasswordAccessoryViewInterface::AccessoryItem;
DEFINE_WEB_CONTENTS_USER_DATA_KEY(PasswordAccessoryController);
struct PasswordAccessoryController::GenerationElementData {
GenerationElementData(autofill::FormSignature form_signature,
autofill::FieldSignature field_signature,
uint32_t max_password_length)
: form_signature(form_signature),
field_signature(field_signature),
max_password_length(max_password_length) {}
// Signature of the form for which password generation is triggered.
autofill::FormSignature form_signature;
// Signature of the field for which password generation is triggered.
autofill::FieldSignature field_signature;
// Maximum length of the generated password.
uint32_t max_password_length;
};
PasswordAccessoryController::PasswordAccessoryController(
content::WebContents* web_contents)
: web_contents_(web_contents),
view_(PasswordAccessoryViewInterface::Create(this)) {}
view_(PasswordAccessoryViewInterface::Create(this)),
create_dialog_factory_(
base::BindRepeating(&PasswordGenerationDialogViewInterface::Create)) {
}
// Additional creation functions in unit tests only:
PasswordAccessoryController::PasswordAccessoryController(
content::WebContents* web_contents,
std::unique_ptr<PasswordAccessoryViewInterface> view)
: web_contents_(web_contents), view_(std::move(view)) {}
std::unique_ptr<PasswordAccessoryViewInterface> view,
CreateDialogFactory create_dialog_factory)
: web_contents_(web_contents),
view_(std::move(view)),
create_dialog_factory_(create_dialog_factory) {}
PasswordAccessoryController::~PasswordAccessoryController() = default;
// static
void PasswordAccessoryController::CreateForWebContentsForTesting(
content::WebContents* web_contents,
std::unique_ptr<PasswordAccessoryViewInterface> view) {
std::unique_ptr<PasswordAccessoryViewInterface> view,
CreateDialogFactory create_dialog_factory) {
DCHECK(web_contents) << "Need valid WebContents to attach controller to!";
DCHECK(!FromWebContents(web_contents)) << "Controller already attached!";
web_contents->SetUserData(UserDataKey(),
base::WrapUnique(new PasswordAccessoryController(
web_contents, std::move(view))));
web_contents->SetUserData(
UserDataKey(),
base::WrapUnique(new PasswordAccessoryController(
web_contents, std::move(view), create_dialog_factory)));
}
void PasswordAccessoryController::OnPasswordsAvailable(
......@@ -97,6 +126,27 @@ void PasswordAccessoryController::DidNavigateMainFrame() {
web_contents_->GetMainFrame()->GetLastCommittedOrigin().GetURL());
}
void PasswordAccessoryController::OnAutomaticGenerationStatusChanged(
bool available,
const base::Optional<
autofill::password_generation::PasswordGenerationUIData>& ui_data,
const base::WeakPtr<password_manager::PasswordManagerDriver>& driver) {
target_frame_driver_ = driver;
if (available) {
DCHECK(ui_data.has_value());
generation_element_data_ = std::make_unique<GenerationElementData>(
autofill::CalculateFormSignature(
ui_data.value().password_form.form_data),
autofill::CalculateFieldSignatureByNameAndType(
ui_data.value().generation_element, "password"),
ui_data.value().max_length);
} else {
generation_element_data_.reset();
}
DCHECK(view_);
view_->OnAutomaticGenerationStatusChanged(available);
}
void PasswordAccessoryController::OnFillingTriggered(
bool is_password,
const base::string16& textToFill) const {
......@@ -122,6 +172,37 @@ void PasswordAccessoryController::OnOptionSelected(
}
}
void PasswordAccessoryController::OnGenerationRequested() {
if (!target_frame_driver_)
return;
// TODO(crbug.com/835234): Take the modal dialog logic out of the accessory
// controller.
dialog_view_ = create_dialog_factory_.Run(this);
base::string16 password =
target_frame_driver_->GetPasswordGenerationManager()->GeneratePassword(
web_contents_->GetLastCommittedURL().GetOrigin(),
generation_element_data_->form_signature,
generation_element_data_->field_signature,
generation_element_data_->max_password_length);
dialog_view_->Show(password);
}
void PasswordAccessoryController::GeneratedPasswordAccepted(
const base::string16& password) {
if (!target_frame_driver_)
return;
target_frame_driver_->GeneratedPasswordAccepted(password);
dialog_view_.reset();
}
void PasswordAccessoryController::GeneratedPasswordRejected() {
dialog_view_.reset();
}
gfx::NativeView PasswordAccessoryController::container_view() const {
return web_contents_->GetNativeView();
}
gfx::NativeWindow PasswordAccessoryController::native_window() const {
return web_contents_->GetTopLevelNativeWindow();
}
......@@ -9,17 +9,24 @@
#include <memory>
#include <utility>
#include "base/callback.h"
#include "base/macros.h"
#include "base/strings/string16.h"
#include "components/autofill/core/common/password_generation_util.h"
#include "content/public/browser/web_contents_user_data.h"
#include "ui/gfx/native_widget_types.h"
#include "url/gurl.h"
namespace autofill {
struct PasswordForm;
}
} // namespace autofill
namespace password_manager {
class PasswordManagerDriver;
} // namespace password_manager
class PasswordAccessoryViewInterface;
class PasswordGenerationDialogViewInterface;
// The controller for the view located below the keyboard accessory.
// Upon creation, it creates (and owns) a corresponding PasswordAccessoryView.
......@@ -38,6 +45,8 @@ class PasswordAccessoryViewInterface;
class PasswordAccessoryController
: public content::WebContentsUserData<PasswordAccessoryController> {
public:
using CreateDialogFactory = base::RepeatingCallback<std::unique_ptr<
PasswordGenerationDialogViewInterface>(PasswordAccessoryController*)>;
~PasswordAccessoryController() override;
// Notifies the view about credentials to be displayed.
......@@ -49,6 +58,13 @@ class PasswordAccessoryController
// Send empty suggestions and default options to the view.
void DidNavigateMainFrame();
// Notifies the view that automatic password generation status changed.
void OnAutomaticGenerationStatusChanged(
bool available,
const base::Optional<
autofill::password_generation::PasswordGenerationUIData>& ui_data,
const base::WeakPtr<password_manager::PasswordManagerDriver>& driver);
// Called by the UI code to request that |textToFill| is to be filled into the
// currently focused field.
void OnFillingTriggered(bool is_password,
......@@ -57,14 +73,27 @@ class PasswordAccessoryController
// Called by the UI code because a user triggered the |selectedOption|.
void OnOptionSelected(const base::string16& selectedOption) const;
// Called by the UI code to signal that the user requested password
// generation. This should prompt a modal dialog with the generated password.
void OnGenerationRequested();
// Called from the modal dialog if the user accepted the generated password.
void GeneratedPasswordAccepted(const base::string16& password);
// Called from the modal dialog if the user rejected the generated password.
void GeneratedPasswordRejected();
// The web page view containing the focused field.
gfx::NativeView container_view() const;
gfx::NativeWindow native_window() const;
// Like |CreateForWebContents|, it creates the controller and attaches it to
// the given |web_contents|. Additionally, it allows inject a fake/mock view.
static void CreateForWebContentsForTesting(
content::WebContents* web_contents,
std::unique_ptr<PasswordAccessoryViewInterface> test_view);
std::unique_ptr<PasswordAccessoryViewInterface> test_view,
CreateDialogFactory create_dialog_callback);
#if defined(UNIT_TEST)
// Returns the held view for testing.
......@@ -72,6 +101,10 @@ class PasswordAccessoryController
#endif // defined(UNIT_TEST)
private:
// Data including the form and field for which generation was requested,
// their signatures and the maximum password size.
struct GenerationElementData;
// Required for construction via |CreateForWebContents|:
explicit PasswordAccessoryController(content::WebContents* contents);
friend class content::WebContentsUserData<PasswordAccessoryController>;
......@@ -79,16 +112,29 @@ class PasswordAccessoryController
// Constructor that allows to inject a mock or fake view.
PasswordAccessoryController(
content::WebContents* web_contents,
std::unique_ptr<PasswordAccessoryViewInterface> view);
std::unique_ptr<PasswordAccessoryViewInterface> view,
CreateDialogFactory create_dialog_callback);
// The tab for which this class is scoped.
content::WebContents* web_contents_;
// Data for the generation element used to generate the password.
std::unique_ptr<GenerationElementData> generation_element_data_;
// Password manager driver for the target frame used for password generation.
base::WeakPtr<password_manager::PasswordManagerDriver> target_frame_driver_;
// Modal dialog view meant to display the generated password.
std::unique_ptr<PasswordGenerationDialogViewInterface> dialog_view_;
// Hold the native instance of the view. Must be last declared and initialized
// member so the view can be created in the constructor with a fully set up
// controller instance.
std::unique_ptr<PasswordAccessoryViewInterface> view_;
// Creation callback for the modal dialog view meant to facilitate testing.
CreateDialogFactory create_dialog_factory_;
DISALLOW_COPY_AND_ASSIGN(PasswordAccessoryController);
};
......
......@@ -7,11 +7,19 @@
#include <memory>
#include "base/memory/weak_ptr.h"
#include "base/optional.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/mock_callback.h"
#include "chrome/browser/password_manager/password_accessory_view_interface.h"
#include "chrome/browser/password_manager/password_generation_dialog_view_interface.h"
#include "chrome/grit/generated_resources.h"
#include "chrome/test/base/chrome_render_view_host_test_harness.h"
#include "components/autofill/core/common/password_form.h"
#include "components/autofill/core/common/password_generation_util.h"
#include "components/autofill/core/common/signatures_util.h"
#include "components/password_manager/core/browser/password_generation_manager.h"
#include "components/password_manager/core/browser/stub_password_manager_client.h"
#include "components/password_manager/core/browser/stub_password_manager_driver.h"
#include "components/strings/grit/components_strings.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
......@@ -19,15 +27,17 @@
namespace {
using autofill::PasswordForm;
using autofill::password_generation::PasswordGenerationUIData;
using base::ASCIIToUTF16;
using base::UTF16ToASCII;
using base::UTF16ToWide;
using testing::_;
using testing::ByMove;
using testing::ElementsAre;
using testing::Eq;
using testing::Mock;
using testing::NotNull;
using testing::PrintToString;
using testing::Return;
using testing::StrictMock;
using AccessoryItem = PasswordAccessoryViewInterface::AccessoryItem;
using ItemType = AccessoryItem::Type;
......@@ -47,11 +57,53 @@ class MockPasswordAccessoryView : public PasswordAccessoryViewInterface {
const std::vector<AccessoryItem>& items));
MOCK_METHOD1(OnFillingTriggered, void(const base::string16& textToFill));
MOCK_METHOD0(OnViewDestroyed, void());
MOCK_METHOD1(OnAutomaticGenerationStatusChanged, void(bool));
private:
DISALLOW_COPY_AND_ASSIGN(MockPasswordAccessoryView);
};
class MockPasswordManagerDriver
: public password_manager::StubPasswordManagerDriver {
public:
MockPasswordManagerDriver() = default;
MOCK_METHOD0(GetPasswordGenerationManager,
password_manager::PasswordGenerationManager*());
private:
DISALLOW_COPY_AND_ASSIGN(MockPasswordManagerDriver);
};
class MockPasswordGenerationManager
: public password_manager::PasswordGenerationManager {
public:
MockPasswordGenerationManager(password_manager::PasswordManagerClient* client,
password_manager::PasswordManagerDriver* driver)
: password_manager::PasswordGenerationManager(client, driver) {}
MOCK_METHOD4(GeneratePassword,
base::string16(const GURL&,
autofill::FormSignature,
autofill::FieldSignature,
uint32_t));
private:
DISALLOW_COPY_AND_ASSIGN(MockPasswordGenerationManager);
};
// Mock modal dialog view used to bypass the need of a valid top level window.
class MockPasswordGenerationDialogView
: public PasswordGenerationDialogViewInterface {
public:
MockPasswordGenerationDialogView() = default;
MOCK_METHOD1(Show, void(base::string16&));
private:
DISALLOW_COPY_AND_ASSIGN(MockPasswordGenerationDialogView);
};
// Pretty prints input for the |MatchesItem| matcher.
std::string PrintItem(const base::string16& text,
const base::string16& description,
......@@ -138,6 +190,30 @@ base::string16 manage_passwords_str() {
IDS_PASSWORD_MANAGER_ACCESSORY_ALL_PASSWORDS_LINK);
}
PasswordGenerationUIData GetTestGenerationUIData1() {
PasswordForm form;
form.form_data = autofill::FormData();
form.form_data.action = GURL("http://www.example1.com/accounts/Login");
form.form_data.origin = GURL("http://www.example1.com/accounts/LoginAuth");
PasswordGenerationUIData data;
data.password_form = form;
data.generation_element = ASCIIToUTF16("testelement1");
data.max_length = 10;
return data;
}
PasswordGenerationUIData GetTestGenerationUIData2() {
PasswordForm form;
form.form_data = autofill::FormData();
form.form_data.action = GURL("http://www.example2.com/accounts/Login");
form.form_data.origin = GURL("http://www.example2.com/accounts/LoginAuth");
PasswordGenerationUIData data;
data.password_form = form;
data.generation_element = ASCIIToUTF16("testelement2");
data.max_length = 11;
return data;
}
} // namespace
// Automagically used to pretty-print AccessoryItems. Must be in same namespace.
......@@ -165,7 +241,8 @@ class PasswordAccessoryControllerTest : public ChromeRenderViewHostTestHarness {
NavigateAndCommit(GURL(kExampleSite));
PasswordAccessoryController::CreateForWebContentsForTesting(
web_contents(),
std::make_unique<StrictMock<MockPasswordAccessoryView>>());
std::make_unique<StrictMock<MockPasswordAccessoryView>>(),
mock_dialog_factory_.Get());
NavigateAndCommit(GURL("https://example.com"));
}
......@@ -176,6 +253,15 @@ class PasswordAccessoryControllerTest : public ChromeRenderViewHostTestHarness {
MockPasswordAccessoryView* view() {
return static_cast<MockPasswordAccessoryView*>(controller()->view());
}
const base::MockCallback<PasswordAccessoryController::CreateDialogFactory>&
mock_dialog_factory() {
return mock_dialog_factory_;
}
private:
base::MockCallback<PasswordAccessoryController::CreateDialogFactory>
mock_dialog_factory_;
};
TEST_F(PasswordAccessoryControllerTest, IsNotRecreatedForSameWebContents) {
......@@ -290,3 +376,55 @@ TEST_F(PasswordAccessoryControllerTest, IgnoresCrossOriginCalls) {
controller()->OnPasswordsAvailable({CreateEntry("Ben", "S3cur3").first},
GURL("https://other-domain.com"));
}
TEST_F(PasswordAccessoryControllerTest, RelaysAutomaticGenerationAvailable) {
EXPECT_CALL(*view(), OnAutomaticGenerationStatusChanged(true));
controller()->OnAutomaticGenerationStatusChanged(
true, GetTestGenerationUIData1(), nullptr);
}
TEST_F(PasswordAccessoryControllerTest, RelaysAutmaticGenerationUnavailable) {
EXPECT_CALL(*view(), OnAutomaticGenerationStatusChanged(false));
controller()->OnAutomaticGenerationStatusChanged(false, base::nullopt,
nullptr);
}
// Tests that if AutomaticGenerationStatusChanged(true) is called for different
// password forms, the form and field signatures used for password generation
// are updated.
TEST_F(PasswordAccessoryControllerTest,
UpdatesSignaturesForDifferentGenerationForms) {
MockPasswordManagerDriver mock_driver;
password_manager::StubPasswordManagerClient stub_client;
MockPasswordGenerationManager mock_generation_manager(&stub_client,
&mock_driver);
// Called twice for different forms.
EXPECT_CALL(*view(), OnAutomaticGenerationStatusChanged(true)).Times(2);
controller()->OnAutomaticGenerationStatusChanged(
true, GetTestGenerationUIData1(), (&mock_driver)->AsWeakPtr());
PasswordGenerationUIData new_ui_data = GetTestGenerationUIData2();
controller()->OnAutomaticGenerationStatusChanged(true, new_ui_data,
(&mock_driver)->AsWeakPtr());
autofill::FormSignature form_signature =
autofill::CalculateFormSignature(new_ui_data.password_form.form_data);
autofill::FieldSignature field_signature =
autofill::CalculateFieldSignatureByNameAndType(
new_ui_data.generation_element, "password");
std::unique_ptr<MockPasswordGenerationDialogView> dialog_view =
std::make_unique<MockPasswordGenerationDialogView>();
MockPasswordGenerationDialogView* raw_dialog_view = dialog_view.get();
base::string16 generated_password = ASCIIToUTF16("t3stp@ssw0rd");
EXPECT_CALL(mock_dialog_factory(), Run)
.WillOnce(Return(ByMove(std::move(dialog_view))));
EXPECT_CALL(mock_driver, GetPasswordGenerationManager())
.WillOnce(Return(&mock_generation_manager));
EXPECT_CALL(mock_generation_manager,
GeneratePassword(_, form_signature, field_signature,
uint32_t(new_ui_data.max_length)))
.WillOnce(Return(generated_password));
EXPECT_CALL(*raw_dialog_view, Show(generated_password));
controller()->OnGenerationRequested();
}
......@@ -61,6 +61,8 @@ class PasswordAccessoryViewInterface {
itemType(itemType) {}
};
virtual ~PasswordAccessoryViewInterface() = default;
// Called with items that should replace all existing items in the accessory
// sheet. The |origin| will be used to let the user know to which site the
// passwords belong and the |items| are the labels and actions that allow the
......@@ -68,7 +70,9 @@ class PasswordAccessoryViewInterface {
virtual void OnItemsAvailable(const GURL& origin,
const std::vector<AccessoryItem>& items) = 0;
virtual ~PasswordAccessoryViewInterface() = default;
// Called when the generation action should be offered or rescinded
// in the keyboard accessory.
virtual void OnAutomaticGenerationStatusChanged(bool available) = 0;
private:
friend class PasswordAccessoryController;
......
// 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.
#ifndef CHROME_BROWSER_PASSWORD_MANAGER_PASSWORD_GENERATION_DIALOG_VIEW_INTERFACE_H_
#define CHROME_BROWSER_PASSWORD_MANAGER_PASSWORD_GENERATION_DIALOG_VIEW_INTERFACE_H_
#include "base/strings/string16.h"
class PasswordAccessoryController;
class PasswordGenerationDialogViewInterface {
public:
virtual ~PasswordGenerationDialogViewInterface() = default;
// Called to show the dialog. |password| is the generated password.
virtual void Show(base::string16& password) = 0;
private:
friend class PasswordAccessoryController;
// Factory function used to create a concrete instance of this view.
static std::unique_ptr<PasswordGenerationDialogViewInterface> Create(
PasswordAccessoryController* controller);
};
#endif // CHROME_BROWSER_PASSWORD_MANAGER_PASSWORD_GENERATION_DIALOG_VIEW_INTERFACE_H_
......@@ -547,18 +547,24 @@ bool PasswordGenerationAgent::FocusedNodeHasChanged(
if (!generation_element_.IsNull())
generation_element_.SetShouldRevealPassword(false);
if (node.IsNull() || !node.IsElementNode())
if (node.IsNull() || !node.IsElementNode()) {
AutomaticGenerationStatusChanged(false);
return false;
}
const blink::WebElement web_element = node.ToConst<blink::WebElement>();
if (!web_element.GetDocument().GetFrame())
if (!web_element.GetDocument().GetFrame()) {
AutomaticGenerationStatusChanged(false);
return false;
}
const blink::WebInputElement* element = ToWebInputElement(&web_element);
if (element && element->IsPasswordFieldForAutofill())
last_focused_password_element_ = *element;
if (!element || *element != generation_element_)
if (!element || *element != generation_element_) {
AutomaticGenerationStatusChanged(false);
return false;
}
if (password_is_generated_) {
if (generation_element_.Value().IsEmpty()) {
......@@ -579,6 +585,7 @@ bool PasswordGenerationAgent::FocusedNodeHasChanged(
return true;
}
AutomaticGenerationStatusChanged(false);
return false;
}
......
......@@ -74,10 +74,15 @@ class PasswordGenerationManager {
// password shall be generated.
// |max_length| refers to the maximum allowed length according to the site and
// may be 0 if unset.
base::string16 GeneratePassword(const GURL& last_committed_url,
autofill::FormSignature form_signature,
autofill::FieldSignature field_signature,
uint32_t max_length);
//
// Virtual for testing
//
// TODO(crbug.com/855595): Add a stub for this class to facilitate testing.
virtual base::string16 GeneratePassword(
const GURL& last_committed_url,
autofill::FormSignature form_signature,
autofill::FieldSignature field_signature,
uint32_t max_length);
private:
friend class PasswordGenerationManagerTest;
......
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