[Messages] Password edit dialog implementation

This CL adds implementation of password edit dialog. The dialog will be
invoked from native code of ChromePasswordManagerClient. The UX specs
for the dialog are at https://docs.google.com/presentation/d/1g1kh48QQI83Smn6Nyq0AaRJ8ZVcURagRgIrZh02dF00/edit?ts=6063bb53#slide=id.gc8f2c5d1db_0_16

BUG=1192328
[email protected],[email protected]

Change-Id: If736f7a85c325a5ac975e814b02f574ca6a712b3
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2836833
Reviewed-by: Filip Gorski <[email protected]>
Reviewed-by: Ioana Pandele <[email protected]>
Reviewed-by: Lijin Shen <[email protected]>
Commit-Queue: Pavel Yatsuk <[email protected]>
Cr-Commit-Position: refs/heads/master@{#877463}
diff --git a/chrome/android/BUILD.gn b/chrome/android/BUILD.gn
index d7ffb02..fbe608a 100644
--- a/chrome/android/BUILD.gn
+++ b/chrome/android/BUILD.gn
@@ -694,6 +694,7 @@
     "//chrome/browser/download/internal/android:java",
     "//chrome/browser/page_annotations/android:java",
     "//chrome/browser/password_check:internal_java",
+    "//chrome/browser/password_edit_dialog/android:java",
     "//chrome/browser/password_entry_edit/android/internal:java",
     "//chrome/browser/tabmodel/internal:java",
     "//chrome/browser/touch_to_fill/android/internal:java",
@@ -898,6 +899,7 @@
     "//chrome/browser/omaha/android:java",
     "//chrome/browser/optimization_guide/android:java",
     "//chrome/browser/page_annotations/test/android:junit",
+    "//chrome/browser/password_edit_dialog/android:junit",
     "//chrome/browser/password_entry_edit/android/internal:junit",
     "//chrome/browser/payments/android:junit",
     "//chrome/browser/performance_hints/android:java",
@@ -1193,6 +1195,7 @@
     "//chrome/browser/paint_preview/android:java",
     "//chrome/browser/paint_preview/android:javatests",
     "//chrome/browser/password_check:public_java",
+    "//chrome/browser/password_edit_dialog/android:javatests",
     "//chrome/browser/password_entry_edit/android/internal:javatests",
     "//chrome/browser/password_manager/android:java",
     "//chrome/browser/password_manager/android_test_helpers:test_support_java",
diff --git a/chrome/app/generated_resources.grd b/chrome/app/generated_resources.grd
index c2e27fc4..a816fdf 100644
--- a/chrome/app/generated_resources.grd
+++ b/chrome/app/generated_resources.grd
@@ -6223,10 +6223,10 @@
           No thanks
         </message>
       </if>
-      <message name="IDS_PASSWORD_MANAGER_USERNAME_LABEL" desc="Label for the username field in a prompt to store new credential">
+      <message name="IDS_PASSWORD_MANAGER_USERNAME_LABEL" desc="Label for the username field in a prompt to store new credential" formatter_data="android_java">
         Username
       </message>
-      <message name="IDS_PASSWORD_MANAGER_PASSWORD_LABEL" desc="Label for the password field in a prompt to store new credential">
+      <message name="IDS_PASSWORD_MANAGER_PASSWORD_LABEL" desc="Label for the password field in a prompt to store new credential" formatter_data="android_java">
         Password
       </message>
       <if expr="not is_android">
@@ -6334,7 +6334,7 @@
         <message name="IDS_PASSWORD_MANAGER_BLOCKLIST_BUTTON" desc="Mobile: Button text for the 'Save Password' infobar's 'Never remember for this site' option">
           Never
         </message>
-        <message name="IDS_PASSWORD_MANAGER_UPDATE_BUTTON" desc="Label for the 'update' button in the Update Password infobar. This infobar asks if the user wishes to update the saved password for a site to a new password the user has just entered; the button applies the suggested update.">
+        <message name="IDS_PASSWORD_MANAGER_UPDATE_BUTTON" desc="Label for the 'update' button in the Update Password infobar. This infobar asks if the user wishes to update the saved password for a site to a new password the user has just entered; the button applies the suggested update." formatter_data="android_java">
           Update
         </message>
         <message name="IDS_PASSWORD_SAVING_STATUS_TOGGLE" desc="Label for the toggle that shows whether saving passwords for the current site is enabled or disabled.">
@@ -6361,10 +6361,10 @@
         <message name="IDS_PASSWORD_MANAGER_ACCESSORY_GENERATE_PASSWORD_BUTTON_TITLE" desc="Text for the button used to generate a password.">
           Suggest strong password
         </message>
-        <message name="IDS_UPDATE_PASSWORD_DIALOG_TITLE" desc="The title of the update password dialog.">
+        <message name="IDS_UPDATE_PASSWORD_DIALOG_TITLE" desc="The title of the update password dialog." formatter_data="android_java">
           Update password for <ph name="ORIGIN">%1$s<ex>example.com</ex></ph>
         </message>
-        <message name="IDS_UPDATE_PASSWORD_DIALOG_SIGNED_IN_DESCRIPTION" desc="The description at the bottom of update password dialog when the user is signed in.">
+        <message name="IDS_UPDATE_PASSWORD_DIALOG_SIGNED_IN_DESCRIPTION" desc="The description at the bottom of update password dialog when the user is signed in." formatter_data="android_java">
           Passwords are saved in your Google Account (<ph name="ACCOUNT">%1$s<ex>[email protected]</ex></ph>) so you can use them on any device
         </message>
       </if>
diff --git a/chrome/browser/BUILD.gn b/chrome/browser/BUILD.gn
index 3d608e8c..83fd706 100644
--- a/chrome/browser/BUILD.gn
+++ b/chrome/browser/BUILD.gn
@@ -3235,6 +3235,7 @@
       "//chrome/browser/optimization_guide/android:jni_headers",
       "//chrome/browser/password_check/android:jni_headers",
       "//chrome/browser/password_check/android:password_check_enums_srcjar",
+      "//chrome/browser/password_edit_dialog/android",
       "//chrome/browser/password_entry_edit/android",
       "//chrome/browser/password_manager/android:jni_headers",
       "//chrome/browser/payments/android:jni_headers",
diff --git a/chrome/browser/password_edit_dialog/DIR_METADATA b/chrome/browser/password_edit_dialog/DIR_METADATA
new file mode 100644
index 0000000..b420232
--- /dev/null
+++ b/chrome/browser/password_edit_dialog/DIR_METADATA
@@ -0,0 +1,4 @@
+monorail: {
+  component: "UI>Browser>Passwords"
+}
+team_email: "[email protected]"
diff --git a/chrome/browser/password_edit_dialog/OWNERS b/chrome/browser/password_edit_dialog/OWNERS
new file mode 100644
index 0000000..6d65cfd
--- /dev/null
+++ b/chrome/browser/password_edit_dialog/OWNERS
@@ -0,0 +1,3 @@
[email protected]
[email protected]
[email protected]
diff --git a/chrome/browser/password_edit_dialog/android/BUILD.gn b/chrome/browser/password_edit_dialog/android/BUILD.gn
new file mode 100644
index 0000000..6d884e2
--- /dev/null
+++ b/chrome/browser/password_edit_dialog/android/BUILD.gn
@@ -0,0 +1,89 @@
+# Copyright 2021 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.
+
+import("//build/config/android/rules.gni")
+
+source_set("android") {
+  sources = [
+    "password_edit_dialog_bridge.cc",
+    "password_edit_dialog_bridge.h",
+  ]
+
+  deps = [
+    ":jni_headers",
+    "//base",
+    "//chrome/app:generated_resources",
+    "//content/public/browser",
+    "//ui/android:android",
+  ]
+}
+
+android_library("java") {
+  sources = [
+    "java/src/org/chromium/chrome/browser/password_edit_dialog/PasswordEditDialogBridge.java",
+    "java/src/org/chromium/chrome/browser/password_edit_dialog/PasswordEditDialogCoordinator.java",
+    "java/src/org/chromium/chrome/browser/password_edit_dialog/PasswordEditDialogProperties.java",
+    "java/src/org/chromium/chrome/browser/password_edit_dialog/PasswordEditDialogView.java",
+  ]
+
+  deps = [
+    ":java_resources",
+    "//base:base_java",
+    "//third_party/androidx:androidx_annotation_annotation_java",
+    "//ui/android:ui_no_recycler_view_java",
+  ]
+
+  annotation_processor_deps = [ "//base/android/jni_generator:jni_processor" ]
+  resources_package = "org.chromium.chrome.browser.password_edit_dialog"
+}
+
+generate_jni("jni_headers") {
+  sources = [ "java/src/org/chromium/chrome/browser/password_edit_dialog/PasswordEditDialogBridge.java" ]
+}
+
+android_resources("java_resources") {
+  sources = [ "java/res/layout/password_edit_dialog.xml" ]
+
+  deps = [
+    "//chrome/app:java_strings_grd",
+    "//chrome/browser/ui/android/strings:ui_strings_grd",
+  ]
+}
+
+java_library("junit") {
+  # Skip platform checks since Robolectric depends on requires_android targets.
+  bypass_platform_checks = true
+  testonly = true
+  sources = [ "java/src/org/chromium/chrome/browser/password_edit_dialog/PasswordEditDialogTest.java" ]
+
+  deps = [
+    ":java",
+    "//base:base_java",
+    "//base:base_java_test_support",
+    "//base:base_junit_test_support",
+    "//third_party/android_deps:robolectric_all_java",
+    "//third_party/hamcrest:hamcrest_library_java",
+    "//third_party/junit",
+    "//third_party/mockito:mockito_java",
+    "//ui/android:ui_no_recycler_view_java",
+  ]
+}
+
+android_library("javatests") {
+  testonly = true
+
+  sources = [ "java/src/org/chromium/chrome/browser/password_edit_dialog/PasswordEditDialogViewTest.java" ]
+
+  deps = [
+    ":java",
+    "//base:base_java_test_support",
+    "//chrome/test/android:chrome_java_test_support",
+    "//content/public/test/android:content_java_test_support",
+    "//third_party/androidx:androidx_test_runner_java",
+    "//third_party/hamcrest:hamcrest_library_java",
+    "//third_party/junit",
+    "//ui/android:ui_java",
+    "//ui/android:ui_java_test_support",
+  ]
+}
diff --git a/chrome/browser/password_edit_dialog/android/java/res/layout/password_edit_dialog.xml b/chrome/browser/password_edit_dialog/android/java/res/layout/password_edit_dialog.xml
new file mode 100644
index 0000000..23c0619
--- /dev/null
+++ b/chrome/browser/password_edit_dialog/android/java/res/layout/password_edit_dialog.xml
@@ -0,0 +1,60 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright 2021 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. -->
+
+<org.chromium.chrome.browser.password_edit_dialog.PasswordEditDialogView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:orientation="vertical"
+    style="@style/AlertDialogContent">
+
+    <!-- Usernames -->
+    <TextView
+        android:labelFor="@+id/usernames_spinner"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:textAppearance="@style/TextAppearance.TextSmall.Secondary"
+        android:text="@string/password_manager_username_label" />
+
+    <androidx.appcompat.widget.AppCompatSpinner
+        android:id="@+id/usernames_spinner"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:focusable="true"
+        android:focusableInTouchMode="true"/>
+
+    <View style="@style/PreferenceSpinnerUnderlineView"/>
+
+    <!-- Password -->
+    <com.google.android.material.textfield.TextInputLayout
+        android:labelFor="@+id/password"
+        android:layout_height="wrap_content"
+        android:layout_width="match_parent"
+        android:hint="@string/password_manager_password_label"
+        app:endIconMode="password_toggle"
+        app:hintTextAppearance="@style/TextAppearance.TextSmall.Secondary">
+
+        <EditText
+            tools:ignore="LabelFor"
+            android:id="@+id/password"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:inputType="textPassword"
+            android:imeOptions="flagNoExtractUi"
+            android:enabled="false"
+            android:focusable="false"
+            android:importantForAutofill="noExcludeDescendants"
+            android:textAppearance="@style/TextAppearance.TextLarge.Primary"/>
+    </com.google.android.material.textfield.TextInputLayout>
+
+    <TextView
+        android:id="@+id/footer"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:visibility="gone"
+        android:textAppearance="@style/TextAppearance.TextSmall.Secondary"/>
+</org.chromium.chrome.browser.password_edit_dialog.PasswordEditDialogView>
diff --git a/chrome/browser/password_edit_dialog/android/java/src/org/chromium/chrome/browser/password_edit_dialog/PasswordEditDialogBridge.java b/chrome/browser/password_edit_dialog/android/java/src/org/chromium/chrome/browser/password_edit_dialog/PasswordEditDialogBridge.java
new file mode 100644
index 0000000..16d74eb
--- /dev/null
+++ b/chrome/browser/password_edit_dialog/android/java/src/org/chromium/chrome/browser/password_edit_dialog/PasswordEditDialogBridge.java
@@ -0,0 +1,62 @@
+// Copyright 2021 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_edit_dialog;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import org.chromium.base.annotations.CalledByNative;
+import org.chromium.base.annotations.NativeMethods;
+import org.chromium.ui.base.WindowAndroid;
+
+/**
+ * Java part of PasswordEditBridge pair providing communication between native password manager code
+ * and Java password edit dialog UI components.
+ */
+public class PasswordEditDialogBridge implements PasswordEditDialogCoordinator.Delegate {
+    private long mNativeDialog;
+    private final PasswordEditDialogCoordinator mDialogCoordinator;
+
+    @CalledByNative
+    static PasswordEditDialogBridge create(
+            long nativeDialog, @NonNull WindowAndroid windowAndroid) {
+        return new PasswordEditDialogBridge(nativeDialog, windowAndroid);
+    }
+
+    private PasswordEditDialogBridge(long nativeDialog, @NonNull WindowAndroid windowAndroid) {
+        mNativeDialog = nativeDialog;
+        mDialogCoordinator = PasswordEditDialogCoordinator.create(windowAndroid, this);
+    }
+
+    @CalledByNative
+    void show(@NonNull String[] usernames, int selectedUsernameIndex, @NonNull String password,
+            @NonNull String origin, @Nullable String account) {
+        mDialogCoordinator.show(usernames, selectedUsernameIndex, password, origin, account);
+    }
+
+    @CalledByNative
+    void dismiss() {
+        mDialogCoordinator.dismiss();
+    }
+
+    @Override
+    public void onDialogAccepted(int selectedUsernameIndex) {
+        assert mNativeDialog != 0;
+        PasswordEditDialogBridgeJni.get().onDialogAccepted(mNativeDialog, selectedUsernameIndex);
+    }
+
+    @Override
+    public void onDialogDismissed() {
+        assert mNativeDialog != 0;
+        PasswordEditDialogBridgeJni.get().onDialogDismissed(mNativeDialog);
+        mNativeDialog = 0;
+    }
+
+    @NativeMethods
+    interface Natives {
+        void onDialogAccepted(long nativePasswordEditDialogBridge, int selectedUsernameIndex);
+        void onDialogDismissed(long nativePasswordEditDialogBridge);
+    }
+}
\ No newline at end of file
diff --git a/chrome/browser/password_edit_dialog/android/java/src/org/chromium/chrome/browser/password_edit_dialog/PasswordEditDialogCoordinator.java b/chrome/browser/password_edit_dialog/android/java/src/org/chromium/chrome/browser/password_edit_dialog/PasswordEditDialogCoordinator.java
new file mode 100644
index 0000000..aeea149c
--- /dev/null
+++ b/chrome/browser/password_edit_dialog/android/java/src/org/chromium/chrome/browser/password_edit_dialog/PasswordEditDialogCoordinator.java
@@ -0,0 +1,170 @@
+// Copyright 2021 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_edit_dialog;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+
+import org.chromium.ui.base.WindowAndroid;
+import org.chromium.ui.modaldialog.DialogDismissalCause;
+import org.chromium.ui.modaldialog.ModalDialogManager;
+import org.chromium.ui.modaldialog.ModalDialogProperties;
+import org.chromium.ui.modaldialog.ModalDialogProperties.ButtonType;
+import org.chromium.ui.modelutil.PropertyModel;
+import org.chromium.ui.modelutil.PropertyModelChangeProcessor;
+
+import java.util.Arrays;
+
+/** Coordinator for password edit dialog. */
+class PasswordEditDialogCoordinator implements ModalDialogProperties.Controller {
+    /**
+     * A delegate interface for PasswordEditDialogBridge to receive the results of password edit
+     * dialog interactions.
+     */
+    interface Delegate {
+        /**
+         * Called when the user taps the dialog positive button.
+         *
+         * @param selectedUsernameIndex The index of the username selected by the user.
+         */
+        void onDialogAccepted(int selectedUsernameIndex);
+
+        /** Called when the dialog is dismissed. */
+        void onDialogDismissed();
+    }
+
+    private final Context mContext;
+    private final ModalDialogManager mModalDialogManager;
+    private final PasswordEditDialogView mDialogView;
+    private final Delegate mDelegate;
+
+    private PropertyModel mDialogModel;
+    private PropertyModel mDialogViewModel;
+
+    /**
+     * Creates the {@link PasswordEditDialogCoordinator}.
+     *
+     * @param windowAndroid The window where the dialog will be displayed.
+     * @param delegate The delegate to be called with results of interaction.
+     */
+    static PasswordEditDialogCoordinator create(
+            @NonNull WindowAndroid windowAndroid, @NonNull Delegate delegate) {
+        Context context = windowAndroid.getContext().get();
+        PasswordEditDialogView dialogView =
+                (PasswordEditDialogView) LayoutInflater.from(context).inflate(
+                        R.layout.password_edit_dialog, null);
+        return new PasswordEditDialogCoordinator(
+                context, windowAndroid.getModalDialogManager(), dialogView, delegate);
+    }
+
+    /**
+     * Internal constructor for {@link PasswordEditDialogCoordinator}. Used by tests to inject
+     * parameters. External code should use PasswordEditDialogCoordinator#create.
+     *
+     * @param context The context for accessing resources.
+     * @param modalDialogManager The ModalDialogManager to display the dialog.
+     * @param dialogView The custom view with dialog content.
+     * @param delegate The delegate to be called with results of interaction.
+     */
+    @VisibleForTesting
+    PasswordEditDialogCoordinator(@NonNull Context context,
+            @NonNull ModalDialogManager modalDialogManager,
+            @NonNull PasswordEditDialogView dialogView, @NonNull Delegate delegate) {
+        mContext = context;
+        mModalDialogManager = modalDialogManager;
+        mDialogView = dialogView;
+        mDelegate = delegate;
+    }
+
+    /**
+     * Shows the password edit dialog.
+     *
+     * @param usernames The list of usernames that will be presented in the Spinner.
+     * @param selectedUsernameIndex The index in the usernames list of the user that should be
+     *         selected initially.
+     * @param password The password.
+     * @param origin The origin with which these credentials are associated.
+     * @param account The account name where the password will be saved. When the user is not signed
+     *         in the account is null.
+     */
+    void show(@NonNull String[] usernames, int selectedUsernameIndex, @NonNull String password,
+            @NonNull String origin, @Nullable String account) {
+        Resources resources = mContext.getResources();
+
+        PropertyModel.Builder dialogProperties =
+                new PropertyModel.Builder(PasswordEditDialogProperties.ALL_KEYS)
+                        .with(PasswordEditDialogProperties.USERNAMES, Arrays.asList(usernames))
+                        .with(PasswordEditDialogProperties.SELECTED_USERNAME_INDEX,
+                                selectedUsernameIndex)
+                        .with(PasswordEditDialogProperties.PASSWORD, password)
+                        .with(PasswordEditDialogProperties.USERNAME_SELECTED_CALLBACK,
+                                this::handleUsernameSelected);
+        if (!TextUtils.isEmpty(account)) {
+            dialogProperties.with(PasswordEditDialogProperties.FOOTER,
+                    resources.getString(
+                            R.string.update_password_dialog_signed_in_description, account));
+        }
+        mDialogViewModel = dialogProperties.build();
+        PropertyModelChangeProcessor.create(
+                mDialogViewModel, mDialogView, PasswordEditDialogView::bind);
+
+        mDialogModel =
+                new PropertyModel.Builder(ModalDialogProperties.ALL_KEYS)
+                        .with(ModalDialogProperties.CONTROLLER, this)
+                        .with(ModalDialogProperties.TITLE,
+                                resources.getString(R.string.update_password_dialog_title, origin))
+                        .with(ModalDialogProperties.POSITIVE_BUTTON_TEXT, resources,
+                                R.string.password_manager_update_button)
+                        .with(ModalDialogProperties.NEGATIVE_BUTTON_TEXT, resources,
+                                R.string.password_generation_dialog_cancel_button)
+                        .with(ModalDialogProperties.PRIMARY_BUTTON_FILLED, true)
+                        .with(ModalDialogProperties.CUSTOM_VIEW, mDialogView)
+                        .build();
+        mModalDialogManager.showDialog(mDialogModel, ModalDialogManager.ModalDialogType.TAB);
+    }
+
+    /** Dismisses the displayed dialog. */
+    void dismiss() {
+        mModalDialogManager.dismissDialog(mDialogModel, DialogDismissalCause.DISMISSED_BY_NATIVE);
+    }
+
+    private void handleUsernameSelected(int selectedUsernameIndex) {
+        mDialogViewModel.set(
+                PasswordEditDialogProperties.SELECTED_USERNAME_INDEX, selectedUsernameIndex);
+    }
+
+    // ModalDialogProperties.Controller implementation.
+    @Override
+    public void onClick(PropertyModel model, @ButtonType int buttonType) {
+        if (buttonType == ButtonType.POSITIVE) {
+            mDelegate.onDialogAccepted(
+                    mDialogViewModel.get(PasswordEditDialogProperties.SELECTED_USERNAME_INDEX));
+        }
+        mModalDialogManager.dismissDialog(model,
+                buttonType == ButtonType.POSITIVE ? DialogDismissalCause.POSITIVE_BUTTON_CLICKED
+                                                  : DialogDismissalCause.NEGATIVE_BUTTON_CLICKED);
+    }
+
+    @Override
+    public void onDismiss(PropertyModel model, @DialogDismissalCause int dismissalCause) {
+        mDelegate.onDialogDismissed();
+    }
+
+    @VisibleForTesting
+    PropertyModel getDialogModelForTesting() {
+        return mDialogModel;
+    }
+
+    @VisibleForTesting
+    PropertyModel getDialogViewModelForTesting() {
+        return mDialogViewModel;
+    }
+}
\ No newline at end of file
diff --git a/chrome/browser/password_edit_dialog/android/java/src/org/chromium/chrome/browser/password_edit_dialog/PasswordEditDialogProperties.java b/chrome/browser/password_edit_dialog/android/java/src/org/chromium/chrome/browser/password_edit_dialog/PasswordEditDialogProperties.java
new file mode 100644
index 0000000..8c1855a
--- /dev/null
+++ b/chrome/browser/password_edit_dialog/android/java/src/org/chromium/chrome/browser/password_edit_dialog/PasswordEditDialogProperties.java
@@ -0,0 +1,39 @@
+// Copyright 2021 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_edit_dialog;
+
+import org.chromium.base.Callback;
+import org.chromium.ui.modelutil.PropertyKey;
+import org.chromium.ui.modelutil.PropertyModel;
+
+import java.util.List;
+
+/**
+ * Defines properties for password edit dialog custom view.
+ */
+class PasswordEditDialogProperties {
+    /**
+     * The callback, invoked when the user selects a username. The value is 0 based index of
+     * selected username.
+     */
+    static final PropertyModel
+            .ReadableObjectPropertyKey<Callback<Integer>> USERNAME_SELECTED_CALLBACK =
+            new PropertyModel.ReadableObjectPropertyKey<>();
+
+    static final PropertyModel.ReadableObjectPropertyKey<List<String>> USERNAMES =
+            new PropertyModel.ReadableObjectPropertyKey<>();
+
+    static final PropertyModel.WritableIntPropertyKey SELECTED_USERNAME_INDEX =
+            new PropertyModel.WritableIntPropertyKey();
+
+    static final PropertyModel.ReadableObjectPropertyKey<String> PASSWORD =
+            new PropertyModel.ReadableObjectPropertyKey<>();
+
+    static final PropertyModel.ReadableObjectPropertyKey<String> FOOTER =
+            new PropertyModel.ReadableObjectPropertyKey<>();
+
+    static final PropertyKey[] ALL_KEYS = {
+            USERNAME_SELECTED_CALLBACK, USERNAMES, SELECTED_USERNAME_INDEX, PASSWORD, FOOTER};
+}
\ No newline at end of file
diff --git a/chrome/browser/password_edit_dialog/android/java/src/org/chromium/chrome/browser/password_edit_dialog/PasswordEditDialogTest.java b/chrome/browser/password_edit_dialog/android/java/src/org/chromium/chrome/browser/password_edit_dialog/PasswordEditDialogTest.java
new file mode 100644
index 0000000..5c88729
--- /dev/null
+++ b/chrome/browser/password_edit_dialog/android/java/src/org/chromium/chrome/browser/password_edit_dialog/PasswordEditDialogTest.java
@@ -0,0 +1,148 @@
+// Copyright 2021 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_edit_dialog;
+
+import static org.hamcrest.Matchers.contains;
+import static org.hamcrest.Matchers.containsString;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.never;
+
+import org.junit.Assert;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+import org.robolectric.RuntimeEnvironment;
+
+import org.chromium.base.Callback;
+import org.chromium.base.test.BaseRobolectricTestRunner;
+import org.chromium.ui.modaldialog.DialogDismissalCause;
+import org.chromium.ui.modaldialog.ModalDialogManager;
+import org.chromium.ui.modaldialog.ModalDialogProperties;
+import org.chromium.ui.modelutil.PropertyModel;
+
+/** Tests for password edit dialog. */
+@RunWith(BaseRobolectricTestRunner.class)
+public class PasswordEditDialogTest {
+    private static final long NATIVE_PTR = 1;
+    private static final String[] USERNAMES = {"user1", "user2", "user3"};
+    private static final int INITIAL_USERNAME_INDEX = 1;
+    private static final int SELECTED_USERNAME_INDEX = 2;
+    private static final String PASSWORD = "password";
+    private static final String ORIGIN = "example.com";
+    private static final String ACCOUNT_NAME = "[email protected]";
+
+    @Rule
+    public MockitoRule mMockitoRule = MockitoJUnit.rule();
+
+    @Mock
+    private PasswordEditDialogCoordinator.Delegate mDelegateMock;
+
+    @Mock
+    private ModalDialogManager mModalDialogManagerMock;
+
+    @Mock
+    private PasswordEditDialogView mDialogViewMock;
+
+    private PropertyModel mDialogProperties;
+    private PropertyModel mModalDialogModel;
+
+    private PasswordEditDialogCoordinator mDialogCoordinator;
+
+    /**
+     * Tests that properties of modal dialog and custom view are set correctly based on passed
+     * parameters.
+     */
+    @Test
+    public void testDialogProperties() {
+        createAndShowDialog(true);
+        Mockito.verify(mModalDialogManagerMock)
+                .showDialog(mModalDialogModel, ModalDialogManager.ModalDialogType.TAB);
+        Assert.assertThat(
+                mModalDialogModel.get(ModalDialogProperties.TITLE), containsString(ORIGIN));
+        Assert.assertThat("Usernames don't match",
+                mDialogProperties.get(PasswordEditDialogProperties.USERNAMES), contains(USERNAMES));
+        Assert.assertEquals("Selected username doesn't match", INITIAL_USERNAME_INDEX,
+                mDialogProperties.get(PasswordEditDialogProperties.SELECTED_USERNAME_INDEX));
+        Assert.assertEquals("Password doesn't match", PASSWORD,
+                mDialogProperties.get(PasswordEditDialogProperties.PASSWORD));
+        // Non-empty account name should cause footer to be displayed.
+        Assert.assertNotNull(
+                "Footer is empty", mDialogProperties.get(PasswordEditDialogProperties.FOOTER));
+    }
+
+    /** Tests that the footer is not displayed for signed out user. */
+    @Test
+    public void testFooterForSignedOutUser() {
+        createAndShowDialog(false);
+        // Null account name passed to show() indicates that the user is not signed-in. Footer
+        // shouldn't displayed in this case.
+        Assert.assertNull(
+                "Footer is not empty", mDialogProperties.get(PasswordEditDialogProperties.FOOTER));
+    }
+
+    /** Tests that the username selected in spinner gets reflected in the callback patameter. */
+    @Test
+    public void testUserSelection() {
+        createAndShowDialog(true);
+        Callback<Integer> usernameSelectedCallback =
+                mDialogProperties.get(PasswordEditDialogProperties.USERNAME_SELECTED_CALLBACK);
+        usernameSelectedCallback.onResult(SELECTED_USERNAME_INDEX);
+        Assert.assertEquals("Selected username doesn't match", SELECTED_USERNAME_INDEX,
+                mDialogProperties.get(PasswordEditDialogProperties.SELECTED_USERNAME_INDEX));
+        ModalDialogProperties.Controller dialogController =
+                mModalDialogModel.get(ModalDialogProperties.CONTROLLER);
+        dialogController.onClick(mModalDialogModel, ModalDialogProperties.ButtonType.POSITIVE);
+        Mockito.verify(mDelegateMock).onDialogAccepted(SELECTED_USERNAME_INDEX);
+        Mockito.verify(mModalDialogManagerMock)
+                .dismissDialog(mModalDialogModel, DialogDismissalCause.POSITIVE_BUTTON_CLICKED);
+    }
+
+    /**
+     * Tests that the username is not saved and the dialog is dismissed when dismiss() is called
+     * from native code.
+     */
+    @Test
+    public void testDialogDismissedFromNative() {
+        createAndShowDialog(true);
+        mDialogCoordinator.dismiss();
+        Mockito.verify(mDelegateMock, never()).onDialogAccepted(anyInt());
+        Mockito.verify(mModalDialogManagerMock)
+                .dismissDialog(mModalDialogModel, DialogDismissalCause.DISMISSED_BY_NATIVE);
+    }
+
+    /**
+     * Tests that the username is not saved and the dialog is dismissed when the user taps on the
+     * negative button.
+     */
+    @Test
+    public void testDialogDismissedWithNegativeButton() {
+        createAndShowDialog(true);
+        ModalDialogProperties.Controller dialogController =
+                mModalDialogModel.get(ModalDialogProperties.CONTROLLER);
+        dialogController.onClick(mModalDialogModel, ModalDialogProperties.ButtonType.NEGATIVE);
+        Mockito.verify(mDelegateMock, never()).onDialogAccepted(anyInt());
+        Mockito.verify(mModalDialogManagerMock)
+                .dismissDialog(mModalDialogModel, DialogDismissalCause.NEGATIVE_BUTTON_CLICKED);
+    }
+
+    /**
+     * Helper function that creates PasswordEditDialogCoordinator, calls show and captures property
+     * models for modal dialog and custom dialog view.
+     *
+     * @param signedIn Simulates user's sign-in state.
+     */
+    private void createAndShowDialog(boolean signedIn) {
+        mDialogCoordinator = new PasswordEditDialogCoordinator(RuntimeEnvironment.application,
+                mModalDialogManagerMock, mDialogViewMock, mDelegateMock);
+        mDialogCoordinator.show(USERNAMES, INITIAL_USERNAME_INDEX, PASSWORD, ORIGIN,
+                signedIn ? ACCOUNT_NAME : null);
+        mModalDialogModel = mDialogCoordinator.getDialogModelForTesting();
+        mDialogProperties = mDialogCoordinator.getDialogViewModelForTesting();
+    }
+}
diff --git a/chrome/browser/password_edit_dialog/android/java/src/org/chromium/chrome/browser/password_edit_dialog/PasswordEditDialogView.java b/chrome/browser/password_edit_dialog/android/java/src/org/chromium/chrome/browser/password_edit_dialog/PasswordEditDialogView.java
new file mode 100644
index 0000000..92350a4
--- /dev/null
+++ b/chrome/browser/password_edit_dialog/android/java/src/org/chromium/chrome/browser/password_edit_dialog/PasswordEditDialogView.java
@@ -0,0 +1,106 @@
+// Copyright 2021 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_edit_dialog;
+
+import android.content.Context;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemSelectedListener;
+import android.widget.ArrayAdapter;
+import android.widget.LinearLayout;
+import android.widget.Spinner;
+import android.widget.TextView;
+
+import org.chromium.base.Callback;
+import org.chromium.ui.modelutil.PropertyKey;
+import org.chromium.ui.modelutil.PropertyModel;
+
+import java.util.List;
+
+/**
+ * The custom view for password edit modal dialog.
+ */
+public class PasswordEditDialogView extends LinearLayout implements OnItemSelectedListener {
+    private Spinner mUsernamesSpinner;
+    private TextView mPasswordView;
+    private TextView mFooterView;
+    private Callback<Integer> mUsernameSelectedCallback;
+
+    /**
+     * The view binder method to propagate parameters from model to view
+     */
+    public static void bind(
+            PropertyModel model, PasswordEditDialogView dialogView, PropertyKey propertyKey) {
+        if (propertyKey == PasswordEditDialogProperties.USERNAMES) {
+            // Propagation of USERNAMES property triggers passing both USERNAMES and
+            // SELECTED_USERNAME_INDEX properties to the view. This is safe because both properties
+            // are set through property model builder and available by the time the property model
+            // is bound to the view. The SELECTED_USERNAME_INDEX property is writable since it
+            // maintains username index of the user, currently selected in UI. Updating the property
+            // by itself doesn't get propagated to the view as the value originates in the view and
+            // gets routed to coordinator through USERNAME_SELECTED_CALLBACK.
+            dialogView.setUsernames(model.get(PasswordEditDialogProperties.USERNAMES),
+                    model.get(PasswordEditDialogProperties.SELECTED_USERNAME_INDEX));
+        } else if (propertyKey == PasswordEditDialogProperties.PASSWORD) {
+            dialogView.setPassword(model.get(PasswordEditDialogProperties.PASSWORD));
+        } else if (propertyKey == PasswordEditDialogProperties.FOOTER) {
+            dialogView.setFooter(model.get(PasswordEditDialogProperties.FOOTER));
+        } else if (propertyKey == PasswordEditDialogProperties.USERNAME_SELECTED_CALLBACK) {
+            dialogView.setUsernameSelectedCallback(
+                    model.get(PasswordEditDialogProperties.USERNAME_SELECTED_CALLBACK));
+        }
+    }
+
+    public PasswordEditDialogView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    /**
+     * Stores references to the dialog fields after dialog inflation.
+     */
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+        mUsernamesSpinner = findViewById(R.id.usernames_spinner);
+        mUsernamesSpinner.setOnItemSelectedListener(this);
+
+        mPasswordView = findViewById(R.id.password);
+        mFooterView = findViewById(R.id.footer);
+    }
+
+    void setUsernames(List<String> usernames, int selectedUsernameIndex) {
+        ArrayAdapter<String> usernamesAdapter =
+                new ArrayAdapter<>(getContext(), android.R.layout.simple_spinner_item);
+        usernamesAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+        usernamesAdapter.addAll(usernames);
+        mUsernamesSpinner.setAdapter(usernamesAdapter);
+        mUsernamesSpinner.setSelection(selectedUsernameIndex);
+    }
+
+    void setPassword(String password) {
+        mPasswordView.setText(password);
+    }
+
+    void setFooter(String footer) {
+        mFooterView.setVisibility(!TextUtils.isEmpty(footer) ? View.VISIBLE : View.GONE);
+        mFooterView.setText(footer);
+    }
+
+    void setUsernameSelectedCallback(Callback<Integer> callback) {
+        mUsernameSelectedCallback = callback;
+    }
+
+    @Override
+    public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
+        if (mUsernameSelectedCallback != null) {
+            mUsernameSelectedCallback.onResult(position);
+        }
+    }
+
+    @Override
+    public void onNothingSelected(AdapterView<?> parent) {}
+}
diff --git a/chrome/browser/password_edit_dialog/android/java/src/org/chromium/chrome/browser/password_edit_dialog/PasswordEditDialogViewTest.java b/chrome/browser/password_edit_dialog/android/java/src/org/chromium/chrome/browser/password_edit_dialog/PasswordEditDialogViewTest.java
new file mode 100644
index 0000000..c8079db6
--- /dev/null
+++ b/chrome/browser/password_edit_dialog/android/java/src/org/chromium/chrome/browser/password_edit_dialog/PasswordEditDialogViewTest.java
@@ -0,0 +1,139 @@
+// Copyright 2021 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_edit_dialog;
+
+import android.app.Activity;
+import android.view.View;
+import android.widget.Spinner;
+import android.widget.TextView;
+
+import androidx.test.filters.MediumTest;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import org.chromium.base.test.BaseActivityTestRule;
+import org.chromium.base.test.BaseJUnit4ClassRunner;
+import org.chromium.base.test.util.CriteriaHelper;
+import org.chromium.content_public.browser.test.util.TestThreadUtils;
+import org.chromium.ui.modelutil.PropertyModel;
+import org.chromium.ui.modelutil.PropertyModelChangeProcessor;
+import org.chromium.ui.test.util.DummyUiActivity;
+
+import java.util.Arrays;
+
+/** View tests for PasswordEditDialogView. */
+@RunWith(BaseJUnit4ClassRunner.class)
+public class PasswordEditDialogViewTest {
+    private static final String[] USERNAMES = {"user1", "user2", "user3"};
+    private static final int INITIAL_USERNAME_INDEX = 1;
+    private static final int SELECTED_USERNAME_INDEX = 2;
+    private static final String PASSWORD = "Password";
+    private static final String FOOTER = "Footer";
+
+    @ClassRule
+    public static BaseActivityTestRule<DummyUiActivity> sActivityTestRule =
+            new BaseActivityTestRule<>(DummyUiActivity.class);
+
+    private static Activity sActivity;
+
+    PasswordEditDialogView mDialogView;
+    Spinner mUsernamesView;
+    TextView mPasswordView;
+    TextView mFooterView;
+    int mSelectedUsernameIndex;
+
+    @BeforeClass
+    public static void setupSuite() {
+        sActivityTestRule.launchActivity(null);
+        sActivity = TestThreadUtils.runOnUiThreadBlockingNoException(
+                () -> sActivityTestRule.getActivity());
+    }
+
+    @Before
+    public void setupTest() throws Exception {
+        TestThreadUtils.runOnUiThreadBlocking(() -> {
+            mDialogView = (PasswordEditDialogView) sActivity.getLayoutInflater().inflate(
+                    R.layout.password_edit_dialog, null);
+            mUsernamesView = (Spinner) mDialogView.findViewById(R.id.usernames_spinner);
+            mPasswordView = (TextView) mDialogView.findViewById(R.id.password);
+            mFooterView = (TextView) mDialogView.findViewById(R.id.footer);
+            sActivity.setContentView(mDialogView);
+        });
+    }
+
+    void handleUsernameSelection(int selectedUsernameIndex) {
+        mSelectedUsernameIndex = selectedUsernameIndex;
+    }
+
+    PropertyModel.Builder populateDialogPropertiesBuilder() {
+        return new PropertyModel.Builder(PasswordEditDialogProperties.ALL_KEYS)
+                .with(PasswordEditDialogProperties.USERNAMES, Arrays.asList(USERNAMES))
+                .with(PasswordEditDialogProperties.SELECTED_USERNAME_INDEX, INITIAL_USERNAME_INDEX)
+                .with(PasswordEditDialogProperties.PASSWORD, PASSWORD)
+                .with(PasswordEditDialogProperties.USERNAME_SELECTED_CALLBACK,
+                        this::handleUsernameSelection);
+    }
+
+    /** Tests that all the properties propagated correctly. */
+    @Test
+    @MediumTest
+    public void testProperties() {
+        PropertyModel model = populateDialogPropertiesBuilder()
+                                      .with(PasswordEditDialogProperties.FOOTER, FOOTER)
+                                      .build();
+        TestThreadUtils.runOnUiThreadBlocking(() -> {
+            PropertyModelChangeProcessor.create(model, mDialogView, PasswordEditDialogView::bind);
+        });
+        Assert.assertEquals("Initial selected username index doesn't match", INITIAL_USERNAME_INDEX,
+                mUsernamesView.getSelectedItemPosition());
+        Assert.assertEquals("Username text doesn't match", USERNAMES[INITIAL_USERNAME_INDEX],
+                mUsernamesView.getSelectedItem().toString());
+        Assert.assertEquals("Password doesn't match", PASSWORD, mPasswordView.getText().toString());
+        Assert.assertEquals("Footer should be visible", View.VISIBLE, mFooterView.getVisibility());
+    }
+
+    /** Tests that when the footer property is empty footer view is hidden. */
+    @Test
+    @MediumTest
+    public void testEmptyFooter() {
+        // Test with null footer property.
+        PropertyModel nullModel = populateDialogPropertiesBuilder().build();
+        TestThreadUtils.runOnUiThreadBlocking(() -> {
+            PropertyModelChangeProcessor.create(
+                    nullModel, mDialogView, PasswordEditDialogView::bind);
+        });
+        Assert.assertEquals("Footer should not be visible", View.GONE, mFooterView.getVisibility());
+
+        // Test with footer property containing empty string.
+        PropertyModel emptyModel = populateDialogPropertiesBuilder()
+                                           .with(PasswordEditDialogProperties.FOOTER, "")
+                                           .build();
+        TestThreadUtils.runOnUiThreadBlocking(() -> {
+            PropertyModelChangeProcessor.create(
+                    emptyModel, mDialogView, PasswordEditDialogView::bind);
+        });
+        Assert.assertEquals("Footer should not be visible", View.GONE, mFooterView.getVisibility());
+    }
+
+    /** Tests username selected callback. */
+    @Test
+    @MediumTest
+    public void testUsernameSelection() {
+        PropertyModel model = populateDialogPropertiesBuilder().build();
+        TestThreadUtils.runOnUiThreadBlocking(() -> {
+            PropertyModelChangeProcessor.create(model, mDialogView, PasswordEditDialogView::bind);
+            mUsernamesView.setSelection(SELECTED_USERNAME_INDEX);
+        });
+        CriteriaHelper.pollUiThread(() -> mSelectedUsernameIndex == SELECTED_USERNAME_INDEX);
+        TestThreadUtils.runOnUiThreadBlocking(
+                () -> { mUsernamesView.setSelection(INITIAL_USERNAME_INDEX); });
+        CriteriaHelper.pollUiThread(() -> mSelectedUsernameIndex == INITIAL_USERNAME_INDEX);
+    }
+}
diff --git a/chrome/browser/password_edit_dialog/android/password_edit_dialog_bridge.cc b/chrome/browser/password_edit_dialog/android/password_edit_dialog_bridge.cc
new file mode 100644
index 0000000..e70cd200
--- /dev/null
+++ b/chrome/browser/password_edit_dialog/android/password_edit_dialog_bridge.cc
@@ -0,0 +1,79 @@
+// Copyright 2021 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/password_edit_dialog/android/password_edit_dialog_bridge.h"
+
+#include "base/android/jni_string.h"
+#include "base/memory/ptr_util.h"
+#include "chrome/browser/password_edit_dialog/android/jni_headers/PasswordEditDialogBridge_jni.h"
+#include "content/public/browser/web_contents.h"
+#include "ui/android/window_android.h"
+
+// static
+std::unique_ptr<PasswordEditDialogBridge> PasswordEditDialogBridge::Create(
+    content::WebContents* web_contents,
+    DialogAcceptedCallback dialog_accepted_callback,
+    base::OnceClosure dialog_dismissed_callback) {
+  DCHECK(web_contents);
+
+  ui::WindowAndroid* window_android = web_contents->GetTopLevelNativeWindow();
+  if (!window_android)
+    return nullptr;
+  return base::WrapUnique(new PasswordEditDialogBridge(
+      window_android->GetJavaObject(), std::move(dialog_accepted_callback),
+      std::move(dialog_dismissed_callback)));
+}
+
+PasswordEditDialogBridge::PasswordEditDialogBridge(
+    base::android::ScopedJavaLocalRef<jobject> j_window_android,
+    DialogAcceptedCallback dialog_accepted_callback,
+    base::OnceClosure dialog_dismissed_callback)
+    : dialog_accepted_callback_(std::move(dialog_accepted_callback)),
+      dialog_dismissed_callback_(std::move(dialog_dismissed_callback)) {
+  JNIEnv* env = base::android::AttachCurrentThread();
+  java_password_dialog_ = Java_PasswordEditDialogBridge_create(
+      env, reinterpret_cast<intptr_t>(this), j_window_android);
+}
+
+PasswordEditDialogBridge::~PasswordEditDialogBridge() {
+  DCHECK(java_password_dialog_.is_null());
+}
+
+void PasswordEditDialogBridge::Show(
+    const std::vector<std::u16string>& usernames,
+    int selected_username_index,
+    const std::u16string& password,
+    const std::u16string& origin,
+    const std::string& account_email) {
+  JNIEnv* env = base::android::AttachCurrentThread();
+
+  base::android::ScopedJavaLocalRef<jobjectArray> j_usernames =
+      base::android::ToJavaArrayOfStrings(env, usernames);
+
+  base::android::ScopedJavaLocalRef<jstring> j_password =
+      base::android::ConvertUTF16ToJavaString(env, password);
+  base::android::ScopedJavaLocalRef<jstring> j_origin =
+      base::android::ConvertUTF16ToJavaString(env, origin);
+  base::android::ScopedJavaLocalRef<jstring> j_account_email =
+      base::android::ConvertUTF8ToJavaString(env, account_email);
+
+  Java_PasswordEditDialogBridge_show(env, java_password_dialog_, j_usernames,
+                                     selected_username_index, j_password,
+                                     j_origin, j_account_email);
+}
+
+void PasswordEditDialogBridge::Dismiss() {
+  JNIEnv* env = base::android::AttachCurrentThread();
+  Java_PasswordEditDialogBridge_dismiss(env, java_password_dialog_);
+}
+
+void PasswordEditDialogBridge::OnDialogAccepted(JNIEnv* env,
+                                                jint selected_username_index) {
+  std::move(dialog_accepted_callback_).Run(selected_username_index);
+}
+
+void PasswordEditDialogBridge::OnDialogDismissed(JNIEnv* env) {
+  java_password_dialog_.Reset();
+  std::move(dialog_dismissed_callback_).Run();
+}
diff --git a/chrome/browser/password_edit_dialog/android/password_edit_dialog_bridge.h b/chrome/browser/password_edit_dialog/android/password_edit_dialog_bridge.h
new file mode 100644
index 0000000..ca3ff7545
--- /dev/null
+++ b/chrome/browser/password_edit_dialog/android/password_edit_dialog_bridge.h
@@ -0,0 +1,95 @@
+// Copyright 2021 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_EDIT_DIALOG_ANDROID_PASSWORD_EDIT_DIALOG_BRIDGE_H_
+#define CHROME_BROWSER_PASSWORD_EDIT_DIALOG_ANDROID_PASSWORD_EDIT_DIALOG_BRIDGE_H_
+
+#include <memory>
+
+#include "base/android/jni_android.h"
+#include "base/android/scoped_java_ref.h"
+#include "base/callback.h"
+
+namespace content {
+class WebContents;
+}  // namespace content
+
+// The bridge to invoke password edit dialog in Java from native password
+// manager code.
+// The native code owns the lifetime of the bridge.
+// PasswordEditDialogBridge::Create() creates an instance of the bridge. It can
+// return nullptr when WebContents is not attached to any window. Show()
+// displays the dialog on the screen.
+//
+// OnDialogAccepted callback is called when the user accepts saving the
+// presented password (by tapping Update button). The callback parameter denotes
+// the index of selected username in the list of usernames. The dialog will be
+// dismissed after this callback, feature code shouldn't call Dismiss from
+// callback implementation to dismiss the dialog.
+//
+// OnDialogDismissed callback is called when the dialog is dismissed either from
+// user interactions or from the call to Dismiss() method. The implementation of
+// this callback should destroy the dialog bridge instance.
+//
+// Here is how typically dialog bridge is created:
+//   m_dialog_bridge = PasswordEditDialogBridge::Create(web_contents,
+//       base::BindOnce(&OnDialogAccepted),base::BindOnce(&OnDialogDismissed));
+//   if (m_dialog_bridge) m_dialog_bridge->Show(...);
+//
+// The owning class should dismiss displayed dialog during its own destruction:
+//   if (m_dialog_bridge) m_dialog_bridge->Dismiss();
+//
+// Dialog bridge instance should be destroyed in OnDialogDismissed callback:
+//   void OnDialogDismissed() {
+//     m_dialog_bridge.reset();
+//   }
+class PasswordEditDialogBridge {
+ public:
+  using DialogAcceptedCallback = base::OnceCallback<void(int)>;
+
+  ~PasswordEditDialogBridge();
+
+  // Creates and returns an instance of PasswordEditDialogBridge and
+  // corresponding Java counterpart.
+  // Returns nullptr if |web_contents| is not attached to a window.
+  static std::unique_ptr<PasswordEditDialogBridge> Create(
+      content::WebContents* web_contents,
+      DialogAcceptedCallback dialog_accepted_callback,
+      base::OnceClosure dialog_dismissed_callback);
+
+  // Disallow copy and assign.
+  PasswordEditDialogBridge(const PasswordEditDialogBridge&) = delete;
+  PasswordEditDialogBridge& operator=(const PasswordEditDialogBridge&) = delete;
+
+  // Calls Java side of the bridge to display password edit modal dialog.
+  void Show(const std::vector<std::u16string>& usernames,
+            int selected_username_index,
+            const std::u16string& password,
+            const std::u16string& origin,
+            const std::string& account_email);
+
+  // Dismisses displayed dialog. The owner of PassworDeidtDialogBridge should
+  // call this function to correctly dismiss and destroy the dialog. The object
+  // can be safely destroyed after dismiss callback is executed.
+  void Dismiss();
+
+  // Called from Java to indicate that the user tapped the positive button with
+  // |selected_username| being selected from usernames list.
+  void OnDialogAccepted(JNIEnv* env, jint selected_username_index);
+
+  // Called from Java when the modal dialog is dismissed.
+  void OnDialogDismissed(JNIEnv* env);
+
+ private:
+  PasswordEditDialogBridge(
+      base::android::ScopedJavaLocalRef<jobject> jwindow_android,
+      DialogAcceptedCallback save_password_callback,
+      base::OnceClosure dismiss_callback);
+
+  base::android::ScopedJavaGlobalRef<jobject> java_password_dialog_;
+  DialogAcceptedCallback dialog_accepted_callback_;
+  base::OnceClosure dialog_dismissed_callback_;
+};
+
+#endif  // CHROME_BROWSER_PASSWORD_EDIT_DIALOG_ANDROID_PASSWORD_EDIT_DIALOG_BRIDGE_H_