Files
cromite/build/patches/Restore-chrome-password-store.patch
2026-05-19 10:48:25 +02:00

6700 lines
299 KiB
Diff
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
From: uazo <uazo@users.noreply.github.com>
Date: Wed, 30 Sep 2020 07:40:01 +0000
Subject: Restore chrome password store
Allows the use of local storage for passwords on Android
Adds the possibility of importing and exporting a csv file containing the
list of passwords, with the same functionality available on desktops.
License: GPL-2.0-or-later - https://spdx.org/licenses/GPL-2.0-or-later.html
---
chrome/android/BUILD.gn | 8 +
chrome/android/chrome_java_resources.gni | 5 +
chrome/android/chrome_java_sources.gni | 1 +
.../ManualFillingMediator.java | 5 +-
chrome/android/java/AndroidManifest.xml | 5 +-
.../java/res/layout/password_no_result.xml | 35 +
...e_password_preferences_action_bar_menu.xml | 36 +
chrome/android/java/res/values/dimens.xml | 6 +
.../android/java/res/xml/main_preferences.xml | 3 +-
.../AutofillPaymentMethodsFragment.java | 1 +
.../settings/AutofillProfilesFragment.java | 1 +
.../settings/PasswordSettings.java | 711 ++++++++++++++++++
.../settings/FragmentDependencyProvider.java | 5 +
.../chrome/browser/settings/MainSettings.java | 17 -
.../settings/SettingsNavigationImpl.java | 4 +
chrome/browser/BUILD.gn | 7 +
.../autofill/AutofillClientProviderUtils.java | 1 +
chrome/browser/password_entry_edit/BUILD.gn | 11 +
.../password_entry_edit/android/BUILD.gn | 73 ++
.../android/credential_edit_bridge.cc | 141 ++++
.../android/credential_edit_bridge.h | 96 +++
.../android/internal/BUILD.gn | 96 +++
.../CredentialEditBridge.java | 135 ++++
.../CredentialEditCoordinator.java | 121 +++
.../CredentialEditMediator.java | 326 ++++++++
.../CredentialEditProperties.java | 48 ++
.../CredentialEditUiFactory.java | 52 ++
.../CredentialEditViewBinder.java | 52 ++
.../java/res/layout/credential_edit_view.xml | 141 ++++
.../android/java/res/layout/site_or_app.xml | 27 +
.../menu/credential_edit_action_bar_menu.xml | 26 +
.../android/java/res/values/dimens.xml | 17 +
.../CredentialEditFragmentView.java | 228 ++++++
.../CredentialEntryFragmentViewBase.java | 135 ++++
.../browser/password_manager/android/BUILD.gn | 24 +
.../ConfirmationDialogHelper.java | 146 ++++
.../PasswordManagerHelper.java | 22 +-
.../PasswordManagerUtilBridge.java | 3 +-
.../settings/CallbackDelayer.java | 18 +
.../settings/DialogManager.java | 168 +++++
.../settings/ExportErrorDialogFragment.java | 110 +++
.../password_manager/settings/ExportFlow.java | 583 ++++++++++++++
.../settings/ExportFlowInterface.java | 90 +++
.../settings/NonCancelableProgressBar.java | 55 ++
.../PasswordAccessReauthenticationHelper.java | 143 ++++
.../settings/PasswordListObserver.java | 28 +
.../settings/PasswordManagerHandler.java | 86 +++
.../PasswordManagerHandlerProvider.java | 144 ++++
.../PasswordReauthenticationFragment.java | 111 +++
.../settings/PasswordUiView.java | 223 ++++++
.../settings/PasswordsPreference.java | 1 +
.../settings/ProgressBarDialogFragment.java | 70 ++
.../settings/ReauthenticationManager.java | 198 +++++
.../settings/SavedPasswordEntry.java | 41 +
.../settings/SingleThreadBarrierClosure.java | 49 ++
.../settings/TimedCallbackDelayer.java | 33 +
.../android/password_manager_android_util.cc | 1 +
.../android/password_ui_view_android.cc | 450 +++++++++++
.../android/password_ui_view_android.h | 196 +++++
.../android/pwm_disabled/BUILD.gn | 2 -
...ssword_manager_settings_service_factory.cc | 4 +-
.../password_store_backend_factory.cc | 14 +-
chrome/browser/prefs/browser_prefs.cc | 7 -
.../strings/android_chrome_strings.grd | 123 ++-
.../Import-Password-Android.grdp | 9 +
.../autofill/core/common/autofill_features.cc | 1 +
.../settings/SettingsNavigation.java | 3 +
.../password_manager_features_util.cc | 1 +
.../core/browser/import/password_importer.cc | 11 +
.../core/browser/password_manager.cc | 2 -
.../browser/password_manager_constants.cc | 2 -
.../core/browser/password_manager_constants.h | 2 -
.../core/browser/password_store/BUILD.gn | 13 +
.../password_store/login_database_posix.cc | 4 +-
.../password_store_built_in_backend.cc | 6 +
.../browser/password_store_factory_util.cc | 6 -
.../browser/password_store_factory_util.h | 4 -
.../core/common/password_manager_pref_names.h | 2 -
components/sync/service/sync_prefs.cc | 2 +-
.../Restore-chrome-password-store.inc | 1 +
80 files changed, 5707 insertions(+), 81 deletions(-)
create mode 100644 chrome/android/java/res/layout/password_no_result.xml
create mode 100644 chrome/android/java/res/menu/save_password_preferences_action_bar_menu.xml
create mode 100644 chrome/android/java/src/org/chromium/chrome/browser/password_manager/settings/PasswordSettings.java
create mode 100644 chrome/browser/password_entry_edit/BUILD.gn
create mode 100644 chrome/browser/password_entry_edit/android/BUILD.gn
create mode 100644 chrome/browser/password_entry_edit/android/credential_edit_bridge.cc
create mode 100644 chrome/browser/password_entry_edit/android/credential_edit_bridge.h
create mode 100644 chrome/browser/password_entry_edit/android/internal/BUILD.gn
create mode 100644 chrome/browser/password_entry_edit/android/internal/java/src/org/chromium/chrome/browser/password_entry_edit/CredentialEditBridge.java
create mode 100644 chrome/browser/password_entry_edit/android/internal/java/src/org/chromium/chrome/browser/password_entry_edit/CredentialEditCoordinator.java
create mode 100644 chrome/browser/password_entry_edit/android/internal/java/src/org/chromium/chrome/browser/password_entry_edit/CredentialEditMediator.java
create mode 100644 chrome/browser/password_entry_edit/android/internal/java/src/org/chromium/chrome/browser/password_entry_edit/CredentialEditProperties.java
create mode 100644 chrome/browser/password_entry_edit/android/internal/java/src/org/chromium/chrome/browser/password_entry_edit/CredentialEditUiFactory.java
create mode 100644 chrome/browser/password_entry_edit/android/internal/java/src/org/chromium/chrome/browser/password_entry_edit/CredentialEditViewBinder.java
create mode 100644 chrome/browser/password_entry_edit/android/java/res/layout/credential_edit_view.xml
create mode 100644 chrome/browser/password_entry_edit/android/java/res/layout/site_or_app.xml
create mode 100644 chrome/browser/password_entry_edit/android/java/res/menu/credential_edit_action_bar_menu.xml
create mode 100644 chrome/browser/password_entry_edit/android/java/res/values/dimens.xml
create mode 100644 chrome/browser/password_entry_edit/android/java/src/org/chromium/chrome/browser/password_entry_edit/CredentialEditFragmentView.java
create mode 100644 chrome/browser/password_entry_edit/android/java/src/org/chromium/chrome/browser/password_entry_edit/CredentialEntryFragmentViewBase.java
create mode 100644 chrome/browser/password_manager/android/java/src/org/chromium/chrome/browser/password_manager/ConfirmationDialogHelper.java
create mode 100644 chrome/browser/password_manager/android/java/src/org/chromium/chrome/browser/password_manager/settings/CallbackDelayer.java
create mode 100644 chrome/browser/password_manager/android/java/src/org/chromium/chrome/browser/password_manager/settings/DialogManager.java
create mode 100644 chrome/browser/password_manager/android/java/src/org/chromium/chrome/browser/password_manager/settings/ExportErrorDialogFragment.java
create mode 100644 chrome/browser/password_manager/android/java/src/org/chromium/chrome/browser/password_manager/settings/ExportFlow.java
create mode 100644 chrome/browser/password_manager/android/java/src/org/chromium/chrome/browser/password_manager/settings/ExportFlowInterface.java
create mode 100644 chrome/browser/password_manager/android/java/src/org/chromium/chrome/browser/password_manager/settings/NonCancelableProgressBar.java
create mode 100644 chrome/browser/password_manager/android/java/src/org/chromium/chrome/browser/password_manager/settings/PasswordAccessReauthenticationHelper.java
create mode 100644 chrome/browser/password_manager/android/java/src/org/chromium/chrome/browser/password_manager/settings/PasswordListObserver.java
create mode 100644 chrome/browser/password_manager/android/java/src/org/chromium/chrome/browser/password_manager/settings/PasswordManagerHandler.java
create mode 100644 chrome/browser/password_manager/android/java/src/org/chromium/chrome/browser/password_manager/settings/PasswordManagerHandlerProvider.java
create mode 100644 chrome/browser/password_manager/android/java/src/org/chromium/chrome/browser/password_manager/settings/PasswordReauthenticationFragment.java
create mode 100644 chrome/browser/password_manager/android/java/src/org/chromium/chrome/browser/password_manager/settings/PasswordUiView.java
create mode 100644 chrome/browser/password_manager/android/java/src/org/chromium/chrome/browser/password_manager/settings/ProgressBarDialogFragment.java
create mode 100644 chrome/browser/password_manager/android/java/src/org/chromium/chrome/browser/password_manager/settings/ReauthenticationManager.java
create mode 100644 chrome/browser/password_manager/android/java/src/org/chromium/chrome/browser/password_manager/settings/SavedPasswordEntry.java
create mode 100644 chrome/browser/password_manager/android/java/src/org/chromium/chrome/browser/password_manager/settings/SingleThreadBarrierClosure.java
create mode 100644 chrome/browser/password_manager/android/java/src/org/chromium/chrome/browser/password_manager/settings/TimedCallbackDelayer.java
create mode 100644 chrome/browser/password_manager/android/password_ui_view_android.cc
create mode 100644 chrome/browser/password_manager/android/password_ui_view_android.h
create mode 100644 chrome/browser/ui/android/strings/cromite_android_chrome_strings_grd/Import-Password-Android.grdp
create mode 100644 cromite_flags/components/password_manager/core/browser/features/password_features_cc/Restore-chrome-password-store.inc
diff --git a/chrome/android/BUILD.gn b/chrome/android/BUILD.gn
--- a/chrome/android/BUILD.gn
+++ b/chrome/android/BUILD.gn
@@ -827,6 +827,10 @@ if (_is_default_toolchain) {
"//url/mojom:url_mojom_origin_java",
]
+ deps += [
+ "//chrome/browser/password_entry_edit:public_java",
+ ]
+
deps += feed_deps
srcjar_deps = [
@@ -981,6 +985,10 @@ if (_is_default_toolchain) {
"//components/visited_url_ranking/internal:internal_java",
]
+ deps += [
+ "//chrome/browser/password_entry_edit/android/internal:java",
+ ]
+
if (is_desktop_android) {
deps +=
[ "//chrome/browser/ui/android/extensions/windowing/internal:java" ]
diff --git a/chrome/android/chrome_java_resources.gni b/chrome/android/chrome_java_resources.gni
--- a/chrome/android/chrome_java_resources.gni
+++ b/chrome/android/chrome_java_resources.gni
@@ -666,3 +666,8 @@ chrome_java_resources = [
"java/res/xml/tracing_preferences.xml",
"java/res/xml/unified_account_settings_preferences.xml",
]
+
+chrome_java_resources += [
+ "java/res/layout/password_no_result.xml",
+ "java/res/menu/save_password_preferences_action_bar_menu.xml",
+]
diff --git a/chrome/android/chrome_java_sources.gni b/chrome/android/chrome_java_sources.gni
--- a/chrome/android/chrome_java_sources.gni
+++ b/chrome/android/chrome_java_sources.gni
@@ -947,6 +947,7 @@ chrome_java_sources = [
"java/src/org/chromium/chrome/browser/password_manager/PasswordManagerDialogViewBinder.java",
"java/src/org/chromium/chrome/browser/password_manager/PasswordManagerErrorMessageHelperBridge.java",
"java/src/org/chromium/chrome/browser/password_manager/PasswordManagerLauncher.java",
+ "java/src/org/chromium/chrome/browser/password_manager/settings/PasswordSettings.java",
"java/src/org/chromium/chrome/browser/payments/AddressEditor.java",
"java/src/org/chromium/chrome/browser/payments/AutofillContact.java",
"java/src/org/chromium/chrome/browser/payments/ChromePaymentRequestFactory.java",
diff --git a/chrome/android/features/keyboard_accessory/internal/java/src/org/chromium/chrome/browser/keyboard_accessory/ManualFillingMediator.java b/chrome/android/features/keyboard_accessory/internal/java/src/org/chromium/chrome/browser/keyboard_accessory/ManualFillingMediator.java
--- a/chrome/android/features/keyboard_accessory/internal/java/src/org/chromium/chrome/browser/keyboard_accessory/ManualFillingMediator.java
+++ b/chrome/android/features/keyboard_accessory/internal/java/src/org/chromium/chrome/browser/keyboard_accessory/ManualFillingMediator.java
@@ -620,10 +620,7 @@ class ManualFillingMediator
// suggestions for non credential fields. The check for feature flag needs to happen before
// `IS_CREDENTIAL_FIELD_OR_HAS_AUTOFILL_SUGGESTIONS` check to ensure we get the unbiased
// metrics.
- return isLargeFormFactor()
- && ChromeFeatureList.isEnabled(
- ChromeFeatureList.AUTOFILL_ANDROID_DESKTOP_SUPPRESS_ACCESSORY_ON_EMPTY)
- && !mModel.get(IS_CREDENTIAL_FIELD_OR_HAS_AUTOFILL_SUGGESTIONS);
+ return !mModel.get(IS_CREDENTIAL_FIELD_OR_HAS_AUTOFILL_SUGGESTIONS);
}
/**
diff --git a/chrome/android/java/AndroidManifest.xml b/chrome/android/java/AndroidManifest.xml
--- a/chrome/android/java/AndroidManifest.xml
+++ b/chrome/android/java/AndroidManifest.xml
@@ -477,10 +477,9 @@ by a child template that "extends" this file.
</intent-filter>
</activity>
- <activity android:name="org.chromium.chrome.browser.AutofillOptionsLauncher"
+ <activity android:name="org.chromium.chrome.browser.AutofillOptionsLauncher" android:exported="false"
android:theme="@style/Theme.BrowserUI.NoDisplay"
- android:excludeFromRecents="true"
- android:exported="true">
+ android:excludeFromRecents="true">
<intent-filter>
<action android:name="android.intent.action.APPLICATION_PREFERENCES" />
<category android:name="android.intent.category.DEFAULT" />
diff --git a/chrome/android/java/res/layout/password_no_result.xml b/chrome/android/java/res/layout/password_no_result.xml
new file mode 100644
--- /dev/null
+++ b/chrome/android/java/res/layout/password_no_result.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+Copyright 2020 The Chromium Authors
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <org.chromium.components.browser_ui.widget.MaterialCardViewNoShadow
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:layout_marginStart="16dp"
+ android:layout_marginEnd="16dp"
+ android:foreground="@drawable/button_borderless_compat"
+ style="@style/MaterialCardStyle">
+
+ <org.chromium.ui.widget.TextViewWithLeading
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:padding="@dimen/card_padding"
+ android:text="@string/password_no_result"
+ android:textAlignment="center"
+ android:textAppearance="@style/TextAppearance.TextMedium.Secondary"
+ app:leading="@dimen/text_size_medium_leading" />
+
+ </org.chromium.components.browser_ui.widget.MaterialCardViewNoShadow>
+
+</FrameLayout>
diff --git a/chrome/android/java/res/menu/save_password_preferences_action_bar_menu.xml b/chrome/android/java/res/menu/save_password_preferences_action_bar_menu.xml
new file mode 100644
--- /dev/null
+++ b/chrome/android/java/res/menu/save_password_preferences_action_bar_menu.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+Copyright 2017 The Chromium Authors
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto" >
+
+ <item
+ android:id="@+id/menu_id_search"
+ android:icon="@drawable/ic_search_24dp"
+ android:title="@string/search"
+ app:showAsAction="always"
+ app:actionViewClass="androidx.appcompat.widget.SearchView" />
+
+ <item
+ android:id="@id/menu_id_targeted_help"
+ android:icon="@drawable/ic_help_24dp"
+ android:title="@string/menu_help"
+ android:visibility="gone"
+ app:showAsAction="ifRoom"/>
+
+ <item
+ android:id="@+id/import_passwords"
+ android:title="@string/password_settings_import_action_title"
+ android:contentDescription="@string/password_settings_import_action_description"
+ app:showAsAction="never"/>
+
+ <item
+ android:id="@+id/export_passwords"
+ android:title="@string/password_settings_export_action_title"
+ android:contentDescription="@string/password_settings_export_action_description"
+ app:showAsAction="never"/>
+</menu>
diff --git a/chrome/android/java/res/values/dimens.xml b/chrome/android/java/res/values/dimens.xml
--- a/chrome/android/java/res/values/dimens.xml
+++ b/chrome/android/java/res/values/dimens.xml
@@ -76,6 +76,12 @@ found in the LICENSE file.
<dimen name="keyboard_accessory_height_with_shadow">56dp</dimen>
<dimen name="keyboard_accessory_sheet_height">330dp</dimen>
+ <!-- Password entry editor dimensions -->
+ <dimen name="password_entry_editor_content_spacing">12dp</dimen>
+ <dimen name="password_entry_editor_field_bottom_margin">8dp</dimen>
+ <dimen name="password_entry_editor_field_large_top_margin">16dp</dimen>
+ <dimen name="password_entry_editor_field_top_margin">8dp</dimen>
+
<!-- Password manager illustration dialogs -->
<dimen name="password_manager_dialog_padding">8dp</dimen>
<dimen name="password_manager_dialog_illustration_height">156dp</dimen>
diff --git a/chrome/android/java/res/xml/main_preferences.xml b/chrome/android/java/res/xml/main_preferences.xml
--- a/chrome/android/java/res/xml/main_preferences.xml
+++ b/chrome/android/java/res/xml/main_preferences.xml
@@ -69,7 +69,8 @@ found in the LICENSE file.
android:key="autofill_section"
android:order="12"
android:title="@string/prefs_section_autofill"/>
- <org.chromium.chrome.browser.password_manager.settings.PasswordsPreference
+ <org.chromium.components.browser_ui.settings.ChromeBasePreference
+ android:fragment="org.chromium.chrome.browser.password_manager.settings.PasswordSettings"
android:key="passwords"
android:order="13"
android:title="@string/password_manager_settings_title"/>
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/autofill/settings/AutofillPaymentMethodsFragment.java b/chrome/android/java/src/org/chromium/chrome/browser/autofill/settings/AutofillPaymentMethodsFragment.java
--- a/chrome/android/java/src/org/chromium/chrome/browser/autofill/settings/AutofillPaymentMethodsFragment.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/autofill/settings/AutofillPaymentMethodsFragment.java
@@ -826,6 +826,7 @@ public class AutofillPaymentMethodsFragment extends ChromeBaseSettingsFragment
}
private static boolean disabledSettingsInThirdPartyMode(Profile profile) {
+ if ((true)) return false;
return (AutofillClientProviderUtils.getAndroidAutofillFrameworkAvailability(
UserPrefs.get(profile))
== AndroidAutofillAvailabilityStatus.AVAILABLE);
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/autofill/settings/AutofillProfilesFragment.java b/chrome/android/java/src/org/chromium/chrome/browser/autofill/settings/AutofillProfilesFragment.java
--- a/chrome/android/java/src/org/chromium/chrome/browser/autofill/settings/AutofillProfilesFragment.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/autofill/settings/AutofillProfilesFragment.java
@@ -772,6 +772,7 @@ public class AutofillProfilesFragment extends ChromeBaseSettingsFragment
}
private static boolean disabledSettingsInThirdPartyMode(Profile profile) {
+ if ((true)) return false;
return AutofillClientProviderUtils.getAndroidAutofillFrameworkAvailability(
UserPrefs.get(profile))
== AndroidAutofillAvailabilityStatus.AVAILABLE;
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/password_manager/settings/PasswordSettings.java b/chrome/android/java/src/org/chromium/chrome/browser/password_manager/settings/PasswordSettings.java
new file mode 100644
--- /dev/null
+++ b/chrome/android/java/src/org/chromium/chrome/browser/password_manager/settings/PasswordSettings.java
@@ -0,0 +1,711 @@
+// Copyright 2014 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.chrome.browser.password_manager.settings;
+
+import static org.chromium.chrome.browser.password_manager.PasswordMetricsUtil.PASSWORD_SETTINGS_EXPORT_METRICS_ID;
+import static org.chromium.build.NullUtil.assumeNonNull;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.text.SpannableString;
+import android.text.style.ForegroundColorSpan;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.StringRes;
+import androidx.appcompat.widget.Toolbar;
+import androidx.fragment.app.FragmentManager;
+import androidx.preference.Preference;
+import androidx.preference.PreferenceCategory;
+import androidx.preference.PreferenceGroup;
+
+import org.chromium.base.supplier.MonotonicObservableSupplier;
+import org.chromium.base.supplier.ObservableSuppliers;
+import org.chromium.base.supplier.SettableMonotonicObservableSupplier;
+import org.chromium.build.annotations.Nullable;
+import org.chromium.build.annotations.MonotonicNonNull;
+import org.chromium.chrome.R;
+import org.chromium.chrome.browser.password_manager.ManagePasswordsReferrer;
+import org.chromium.chrome.browser.password_manager.PasswordManagerHelper;
+import org.chromium.chrome.browser.preferences.Pref;
+import org.chromium.chrome.browser.profiles.Profile;
+import org.chromium.chrome.browser.settings.ChromeBaseSettingsFragment;
+import org.chromium.chrome.browser.settings.ChromeManagedPreferenceDelegate;
+import org.chromium.chrome.browser.sync.SyncServiceFactory;
+import org.chromium.chrome.browser.sync.settings.SyncSettingsUtils;
+import org.chromium.components.browser_ui.settings.ChromeBasePreference;
+import org.chromium.components.browser_ui.settings.ChromeSwitchPreference;
+import org.chromium.components.browser_ui.settings.SearchUtils;
+import org.chromium.components.browser_ui.settings.SearchViewProvider;
+import org.chromium.components.browser_ui.settings.TextMessagePreference;
+import org.chromium.components.browser_ui.styles.SemanticColorUtils;
+import org.chromium.components.prefs.PrefService;
+import org.chromium.components.signin.base.CoreAccountInfo;
+import org.chromium.components.sync.PassphraseType;
+import org.chromium.components.sync.SyncService;
+import org.chromium.components.user_prefs.UserPrefs;
+import org.chromium.ui.text.SpanApplier;
+import org.chromium.ui.base.ActivityWindowAndroid;
+import org.chromium.ui.base.ActivityIntentRequestTrackerDelegate;
+import org.chromium.ui.base.IntentRequestTracker;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Locale;
+
+/**
+ * The "Passwords" screen in Settings, which allows the user to enable or disable password saving,
+ * to view saved passwords (just the username and URL), and to delete saved passwords.
+ */
+public class PasswordSettings extends ChromeBaseSettingsFragment
+ implements PasswordListObserver,
+ Preference.OnPreferenceClickListener, SearchViewProvider,
+ SyncService.SyncStateChangedListener {
+ @IntDef({
+ TrustedVaultBannerState.NOT_SHOWN,
+ TrustedVaultBannerState.OFFER_OPT_IN,
+ TrustedVaultBannerState.OPTED_IN
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ private @interface TrustedVaultBannerState {
+ int NOT_SHOWN = 0;
+ int OFFER_OPT_IN = 1;
+ int OPTED_IN = 2;
+ }
+
+ // Keys for name/password dictionaries.
+ public static final String PASSWORD_LIST_URL = "url";
+ public static final String PASSWORD_LIST_NAME = "name";
+ public static final String PASSWORD_LIST_PASSWORD = "password";
+
+ // Used to pass the password id into a new activity.
+ public static final String PASSWORD_LIST_ID = "id";
+
+ // The key for saving |mSearchQuery| to instance bundle.
+ private static final String SAVED_STATE_SEARCH_QUERY = "saved-state-search-query";
+
+ public static final String PREF_SAVE_PASSWORDS_SWITCH = "save_passwords_switch";
+ public static final String PREF_AUTOSIGNIN_SWITCH = "autosignin_switch";
+ public static final String PREF_CHECK_PASSWORDS = "check_passwords";
+ public static final String PREF_TRUSTED_VAULT_BANNER = "trusted_vault_banner";
+ public static final String PREF_KEY_MANAGE_ACCOUNT_LINK = "manage_account_link";
+
+ private static final String PREF_KEY_CATEGORY_SAVED_PASSWORDS = "saved_passwords";
+ private static final String PREF_KEY_CATEGORY_EXCEPTIONS = "exceptions";
+ private static final String PREF_KEY_SAVED_PASSWORDS_NO_TEXT = "saved_passwords_no_text";
+
+ private static final int ORDER_SWITCH = 0;
+ private static final int ORDER_AUTO_SIGNIN_CHECKBOX = 1;
+ private static final int ORDER_CHECK_PASSWORDS = 2;
+ private static final int ORDER_TRUSTED_VAULT_BANNER = 3;
+ private static final int ORDER_MANAGE_ACCOUNT_LINK = 4;
+ private static final int ORDER_SAVED_PASSWORDS = 6;
+ private static final int ORDER_EXCEPTIONS = 7;
+ private static final int ORDER_SAVED_PASSWORDS_NO_TEXT = 8;
+
+ // This request code is not actually consumed today in onActivityResult() but is defined here to
+ // avoid bugs in the future if the request code is reused.
+ private static final int REQUEST_CODE_TRUSTED_VAULT_OPT_IN = 1;
+
+ // Unique request code for the password exporting activity.
+ private static final int PASSWORD_EXPORT_INTENT_REQUEST_CODE = 3485764;
+
+ private boolean mNoPasswords;
+ private boolean mNoPasswordExceptions;
+ private @TrustedVaultBannerState int mTrustedVaultBannerState =
+ TrustedVaultBannerState.NOT_SHOWN;
+
+ private MenuItem mHelpItem;
+ private MenuItem mSearchItem;
+
+ private String mSearchQuery;
+ private Preference mLinkPref;
+ private Menu mMenu;
+
+ private final SettableMonotonicObservableSupplier<String> mPageTitle =
+ ObservableSuppliers.createMonotonic();
+
+ /** For controlling the UX flow of exporting passwords. */
+ private final ExportFlow mExportFlow = new ExportFlow();
+ private ActivityWindowAndroid mWindowAndroid;
+ private IntentRequestTracker mIntentRequestTracker;
+ private @MonotonicNonNull SearchViewProvider.Observer mSearchViewObserver;
+
+ @Override
+ public void onCreatePreferences(@Nullable Bundle savedInstanceState, @Nullable String rootKey) {
+ mExportFlow.onCreate(
+ savedInstanceState,
+ new ExportFlow.Delegate() {
+ @Override
+ public Activity getActivity() {
+ return PasswordSettings.this.getActivity();
+ }
+
+ @Override
+ public FragmentManager getFragmentManager() {
+ return PasswordSettings.this.getFragmentManager();
+ }
+
+ @Override
+ public int getViewId() {
+ return getView().getId();
+ }
+
+ @Override
+ public void runCreateFileOnDiskIntent(Intent intent) {
+ startActivityForResult(intent, PASSWORD_EXPORT_INTENT_REQUEST_CODE);
+ }
+
+ @Override
+ public Profile getProfile() {
+ return PasswordSettings.this.getProfile();
+ }
+ },
+ PASSWORD_SETTINGS_EXPORT_METRICS_ID);
+ mPageTitle.set(getString(R.string.password_manager_settings_title));
+ setPreferenceScreen(getPreferenceManager().createPreferenceScreen(getStyledContext()));
+ PasswordManagerHandlerProvider.getForProfile(getProfile()).addObserver(this);
+
+ if (SyncServiceFactory.getForProfile(getProfile()) != null) {
+ SyncServiceFactory.getForProfile(getProfile()).addSyncStateChangedListener(this);
+ }
+
+ setHasOptionsMenu(true); // Password Export might be optional but Search is always present.
+
+ mIntentRequestTracker = IntentRequestTracker.createFromDelegate(
+ new ActivityIntentRequestTrackerDelegate(getActivity()) {
+ @Override
+ public boolean startActivityForResult(@Nullable Intent intent, int requestCode) {
+ PasswordSettings.this.startActivityForResult(intent, requestCode);
+ return true;
+ }
+ });
+ mWindowAndroid = new ActivityWindowAndroid(
+ getActivity(), /* listenToActivityState */ true,
+ mIntentRequestTracker, /*InsetObserver*/ null,
+ /* trackOcclusion= */ true);
+
+ if (savedInstanceState == null) return;
+
+ if (savedInstanceState.containsKey(SAVED_STATE_SEARCH_QUERY)) {
+ mSearchQuery = savedInstanceState.getString(SAVED_STATE_SEARCH_QUERY);
+ }
+ }
+
+ @Override
+ public MonotonicObservableSupplier<String> getPageTitle() {
+ return mPageTitle;
+ }
+
+ @Override
+ public void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ computeTrustedVaultBannerState();
+ }
+
+ @Override
+ public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+
+ // Disable animations of preference changes.
+ getListView().setItemAnimator(null);
+ }
+
+ @Override
+ public void setSearchViewObserver(SearchViewProvider.Observer observer) {
+ mSearchViewObserver = observer;
+ }
+
+ @Override
+ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+ menu.clear();
+ mMenu = menu;
+ inflater.inflate(R.menu.save_password_preferences_action_bar_menu, menu);
+ menu.findItem(R.id.export_passwords).setVisible(true);
+ menu.findItem(R.id.export_passwords).setEnabled(false);
+ mSearchItem = menu.findItem(R.id.menu_id_search);
+ mSearchItem.setVisible(true);
+ mHelpItem = menu.findItem(R.id.menu_id_targeted_help);
+ mHelpItem.setVisible(false);
+ SearchUtils.initializeSearchView(
+ mSearchItem, mSearchQuery, getActivity(),
+ assumeNonNull(mSearchViewObserver), this::filterPasswords);
+ }
+
+ @Override
+ public void onPrepareOptionsMenu(Menu menu) {
+ menu.findItem(R.id.export_passwords).setEnabled(!mNoPasswords && !mExportFlow.isActive());
+ super.onPrepareOptionsMenu(menu);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ int id = item.getItemId();
+ if (id == R.id.import_passwords) {
+ PasswordManagerHandlerProvider.getForProfile(getProfile())
+ .getPasswordManagerHandler()
+ .startImporting(mWindowAndroid);
+ return true;
+ }
+ if (id == R.id.export_passwords) {
+ mExportFlow.startExporting();
+ return true;
+ }
+ if (SearchUtils.handleSearchNavigation(item, mSearchItem, mSearchQuery, getActivity())) {
+ filterPasswords(null);
+ return true;
+ }
+ if (id == R.id.menu_id_targeted_help) {
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ private void filterPasswords(String query) {
+ mSearchQuery = query;
+ mHelpItem.setShowAsAction(
+ mSearchQuery == null
+ ? MenuItem.SHOW_AS_ACTION_IF_ROOM
+ : MenuItem.SHOW_AS_ACTION_NEVER);
+ rebuildPasswordLists();
+ }
+
+ /** Empty screen message when no passwords or exceptions are stored. */
+ private void displayEmptyScreenMessage() {
+ TextMessagePreference emptyView = new TextMessagePreference(getStyledContext(), null);
+ emptyView.setSummary(R.string.saved_passwords_none_text);
+ emptyView.setKey(PREF_KEY_SAVED_PASSWORDS_NO_TEXT);
+ emptyView.setOrder(ORDER_SAVED_PASSWORDS_NO_TEXT);
+ emptyView.setDividerAllowedAbove(false);
+ emptyView.setDividerAllowedBelow(false);
+ getPreferenceScreen().addPreference(emptyView);
+ }
+
+ /** Include a message when there's no match. */
+ private void displayPasswordNoResultScreenMessage() {
+ Preference noResultView = new Preference(getStyledContext());
+ noResultView.setLayoutResource(R.layout.password_no_result);
+ noResultView.setSelectable(false);
+ getPreferenceScreen().addPreference(noResultView);
+ }
+
+ @Override
+ public void onDetach() {
+ super.onDetach();
+ ReauthenticationManager.resetLastReauth();
+ }
+
+ void rebuildPasswordLists() {
+ mNoPasswords = false;
+ mNoPasswordExceptions = false;
+ getPreferenceScreen().removeAll();
+ if (mSearchQuery != null) {
+ // Only the filtered passwords and exceptions should be shown.
+ PasswordManagerHandlerProvider.getForProfile(getProfile())
+ .getPasswordManagerHandler()
+ .updatePasswordLists();
+ return;
+ }
+
+ createSavePasswordsSwitch();
+ createAutoSignInCheckbox();
+
+ PasswordManagerHandlerProvider.getForProfile(getProfile())
+ .getPasswordManagerHandler()
+ .updatePasswordLists();
+ }
+
+ /**
+ * Removes the UI displaying the list of saved passwords or exceptions.
+ * @param preferenceCategoryKey The key string identifying the PreferenceCategory to be removed.
+ */
+ private void resetList(String preferenceCategoryKey) {
+ PreferenceCategory profileCategory =
+ (PreferenceCategory) getPreferenceScreen().findPreference(preferenceCategoryKey);
+ if (profileCategory != null) {
+ profileCategory.removeAll();
+ getPreferenceScreen().removePreference(profileCategory);
+ }
+ }
+
+ /** Removes the message informing the user that there are no saved entries to display. */
+ private void resetNoEntriesTextMessage() {
+ Preference message = getPreferenceScreen().findPreference(PREF_KEY_SAVED_PASSWORDS_NO_TEXT);
+ if (message != null) {
+ getPreferenceScreen().removePreference(message);
+ }
+ }
+
+ @Override
+ public void passwordListAvailable(int count) {
+ resetList(PREF_KEY_CATEGORY_SAVED_PASSWORDS);
+ resetNoEntriesTextMessage();
+
+ mNoPasswords = count == 0;
+ if (mNoPasswords) {
+ if (mNoPasswordExceptions) displayEmptyScreenMessage();
+ return;
+ }
+
+ displayManageAccountLink();
+
+ PreferenceGroup passwordParent;
+ if (mSearchQuery == null) {
+ PreferenceCategory profileCategory = new PreferenceCategory(getStyledContext());
+ profileCategory.setKey(PREF_KEY_CATEGORY_SAVED_PASSWORDS);
+ profileCategory.setTitle(R.string.password_list_title);
+ profileCategory.setOrder(ORDER_SAVED_PASSWORDS);
+ getPreferenceScreen().addPreference(profileCategory);
+ passwordParent = profileCategory;
+ } else {
+ passwordParent = getPreferenceScreen();
+ }
+ for (int i = 0; i < count; i++) {
+ SavedPasswordEntry saved =
+ PasswordManagerHandlerProvider.getForProfile(getProfile())
+ .getPasswordManagerHandler()
+ .getSavedPasswordEntry(i);
+ String url = saved.getUrl();
+ String name = saved.getUserName();
+ String password = saved.getPassword();
+ if (shouldBeFiltered(url, name)) {
+ continue; // The current password won't show with the active filter, try the next.
+ }
+ Preference preference = new Preference(getStyledContext());
+ preference.setTitle(url);
+ preference.setOnPreferenceClickListener(this);
+ preference.setSummary(name);
+ Bundle args = preference.getExtras();
+ args.putString(PASSWORD_LIST_NAME, name);
+ args.putString(PASSWORD_LIST_URL, url);
+ args.putString(PASSWORD_LIST_PASSWORD, password);
+ args.putInt(PASSWORD_LIST_ID, i);
+ passwordParent.addPreference(preference);
+ }
+ mNoPasswords = passwordParent.getPreferenceCount() == 0;
+ if (mMenu != null) {
+ MenuItem menuItem = mMenu.findItem(R.id.export_passwords);
+ if (menuItem != null) {
+ menuItem.setEnabled(!mNoPasswords && !mExportFlow.isActive());
+ }
+ }
+ if (mNoPasswords) {
+ if (count == 0) displayEmptyScreenMessage(); // Show if the list was already empty.
+ if (mSearchQuery == null) {
+ // If not searching, the category needs to be removed again.
+ getPreferenceScreen().removePreference(passwordParent);
+ } else {
+ displayPasswordNoResultScreenMessage();
+ getView()
+ .announceForAccessibility(
+ getString(R.string.accessible_find_in_page_no_results));
+ }
+ }
+ }
+
+ /**
+ * Returns true if there is a search query that requires the exclusion of an entry based on
+ * the passed url or name.
+ * @param url the visible URL of the entry to check. May be empty but must not be null.
+ * @param name the visible user name of the entry to check. May be empty but must not be null.
+ * @return Returns whether the entry with the passed url and name should be filtered.
+ */
+ private boolean shouldBeFiltered(final String url, final String name) {
+ if (mSearchQuery == null) {
+ return false;
+ }
+ return !url.toLowerCase(Locale.ENGLISH).contains(mSearchQuery.toLowerCase(Locale.ENGLISH))
+ && !name.toLowerCase(Locale.getDefault())
+ .contains(mSearchQuery.toLowerCase(Locale.getDefault()));
+ }
+
+ @Override
+ public void passwordExceptionListAvailable(int count) {
+ if (mSearchQuery != null) return; // Don't show exceptions if a search is ongoing.
+ resetList(PREF_KEY_CATEGORY_EXCEPTIONS);
+ resetNoEntriesTextMessage();
+
+ mNoPasswordExceptions = count == 0;
+ if (mNoPasswordExceptions) {
+ if (mNoPasswords) displayEmptyScreenMessage();
+ return;
+ }
+
+ displayManageAccountLink();
+
+ PreferenceCategory profileCategory = new PreferenceCategory(getStyledContext());
+ profileCategory.setKey(PREF_KEY_CATEGORY_EXCEPTIONS);
+ profileCategory.setTitle(R.string.section_saved_passwords_exceptions);
+ profileCategory.setOrder(ORDER_EXCEPTIONS);
+ getPreferenceScreen().addPreference(profileCategory);
+ for (int i = 0; i < count; i++) {
+ String exception =
+ PasswordManagerHandlerProvider.getForProfile(getProfile())
+ .getPasswordManagerHandler()
+ .getSavedPasswordException(i);
+ Preference preference = new Preference(getStyledContext());
+ preference.setTitle(exception);
+ preference.setOnPreferenceClickListener(this);
+ Bundle args = preference.getExtras();
+ args.putString(PASSWORD_LIST_URL, exception);
+ args.putInt(PASSWORD_LIST_ID, i);
+ profileCategory.addPreference(preference);
+ }
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ rebuildPasswordLists();
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ mExportFlow.onResume();
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent intent) {
+ super.onActivityResult(requestCode, resultCode, intent);
+ mWindowAndroid.getIntentRequestTracker().onActivityResult(requestCode, resultCode, intent);
+ if (requestCode != PASSWORD_EXPORT_INTENT_REQUEST_CODE) return;
+ if (resultCode != Activity.RESULT_OK) return;
+ if (intent == null || intent.getData() == null) return;
+
+ mExportFlow.savePasswordsToDownloads(intent.getData());
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ mExportFlow.onSaveInstanceState(outState);
+ mWindowAndroid.getIntentRequestTracker().saveInstanceState(outState);
+ if (mSearchQuery != null) {
+ outState.putString(SAVED_STATE_SEARCH_QUERY, mSearchQuery);
+ }
+ }
+
+ @Override
+ public void onRequestPermissionsResult(
+ int requestCode, String[] permissions, int[] grantResults) {
+ if (mWindowAndroid.handlePermissionResult(requestCode, permissions, grantResults))
+ return;
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults);
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+
+ if (mSearchViewObserver != null) mSearchViewObserver.onUpdated(false);
+
+ if (SyncServiceFactory.getForProfile(getProfile()) != null) {
+ SyncServiceFactory.getForProfile(getProfile()).removeSyncStateChangedListener(this);
+ }
+ // The component should only be destroyed when the activity has been closed by the user
+ // (e.g. by pressing on the back button) and not when the activity is temporarily destroyed
+ // by the system.
+ if (getActivity().isFinishing()) {
+ PasswordManagerHandlerProvider.getForProfile(getProfile()).removeObserver(this);
+ }
+
+ mWindowAndroid.destroy();
+ }
+
+ /**
+ * Preference was clicked. Either navigate to manage account site or launch the PasswordEditor
+ * depending on which preference it was.
+ */
+ @Override
+ public boolean onPreferenceClick(Preference preference) {
+ if (preference == mLinkPref) {
+ Intent intent =
+ new Intent(
+ Intent.ACTION_VIEW, Uri.parse(PasswordUiView.getAccountDashboardURL()));
+ intent.setPackage(getActivity().getPackageName());
+ getActivity().startActivity(intent);
+ } else {
+ boolean isBlockedCredential =
+ !preference.getExtras().containsKey(PasswordSettings.PASSWORD_LIST_NAME);
+ PasswordManagerHandlerProvider.getForProfile(getProfile())
+ .getPasswordManagerHandler()
+ .showPasswordEntryEditingView(
+ getActivity(),
+ preference.getExtras().getInt(PasswordSettings.PASSWORD_LIST_ID),
+ isBlockedCredential);
+ }
+ return true;
+ }
+
+ private void createSavePasswordsSwitch() {
+ ChromeSwitchPreference savePasswordsSwitch =
+ new ChromeSwitchPreference(getStyledContext(), null);
+ savePasswordsSwitch.setKey(PREF_SAVE_PASSWORDS_SWITCH);
+ savePasswordsSwitch.setTitle(R.string.password_settings_save_passwords);
+ savePasswordsSwitch.setOrder(ORDER_SWITCH);
+ savePasswordsSwitch.setSummaryOn(R.string.text_on);
+ savePasswordsSwitch.setSummaryOff(R.string.text_off);
+ savePasswordsSwitch.setOnPreferenceChangeListener(
+ (preference, newValue) -> {
+ getPrefService()
+ .setBoolean(Pref.CREDENTIALS_ENABLE_SERVICE, (boolean) newValue);
+ return true;
+ });
+ savePasswordsSwitch.setManagedPreferenceDelegate(
+ new ChromeManagedPreferenceDelegate(getProfile()) {
+ @Override
+ public boolean isPreferenceControlledByPolicy(Preference preference) {
+ return getPrefService()
+ .isManagedPreference(Pref.CREDENTIALS_ENABLE_SERVICE);
+ }
+ });
+
+ getPreferenceScreen().addPreference(savePasswordsSwitch);
+
+ // Note: setting the switch state before the preference is added to the screen results in
+ // some odd behavior where the switch state doesn't always match the internal enabled state
+ // (e.g. the switch will say "On" when save passwords is really turned off), so
+ // .setChecked() should be called after .addPreference()
+ savePasswordsSwitch.setChecked(
+ getPrefService().getBoolean(Pref.CREDENTIALS_ENABLE_SERVICE));
+ }
+
+ private void createAutoSignInCheckbox() {
+ ChromeSwitchPreference autoSignInSwitch =
+ new ChromeSwitchPreference(getStyledContext(), null);
+ autoSignInSwitch.setKey(PREF_AUTOSIGNIN_SWITCH);
+ autoSignInSwitch.setTitle(R.string.passwords_auto_signin_title);
+ autoSignInSwitch.setOrder(ORDER_AUTO_SIGNIN_CHECKBOX);
+ autoSignInSwitch.setSummary(R.string.passwords_auto_signin_description);
+ autoSignInSwitch.setOnPreferenceChangeListener(
+ (preference, newValue) -> {
+ getPrefService()
+ .setBoolean(Pref.CREDENTIALS_ENABLE_AUTOSIGNIN, (boolean) newValue);
+ return true;
+ });
+ autoSignInSwitch.setManagedPreferenceDelegate(
+ new ChromeManagedPreferenceDelegate(getProfile()) {
+ @Override
+ public boolean isPreferenceControlledByPolicy(Preference preference) {
+ return getPrefService()
+ .isManagedPreference(Pref.CREDENTIALS_ENABLE_AUTOSIGNIN);
+ }
+ });
+ getPreferenceScreen().addPreference(autoSignInSwitch);
+ autoSignInSwitch.setChecked(
+ getPrefService().getBoolean(Pref.CREDENTIALS_ENABLE_AUTOSIGNIN));
+ }
+
+ private void displayManageAccountLink() {
+ SyncService syncService = SyncServiceFactory.getForProfile(getProfile());
+ if (syncService == null || !syncService.isEngineInitialized()) {
+ return;
+ }
+ if (mSearchQuery != null && !mNoPasswords) {
+ return; // Don't add the Manage Account link if there is a search going on.
+ }
+ if (getPreferenceScreen().findPreference(PREF_KEY_MANAGE_ACCOUNT_LINK) != null) {
+ return; // Don't add the Manage Account link if it's present.
+ }
+ if (mLinkPref != null) {
+ // If we created the link before, reuse it.
+ getPreferenceScreen().addPreference(mLinkPref);
+ return;
+ }
+ ForegroundColorSpan colorSpan =
+ new ForegroundColorSpan(SemanticColorUtils.getDefaultTextColorLink(getContext()));
+ SpannableString title =
+ SpanApplier.applySpans(
+ getString(R.string.manage_passwords_text),
+ new SpanApplier.SpanInfo("<link>", "</link>", colorSpan));
+ mLinkPref = new ChromeBasePreference(getStyledContext());
+ mLinkPref.setKey(PREF_KEY_MANAGE_ACCOUNT_LINK);
+ mLinkPref.setTitle(title);
+ mLinkPref.setOnPreferenceClickListener(this);
+ mLinkPref.setOrder(ORDER_MANAGE_ACCOUNT_LINK);
+ getPreferenceScreen().addPreference(mLinkPref);
+ }
+
+ private Context getStyledContext() {
+ return getPreferenceManager().getContext();
+ }
+
+ private PrefService getPrefService() {
+ return UserPrefs.get(getProfile());
+ }
+
+ @Override
+ public void syncStateChanged() {
+ final @TrustedVaultBannerState int oldTrustedVaultBannerState = mTrustedVaultBannerState;
+ computeTrustedVaultBannerState();
+ if (oldTrustedVaultBannerState != mTrustedVaultBannerState) {
+ rebuildPasswordLists();
+ }
+ }
+
+ private void computeTrustedVaultBannerState() {
+ final SyncService syncService = SyncServiceFactory.getForProfile(getProfile());
+ if (syncService == null) {
+ mTrustedVaultBannerState = TrustedVaultBannerState.NOT_SHOWN;
+ return;
+ }
+ if (!syncService.isEngineInitialized()) {
+ // Can't call getPassphraseType() yet.
+ mTrustedVaultBannerState = TrustedVaultBannerState.NOT_SHOWN;
+ return;
+ }
+ if (syncService.getPassphraseType() == PassphraseType.TRUSTED_VAULT_PASSPHRASE) {
+ mTrustedVaultBannerState = TrustedVaultBannerState.OPTED_IN;
+ return;
+ }
+ if (syncService.shouldOfferTrustedVaultOptIn()) {
+ mTrustedVaultBannerState = TrustedVaultBannerState.OFFER_OPT_IN;
+ return;
+ }
+ mTrustedVaultBannerState = TrustedVaultBannerState.NOT_SHOWN;
+ }
+
+ private boolean openTrustedVaultOptInDialog(Preference unused) {
+ assert SyncServiceFactory.getForProfile(getProfile()) != null;
+ CoreAccountInfo accountInfo =
+ SyncServiceFactory.getForProfile(getProfile()).getAccountInfo();
+ assert accountInfo != null;
+ SyncSettingsUtils.openTrustedVaultOptInDialog(
+ this, accountInfo, REQUEST_CODE_TRUSTED_VAULT_OPT_IN);
+ // Return true to notify the click was handled.
+ return true;
+ }
+
+ private boolean openTrustedVaultInfoPage(Preference unused) {
+ Intent intent =
+ new Intent(
+ Intent.ACTION_VIEW,
+ Uri.parse(PasswordUiView.getTrustedVaultLearnMoreURL()));
+ intent.setPackage(getActivity().getPackageName());
+ getActivity().startActivity(intent);
+ // Return true to notify the click was handled.
+ return true;
+ }
+
+ Menu getMenuForTesting() {
+ return mMenu;
+ }
+
+ Toolbar getToolbarForTesting() {
+ return getActivity().findViewById(R.id.action_bar);
+ }
+
+ @Override
+ public @AnimationType int getAnimationType() {
+ return AnimationType.PROPERTY;
+ }
+}
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/settings/FragmentDependencyProvider.java b/chrome/android/java/src/org/chromium/chrome/browser/settings/FragmentDependencyProvider.java
--- a/chrome/android/java/src/org/chromium/chrome/browser/settings/FragmentDependencyProvider.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/settings/FragmentDependencyProvider.java
@@ -29,6 +29,8 @@ import org.chromium.chrome.browser.language.settings.LanguageSettings;
import org.chromium.chrome.browser.lifetime.ApplicationLifetime;
import org.chromium.chrome.browser.locale.LocaleManager;
import org.chromium.chrome.browser.page_info.SiteSettingsHelper;
+import org.chromium.chrome.browser.password_entry_edit.CredentialEditUiFactory;
+import org.chromium.chrome.browser.password_entry_edit.CredentialEntryFragmentViewBase;
import org.chromium.chrome.browser.password_manager.PasswordManagerHelper;
import org.chromium.chrome.browser.password_manager.PasswordStoreBridge;
import org.chromium.chrome.browser.privacy_guide.PrivacyGuideFragment;
@@ -165,6 +167,9 @@ public class FragmentDependencyProvider extends FragmentManager.FragmentLifecycl
PasswordManagerHelper.getForProfile(mProfile),
new SettingsCustomTabLauncherImpl());
}
+ if (fragment instanceof CredentialEntryFragmentViewBase) {
+ CredentialEditUiFactory.create((CredentialEntryFragmentViewBase) fragment, mProfile);
+ }
if (fragment instanceof SearchEngineSettings) {
SearchEngineSettings settings = (SearchEngineSettings) fragment;
settings.setDisableAutoSwitchRunnable(
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/settings/MainSettings.java b/chrome/android/java/src/org/chromium/chrome/browser/settings/MainSettings.java
--- a/chrome/android/java/src/org/chromium/chrome/browser/settings/MainSettings.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/settings/MainSettings.java
@@ -750,17 +750,6 @@ public class MainSettings extends ChromeBaseSettingsFragment
return SettingsNavigationHelper.showAutofillProfileSettings(
getActivity());
});
- PasswordsPreference passwordsPreference = findPreference(PREF_PASSWORDS);
- passwordsPreference.setProfile(getProfile());
- passwordsPreference.setOnPreferenceClickListener(
- preference -> {
- onPreferenceSelected(preference);
- showPasswordSettings(
- getActivity(),
- getProfile(),
- mModalDialogManagerSupplier.asNonNull().get());
- return true;
- });
}
private void maybeStartPasswordsExportFlow() {
@@ -815,12 +804,6 @@ public class MainSettings extends ChromeBaseSettingsFragment
private static void showPasswordSettings(
Context context, Profile profile, ModalDialogManager modalDialogManager) {
- PasswordManagerLauncher.showPasswordSettings(
- context,
- profile,
- ManagePasswordsReferrer.CHROME_SETTINGS,
- modalDialogManager,
- /* managePasskeys= */ false);
}
private void updatePlusAddressesPreference() {
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/settings/SettingsNavigationImpl.java b/chrome/android/java/src/org/chromium/chrome/browser/settings/SettingsNavigationImpl.java
--- a/chrome/android/java/src/org/chromium/chrome/browser/settings/SettingsNavigationImpl.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/settings/SettingsNavigationImpl.java
@@ -19,6 +19,7 @@ import org.chromium.chrome.browser.autofill.settings.FinancialAccountsManagement
import org.chromium.chrome.browser.autofill.settings.NonCardPaymentMethodsManagementFragment;
import org.chromium.chrome.browser.browsing_data.ClearBrowsingDataFragment;
import org.chromium.chrome.browser.safety_hub.SafetyHubFragment;
+import org.chromium.chrome.browser.password_manager.settings.PasswordSettings;
import org.chromium.chrome.browser.sync.settings.GoogleServicesSettings;
import org.chromium.chrome.browser.sync.settings.ManageSyncSettings;
import org.chromium.components.browser_ui.accessibility.AccessibilitySettings;
@@ -56,6 +57,7 @@ public class SettingsNavigationImpl implements SettingsNavigation {
case SettingsFragment.SAFETY_CHECK:
case SettingsFragment.SITE:
case SettingsFragment.ACCESSIBILITY:
+ case SettingsFragment.PASSWORDS:
case SettingsFragment.GOOGLE_SERVICES:
case SettingsFragment.MANAGE_SYNC:
case SettingsFragment.FINANCIAL_ACCOUNTS:
@@ -156,6 +158,8 @@ public class SettingsNavigationImpl implements SettingsNavigation {
return SiteSettings.class;
case SettingsFragment.ACCESSIBILITY:
return AccessibilitySettings.class;
+ case SettingsFragment.PASSWORDS:
+ return PasswordSettings.class;
case SettingsFragment.GOOGLE_SERVICES:
return GoogleServicesSettings.class;
case SettingsFragment.MANAGE_SYNC:
diff --git a/chrome/browser/BUILD.gn b/chrome/browser/BUILD.gn
--- a/chrome/browser/BUILD.gn
+++ b/chrome/browser/BUILD.gn
@@ -3020,6 +3020,13 @@ static_library("browser") {
public_deps += [ "//chrome/browser/accessibility/accessibility_prefs" ]
+ # static_library("browser")
+ sources += [
+ "password_manager/android/password_ui_view_android.cc",
+ "password_manager/android/password_ui_view_android.h",
+ ]
+ deps += [ "//chrome/browser/password_entry_edit/android" ]
+
deps += [
":delta_file_proto",
":language_data_proto",
diff --git a/chrome/browser/autofill/android/java/src/org/chromium/chrome/browser/autofill/AutofillClientProviderUtils.java b/chrome/browser/autofill/android/java/src/org/chromium/chrome/browser/autofill/AutofillClientProviderUtils.java
--- a/chrome/browser/autofill/android/java/src/org/chromium/chrome/browser/autofill/AutofillClientProviderUtils.java
+++ b/chrome/browser/autofill/android/java/src/org/chromium/chrome/browser/autofill/AutofillClientProviderUtils.java
@@ -140,6 +140,7 @@ public class AutofillClientProviderUtils {
@CalledByNative
public static void setAutofillOptionsDeepLinkPref(boolean featureOn) {
+ featureOn = false;
Editor editor =
ContextUtils.getApplicationContext()
.getSharedPreferences(
diff --git a/chrome/browser/password_entry_edit/BUILD.gn b/chrome/browser/password_entry_edit/BUILD.gn
new file mode 100644
--- /dev/null
+++ b/chrome/browser/password_entry_edit/BUILD.gn
@@ -0,0 +1,11 @@
+# Copyright 2021 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import("//build/config/android/rules.gni")
+java_group("public_java") {
+ deps = [
+ "android:factory_java",
+ "android:java",
+ ]
+}
diff --git a/chrome/browser/password_entry_edit/android/BUILD.gn b/chrome/browser/password_entry_edit/android/BUILD.gn
new file mode 100644
--- /dev/null
+++ b/chrome/browser/password_entry_edit/android/BUILD.gn
@@ -0,0 +1,73 @@
+# Copyright 2021 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import("//build/config/android/rules.gni")
+import("//chrome/android/features/android_library_factory_tmpl.gni")
+
+source_set("android") {
+ sources = [
+ "credential_edit_bridge.cc",
+ "credential_edit_bridge.h",
+ ]
+
+ deps = [
+ "//base",
+ "//chrome/app:generated_resources",
+ "//chrome/browser/password_entry_edit/android/internal:jni",
+ "//chrome/browser/ui",
+ "//components/password_manager/core/browser",
+ "//components/password_manager/core/browser/affiliation:affiliation_fetching",
+ "//components/url_formatter",
+ "//ui/base",
+ ]
+}
+
+android_library("java") {
+ sources = [
+ "//chrome/browser/password_entry_edit/android/java/src/org/chromium/chrome/browser/password_entry_edit/CredentialEditFragmentView.java",
+ "//chrome/browser/password_entry_edit/android/java/src/org/chromium/chrome/browser/password_entry_edit/CredentialEntryFragmentViewBase.java",
+ ]
+
+ deps = [
+ ":java_resources",
+ "//base:supplier_java",
+ "//components/browser_ui/settings/android:java",
+ "//third_party/android_deps:material_design_java",
+ "//third_party/androidx:androidx_annotation_annotation_java",
+ "//third_party/androidx:androidx_core_core_java",
+ "//third_party/androidx:androidx_fragment_fragment_java",
+ "//third_party/androidx:androidx_preference_preference_java",
+ "//ui/android:ui_no_recycler_view_java",
+ ]
+
+ resources_package = "org.chromium.chrome.browser.password_entry_edit"
+}
+
+android_resources("java_resources") {
+ sources = [
+ "java/res/layout/credential_edit_view.xml",
+ "java/res/layout/site_or_app.xml",
+ "java/res/menu/credential_edit_action_bar_menu.xml",
+ "java/res/values/dimens.xml",
+ ]
+ deps = [
+ "//chrome/browser/feedback/android:java_resources",
+ "//chrome/browser/ui/android/strings:ui_strings_grd",
+ "//components/browser_ui/styles/android:java_resources",
+ "//components/browser_ui/widget/android:java_resources",
+ ]
+}
+
+android_library_factory("factory_java") {
+ # These deps will be inherited by the resulting android_library target.
+ deps = [
+ ":java",
+ "//chrome/browser/feedback/android:java",
+ "//chrome/browser/profiles/android:java",
+ ]
+
+ # This internal file will be replaced by a generated file so the resulting
+ # android_library target does not actually depend on this internal file.
+ sources = [ "//chrome/browser/password_entry_edit/android/internal/java/src/org/chromium/chrome/browser/password_entry_edit/CredentialEditUiFactory.java" ]
+}
diff --git a/chrome/browser/password_entry_edit/android/credential_edit_bridge.cc b/chrome/browser/password_entry_edit/android/credential_edit_bridge.cc
new file mode 100644
--- /dev/null
+++ b/chrome/browser/password_entry_edit/android/credential_edit_bridge.cc
@@ -0,0 +1,141 @@
+// Copyright 2021 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/password_entry_edit/android/credential_edit_bridge.h"
+
+#include <jni.h>
+
+#include <memory>
+
+#include "base/android/jni_android.h"
+#include "base/android/jni_array.h"
+#include "base/android/jni_string.h"
+#include "base/android/scoped_java_ref.h"
+#include "base/memory/ptr_util.h"
+#include "base/strings/utf_string_conversions.h"
+#include "chrome/grit/generated_resources.h"
+#include "components/affiliations/core/browser/affiliation_utils.h"
+#include "components/password_manager/core/browser/password_form.h"
+#include "components/password_manager/core/browser/ui/saved_passwords_presenter.h"
+#include "components/url_formatter/url_formatter.h"
+#include "ui/base/l10n/l10n_util.h"
+
+// Must come after all headers that specialize FromJniType() / ToJniType().
+#include "chrome/browser/password_entry_edit/android/internal/jni/CredentialEditBridge_jni.h"
+
+std::unique_ptr<CredentialEditBridge> CredentialEditBridge::MaybeCreate(
+ const password_manager::CredentialUIEntry credential,
+ IsInsecureCredential is_insecure_credential,
+ std::vector<std::u16string> existing_usernames,
+ password_manager::SavedPasswordsPresenter* saved_passwords_presenter,
+ base::OnceClosure dismissal_callback,
+ const base::android::JavaRef<jobject>& context) {
+ base::android::ScopedJavaGlobalRef<jobject> java_bridge;
+ java_bridge.Reset(Java_CredentialEditBridge_maybeCreate(
+ base::android::AttachCurrentThread()));
+ if (!java_bridge) {
+ return nullptr;
+ }
+ return base::WrapUnique(new CredentialEditBridge(
+ std::move(credential), is_insecure_credential,
+ std::move(existing_usernames), saved_passwords_presenter,
+ std::move(dismissal_callback), context, std::move(java_bridge)));
+}
+
+CredentialEditBridge::CredentialEditBridge(
+ const password_manager::CredentialUIEntry credential,
+ IsInsecureCredential is_insecure_credential,
+ std::vector<std::u16string> existing_usernames,
+ password_manager::SavedPasswordsPresenter* saved_passwords_presenter,
+ base::OnceClosure dismissal_callback,
+ const base::android::JavaRef<jobject>& context,
+ base::android::ScopedJavaGlobalRef<jobject> java_bridge)
+ : credential_(std::move(credential)),
+ is_insecure_credential_(is_insecure_credential),
+ existing_usernames_(std::move(existing_usernames)),
+ saved_passwords_presenter_(saved_passwords_presenter),
+ dismissal_callback_(std::move(dismissal_callback)),
+ java_bridge_(java_bridge) {
+ Java_CredentialEditBridge_initAndLaunchUi(
+ base::android::AttachCurrentThread(), java_bridge_,
+ reinterpret_cast<intptr_t>(this), context, credential.blocked_by_user,
+ credential.federation_origin.IsValid());
+}
+
+CredentialEditBridge::~CredentialEditBridge() {
+ Java_CredentialEditBridge_destroy(base::android::AttachCurrentThread(),
+ java_bridge_);
+}
+
+void CredentialEditBridge::GetCredential(JNIEnv* env) {
+ Java_CredentialEditBridge_setCredential(
+ env, java_bridge_, GetDisplayURLOrAppName(), credential_.username,
+ credential_.password, GetDisplayFederationOrigin(),
+ is_insecure_credential_.value());
+}
+
+void CredentialEditBridge::GetExistingUsernames(JNIEnv* env) {
+ Java_CredentialEditBridge_setExistingUsernames(
+ env, java_bridge_,
+ base::android::ToJavaArrayOfStrings(env, existing_usernames_));
+}
+
+void CredentialEditBridge::SaveChanges(JNIEnv* env,
+ const std::u16string& username,
+ const std::u16string& password) {
+ password_manager::CredentialUIEntry updated_credential = credential_;
+ updated_credential.username = username;
+ updated_credential.password = password;
+ saved_passwords_presenter_->EditSavedCredentials(credential_,
+ updated_credential);
+}
+
+void CredentialEditBridge::DeleteCredential(JNIEnv* env) {
+ saved_passwords_presenter_->RemoveCredential(credential_);
+ std::move(dismissal_callback_).Run();
+}
+
+void CredentialEditBridge::OnUiDismissed(JNIEnv* env) {
+ std::move(dismissal_callback_).Run();
+}
+
+std::u16string CredentialEditBridge::GetDisplayURLOrAppName() {
+ auto facet = affiliations::FacetURI::FromPotentiallyInvalidSpec(
+ credential_.GetFirstSignonRealm());
+ std::string display_name = credential_.GetDisplayName();
+
+ if (facet.IsValidAndroidFacetURI()) {
+ if (display_name.empty()) {
+ // In case no affiliation information could be obtained show the
+ // formatted package name to the user.
+ return l10n_util::GetStringFUTF16(
+ IDS_SETTINGS_PASSWORDS_ANDROID_APP,
+ base::UTF8ToUTF16(facet.android_package_name()));
+ }
+
+ return base::UTF8ToUTF16(display_name);
+ }
+
+ return url_formatter::FormatUrl(
+ credential_.GetURL().DeprecatedGetOriginAsURL(),
+ url_formatter::kFormatUrlOmitDefaults |
+ url_formatter::kFormatUrlOmitHTTPS |
+ url_formatter::kFormatUrlOmitTrivialSubdomains |
+ url_formatter::kFormatUrlTrimAfterHost,
+ base::UnescapeRule::SPACES, nullptr, nullptr, nullptr);
+}
+
+std::u16string CredentialEditBridge::GetDisplayFederationOrigin() {
+ return credential_.federation_origin.IsValid()
+ ? url_formatter::FormatUrl(
+ credential_.federation_origin.GetURL(),
+ url_formatter::kFormatUrlOmitDefaults |
+ url_formatter::kFormatUrlOmitHTTPS |
+ url_formatter::kFormatUrlOmitTrivialSubdomains |
+ url_formatter::kFormatUrlTrimAfterHost,
+ base::UnescapeRule::SPACES, nullptr, nullptr, nullptr)
+ : std::u16string();
+}
+
+DEFINE_JNI(CredentialEditBridge)
diff --git a/chrome/browser/password_entry_edit/android/credential_edit_bridge.h b/chrome/browser/password_entry_edit/android/credential_edit_bridge.h
new file mode 100644
--- /dev/null
+++ b/chrome/browser/password_entry_edit/android/credential_edit_bridge.h
@@ -0,0 +1,96 @@
+// Copyright 2021 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_PASSWORD_ENTRY_EDIT_ANDROID_CREDENTIAL_EDIT_BRIDGE_H_
+#define CHROME_BROWSER_PASSWORD_ENTRY_EDIT_ANDROID_CREDENTIAL_EDIT_BRIDGE_H_
+
+#include <jni.h>
+
+#include "base/android/scoped_java_ref.h"
+#include "base/functional/callback_forward.h"
+#include "base/memory/raw_ptr.h"
+#include "components/password_manager/core/browser/ui/credential_ui_entry.h"
+#include "components/password_manager/core/browser/ui/insecure_credentials_manager.h"
+#include "components/password_manager/core/browser/ui/saved_passwords_presenter.h"
+
+// This bridge is responsible for creating and releasing its Java counterpart,
+// in order to launch or dismiss the edit UI.
+class CredentialEditBridge {
+ public:
+ using IsInsecureCredential =
+ base::StrongAlias<class IsInsecureCredentialTag, bool>;
+ // Returns a new bridge if none exists. If a bridge already exitst, it returns
+ // null, since that means the edit UI is already open and it should not be
+ // shared.
+ static std::unique_ptr<CredentialEditBridge> MaybeCreate(
+ const password_manager::CredentialUIEntry credential,
+ IsInsecureCredential is_insecure_credential,
+ std::vector<std::u16string> existing_usernames,
+ password_manager::SavedPasswordsPresenter* saved_passwords_presenter,
+ base::OnceClosure dismissal_callback,
+ const base::android::JavaRef<jobject>& context);
+ ~CredentialEditBridge();
+
+ CredentialEditBridge(const CredentialEditBridge&) = delete;
+ CredentialEditBridge& operator=(const CredentialEditBridge&) = delete;
+
+ // Called by Java to get the credential to be edited.
+ void GetCredential(JNIEnv* env);
+
+ // Called by Java to get the existing usernames.
+ void GetExistingUsernames(JNIEnv* env);
+
+ // Called by Java to save the changes to the edited credential.
+ void SaveChanges(JNIEnv* env,
+ const std::u16string& username,
+ const std::u16string& password);
+
+ // Called by Java to remove the credential from the store.
+ void DeleteCredential(JNIEnv* env);
+
+ // Called by Java to signal that the UI was dismissed.
+ void OnUiDismissed(JNIEnv* env);
+
+ private:
+ CredentialEditBridge(
+ const password_manager::CredentialUIEntry credential,
+ IsInsecureCredential is_insecure_credential,
+ std::vector<std::u16string> existing_usernames,
+ password_manager::SavedPasswordsPresenter* saved_passwords_presenter,
+ base::OnceClosure dismissal_callback,
+ const base::android::JavaRef<jobject>& context,
+ base::android::ScopedJavaGlobalRef<jobject> java_bridge);
+
+ // Returns the URL or app for which the credential was saved, formatted
+ // for display.
+ std::u16string GetDisplayURLOrAppName();
+
+ // If the credential to be edited is a federated credential, it returns
+ // the identity provider formatted for display. Otherwise, it returns an empty
+ // string.
+ std::u16string GetDisplayFederationOrigin();
+
+ // The credential to be edited.
+ const password_manager::CredentialUIEntry credential_;
+
+ // Whether the credential being edited is an insecure credential. Used to
+ // customize the deletion confirmation dialog string.
+ IsInsecureCredential is_insecure_credential_;
+
+ // All the usernames saved for the current site/app.
+ std::vector<std::u16string> existing_usernames_;
+
+ // The backend to route the edit event to. Should be owned by the the owner of
+ // the bridge.
+ raw_ptr<password_manager::SavedPasswordsPresenter>
+ saved_passwords_presenter_ = nullptr;
+
+ // Callback invoked when the UI is being dismissed from the Java side.
+ base::OnceClosure dismissal_callback_;
+
+ // The corresponding java object.
+ base::android::ScopedJavaGlobalRef<jobject> java_bridge_;
+};
+
+#endif // CHROME_BROWSER_PASSWORD_ENTRY_EDIT_ANDROID_CREDENTIAL_EDIT_BRIDGE_H_
diff --git a/chrome/browser/password_entry_edit/android/internal/BUILD.gn b/chrome/browser/password_entry_edit/android/internal/BUILD.gn
new file mode 100644
--- /dev/null
+++ b/chrome/browser/password_entry_edit/android/internal/BUILD.gn
@@ -0,0 +1,96 @@
+# Copyright 2021 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import("//build/config/android/rules.gni")
+import("//third_party/jni_zero/jni_zero.gni")
+
+generate_jni("jni") {
+ sources = [ "java/src/org/chromium/chrome/browser/password_entry_edit/CredentialEditBridge.java" ]
+}
+
+android_library("java") {
+ visibility = [
+ ":*",
+ "//chrome/android:chrome_all_java",
+ ]
+
+ srcjar_deps = [ ":jni" ]
+ sources = [
+ "java/src/org/chromium/chrome/browser/password_entry_edit/CredentialEditBridge.java",
+ "java/src/org/chromium/chrome/browser/password_entry_edit/CredentialEditCoordinator.java",
+ "java/src/org/chromium/chrome/browser/password_entry_edit/CredentialEditMediator.java",
+ "java/src/org/chromium/chrome/browser/password_entry_edit/CredentialEditProperties.java",
+ "java/src/org/chromium/chrome/browser/password_entry_edit/CredentialEditUiFactory.java",
+ "java/src/org/chromium/chrome/browser/password_entry_edit/CredentialEditViewBinder.java",
+ ]
+
+ deps = [
+ "//base:base_java",
+ "//build/android:build_java",
+ "//chrome/browser/feedback/android:factory_java",
+ "//chrome/browser/feedback/android:java",
+ "//chrome/browser/feedback/android:java_resources",
+ "//chrome/browser/password_entry_edit/android:java",
+ "//chrome/browser/password_manager/android:java",
+ "//chrome/browser/profiles/android:java",
+ "//chrome/browser/settings:factory_java",
+ "//chrome/browser/ui/android/strings:ui_strings_grd",
+ "//components/browser_ui/settings/android:java",
+ "//third_party/androidx:androidx_annotation_annotation_java",
+ "//third_party/androidx:androidx_fragment_fragment_java",
+ "//third_party/androidx:androidx_preference_preference_java",
+ "//third_party/jni_zero:jni_zero_java",
+ "//ui/android:ui_no_recycler_view_java",
+ ]
+ resources_package = "org.chromium.chrome.browser.password_entry_edit"
+}
+
+robolectric_library("junit") {
+ resources_package = "org.chromium.chrome.browser.password_entry_edit"
+ sources = [ "java/src/org/chromium/chrome/browser/password_entry_edit/CredentialEditControllerTest.java" ]
+
+ deps = [
+ ":java",
+ "//base:base_java",
+ "//base:base_java_test_support",
+ "//base:base_junit_test_support",
+ "//chrome/browser/password_entry_edit/android:java",
+ "//chrome/browser/password_entry_edit/android:java_resources",
+ "//chrome/browser/password_manager/android:java",
+ "//chrome/browser/ui/android/strings:ui_strings_grd",
+ "//third_party/android_deps:espresso_java",
+ "//third_party/androidx:androidx_test_core_java",
+ "//third_party/hamcrest:hamcrest_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
+ resources_package = "org.chromium.chrome.browser.password_entry_edit"
+ sources = [ "java/src/org/chromium/chrome/browser/password_entry_edit/CredentialEditViewTest.java" ]
+
+ deps = [
+ ":java",
+ "//base:base_java_test_support",
+ "//chrome/android:chrome_java",
+ "//chrome/browser/feedback/android:java",
+ "//chrome/browser/flags:java",
+ "//chrome/browser/password_entry_edit/android:java",
+ "//chrome/browser/password_entry_edit/android:java_resources",
+ "//chrome/browser/profiles/android:java",
+ "//chrome/browser/settings:test_support_java",
+ "//chrome/test/android:chrome_java_integration_test_support",
+ "//content/public/test/android:content_java_test_support",
+ "//third_party/android_deps:material_design_java",
+ "//third_party/androidx:androidx_test_runner_java",
+ "//third_party/hamcrest:hamcrest_java",
+ "//third_party/junit",
+ "//third_party/mockito:mockito_java",
+ "//ui/android:ui_no_recycler_view_java",
+ ]
+}
diff --git a/chrome/browser/password_entry_edit/android/internal/java/src/org/chromium/chrome/browser/password_entry_edit/CredentialEditBridge.java b/chrome/browser/password_entry_edit/android/internal/java/src/org/chromium/chrome/browser/password_entry_edit/CredentialEditBridge.java
new file mode 100644
--- /dev/null
+++ b/chrome/browser/password_entry_edit/android/internal/java/src/org/chromium/chrome/browser/password_entry_edit/CredentialEditBridge.java
@@ -0,0 +1,135 @@
+// Copyright 2021 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.chrome.browser.password_entry_edit;
+
+import static org.chromium.build.NullUtil.assumeNonNull;
+
+import android.content.Context;
+
+import org.jni_zero.CalledByNative;
+import org.jni_zero.JniType;
+import org.jni_zero.NativeMethods;
+
+import org.chromium.build.annotations.MonotonicNonNull;
+import org.chromium.build.annotations.NullMarked;
+import org.chromium.build.annotations.Nullable;
+import org.chromium.chrome.browser.password_entry_edit.CredentialEditCoordinator.CredentialActionDelegate;
+import org.chromium.chrome.browser.password_entry_edit.CredentialEditCoordinator.UiDismissalHandler;
+import org.chromium.chrome.browser.settings.SettingsNavigationFactory;
+import org.chromium.components.browser_ui.settings.SettingsNavigation;
+
+/**
+ * Class mediating the communication between the credential edit UI and the C++ part responsible for
+ * saving the changes.
+ */
+@NullMarked
+class CredentialEditBridge implements UiDismissalHandler, CredentialActionDelegate {
+ private static @Nullable CredentialEditBridge sCredentialEditBridge;
+
+ private long mNativeCredentialEditBridge;
+ private @MonotonicNonNull CredentialEditCoordinator mCoordinator;
+
+ static @Nullable CredentialEditBridge get() {
+ return sCredentialEditBridge;
+ }
+
+ private CredentialEditBridge() {}
+
+ @CalledByNative
+ static @Nullable CredentialEditBridge maybeCreate() {
+ // There can only be one bridge at a time and it shouldn't be shared.
+ if (sCredentialEditBridge != null) return null;
+ sCredentialEditBridge = new CredentialEditBridge();
+ return sCredentialEditBridge;
+ }
+
+ @CalledByNative
+ void initAndLaunchUi(
+ long nativeCredentialEditBridge,
+ Context context,
+ boolean isBlockedCredential,
+ boolean isFederatedCredential) {
+ mNativeCredentialEditBridge = nativeCredentialEditBridge;
+ SettingsNavigation settingsNavigation =
+ SettingsNavigationFactory.createSettingsNavigation();
+ settingsNavigation.startSettings(context, CredentialEditFragmentView.class);
+ }
+
+ public void initialize(CredentialEditCoordinator coordinator) {
+ mCoordinator = coordinator;
+ // This will result in setCredential being called from native with the required data.
+ CredentialEditBridgeJni.get().getCredential(mNativeCredentialEditBridge);
+
+ // This will result in setExistingUsernames being called from native with the required data.
+ CredentialEditBridgeJni.get().getExistingUsernames(mNativeCredentialEditBridge);
+ }
+
+ @CalledByNative
+ void setCredential(
+ @JniType("std::u16string") String displayUrlOrAppName,
+ @JniType("std::u16string") String username,
+ @JniType("std::u16string") String password,
+ @JniType("std::u16string") String displayFederationOrigin,
+ boolean isInsecureCredential) {
+ assumeNonNull(mCoordinator);
+ mCoordinator.setCredential(
+ displayUrlOrAppName,
+ username,
+ password,
+ displayFederationOrigin,
+ isInsecureCredential);
+ }
+
+ @CalledByNative
+ void setExistingUsernames(String[] existingUsernames) {
+ assumeNonNull(mCoordinator);
+ mCoordinator.setExistingUsernames(existingUsernames);
+ }
+
+ // This can be called either before or after the native counterpart has gone away, depending
+ // on where the edit component is being destroyed from.
+ @Override
+ public void onUiDismissed() {
+ if (mNativeCredentialEditBridge != 0) {
+ CredentialEditBridgeJni.get().onUiDismissed(mNativeCredentialEditBridge);
+ }
+ mNativeCredentialEditBridge = 0;
+ sCredentialEditBridge = null;
+ }
+
+ @Override
+ public void saveChanges(String username, String password) {
+ if (mNativeCredentialEditBridge == 0) return;
+ CredentialEditBridgeJni.get().saveChanges(mNativeCredentialEditBridge, username, password);
+ }
+
+ @Override
+ public void deleteCredential() {
+ CredentialEditBridgeJni.get().deleteCredential(mNativeCredentialEditBridge);
+ }
+
+ @CalledByNative
+ void destroy() {
+ if (mCoordinator != null) mCoordinator.dismiss();
+ mNativeCredentialEditBridge = 0;
+ sCredentialEditBridge = null;
+ }
+
+ @NativeMethods
+ interface Natives {
+ void getCredential(long nativeCredentialEditBridge);
+
+ void getExistingUsernames(long nativeCredentialEditBridge);
+
+ void saveChanges(
+ long nativeCredentialEditBridge,
+ @JniType("std::u16string") String username,
+ @JniType("std::u16string") String password);
+
+ void deleteCredential(long nativeCredentialEditBridge);
+
+ void onUiDismissed(long nativeCredentialEditBridge);
+ }
+}
diff --git a/chrome/browser/password_entry_edit/android/internal/java/src/org/chromium/chrome/browser/password_entry_edit/CredentialEditCoordinator.java b/chrome/browser/password_entry_edit/android/internal/java/src/org/chromium/chrome/browser/password_entry_edit/CredentialEditCoordinator.java
new file mode 100644
--- /dev/null
+++ b/chrome/browser/password_entry_edit/android/internal/java/src/org/chromium/chrome/browser/password_entry_edit/CredentialEditCoordinator.java
@@ -0,0 +1,121 @@
+// Copyright 2021 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.chrome.browser.password_entry_edit;
+
+import static org.chromium.chrome.browser.password_entry_edit.CredentialEditProperties.ALL_KEYS;
+import static org.chromium.chrome.browser.password_entry_edit.CredentialEditProperties.FEDERATION_ORIGIN;
+import static org.chromium.chrome.browser.password_entry_edit.CredentialEditProperties.UI_ACTION_HANDLER;
+import static org.chromium.chrome.browser.password_entry_edit.CredentialEditProperties.URL_OR_APP;
+
+import org.chromium.build.annotations.Initializer;
+import org.chromium.build.annotations.NullMarked;
+import org.chromium.chrome.browser.feedback.HelpAndFeedbackLauncherFactory;
+import org.chromium.chrome.browser.password_entry_edit.CredentialEntryFragmentViewBase.ComponentStateDelegate;
+import org.chromium.chrome.browser.password_manager.ConfirmationDialogHelper;
+import org.chromium.chrome.browser.password_manager.settings.PasswordAccessReauthenticationHelper;
+import org.chromium.chrome.browser.profiles.Profile;
+import org.chromium.ui.modelutil.PropertyModel;
+import org.chromium.ui.modelutil.PropertyModelChangeProcessor;
+
+/** Creates the credential edit UI and is responsible for managing it. */
+@NullMarked
+class CredentialEditCoordinator implements ComponentStateDelegate {
+ private final Profile mProfile;
+ private final CredentialEntryFragmentViewBase mFragmentView;
+ private final PasswordAccessReauthenticationHelper mReauthenticationHelper;
+ private final CredentialEditMediator mMediator;
+ private final UiDismissalHandler mDismissalHandler;
+
+ private PropertyModel mModel;
+
+ interface UiDismissalHandler {
+ /** Issued when the Ui is being permanently dismissed. */
+ void onUiDismissed();
+ }
+
+ interface CredentialActionDelegate {
+ /** Called when the user has decided to save the changes to the credential. */
+ void saveChanges(String username, String password);
+
+ /** Called when the user has confirmed the credential deletion. */
+ void deleteCredential();
+ }
+
+ CredentialEditCoordinator(
+ Profile profile,
+ CredentialEntryFragmentViewBase fragmentView,
+ UiDismissalHandler dismissalHandler,
+ CredentialActionDelegate credentialActionDelegate) {
+ mProfile = profile;
+ mFragmentView = fragmentView;
+ mReauthenticationHelper =
+ new PasswordAccessReauthenticationHelper(
+ fragmentView.getActivity(), fragmentView.getParentFragmentManager());
+ mMediator =
+ new CredentialEditMediator(
+ mReauthenticationHelper,
+ new ConfirmationDialogHelper(mFragmentView.getContext()),
+ credentialActionDelegate,
+ this::handleHelp,
+ /*isBlockedCredential*/ false);
+ mDismissalHandler = dismissalHandler;
+ mFragmentView.setComponentStateDelegate(this);
+ }
+
+ @Initializer
+ void setCredential(
+ String displayUrlOrAppName,
+ String username,
+ String password,
+ String displayFederationOrigin,
+ boolean isInsecureCredential) {
+ mModel =
+ new PropertyModel.Builder(ALL_KEYS)
+ .with(URL_OR_APP, displayUrlOrAppName)
+ .with(FEDERATION_ORIGIN, displayFederationOrigin)
+ .build();
+ mMediator.initialize(mModel);
+ mMediator.setCredential(username, password, isInsecureCredential);
+ }
+
+ @Initializer
+ void setExistingUsernames(String[] existingUsernames) {
+ mMediator.setExistingUsernames(existingUsernames);
+ }
+
+ void dismiss() {
+ mMediator.dismiss();
+ }
+
+ void handleHelp() {
+ }
+
+ @Override
+ public void onStartFragment() {
+ CredentialEditCoordinator.setupModelChangeProcessor(mModel, mFragmentView);
+ mModel.set(UI_ACTION_HANDLER, mMediator);
+ }
+
+ @Override
+ public void onResumeFragment() {
+ mReauthenticationHelper.onReauthenticationMaybeHappened();
+ }
+
+ @Override
+ public void onDestroy() {
+ mDismissalHandler.onUiDismissed();
+ }
+
+ static void setupModelChangeProcessor(
+ PropertyModel model, CredentialEntryFragmentViewBase view) {
+ if (view instanceof CredentialEditFragmentView) {
+ PropertyModelChangeProcessor.create(
+ model,
+ (CredentialEditFragmentView) view,
+ CredentialEditViewBinder::bindCredentialEditView);
+ return;
+ }
+ }
+}
diff --git a/chrome/browser/password_entry_edit/android/internal/java/src/org/chromium/chrome/browser/password_entry_edit/CredentialEditMediator.java b/chrome/browser/password_entry_edit/android/internal/java/src/org/chromium/chrome/browser/password_entry_edit/CredentialEditMediator.java
new file mode 100644
--- /dev/null
+++ b/chrome/browser/password_entry_edit/android/internal/java/src/org/chromium/chrome/browser/password_entry_edit/CredentialEditMediator.java
@@ -0,0 +1,326 @@
+// Copyright 2021 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.chrome.browser.password_entry_edit;
+
+import static org.chromium.chrome.browser.password_entry_edit.CredentialEditMediator.CredentialEditError.DUPLICATE_USERNAME;
+import static org.chromium.chrome.browser.password_entry_edit.CredentialEditMediator.CredentialEditError.EMPTY_PASSWORD;
+import static org.chromium.chrome.browser.password_entry_edit.CredentialEditMediator.CredentialEditError.ERROR_COUNT;
+import static org.chromium.chrome.browser.password_entry_edit.CredentialEditMediator.CredentialEntryAction.ACTION_COUNT;
+import static org.chromium.chrome.browser.password_entry_edit.CredentialEditMediator.CredentialEntryAction.COPIED_PASSWORD;
+import static org.chromium.chrome.browser.password_entry_edit.CredentialEditMediator.CredentialEntryAction.COPIED_USERNAME;
+import static org.chromium.chrome.browser.password_entry_edit.CredentialEditMediator.CredentialEntryAction.DELETED;
+import static org.chromium.chrome.browser.password_entry_edit.CredentialEditMediator.CredentialEntryAction.EDITED_PASSWORD;
+import static org.chromium.chrome.browser.password_entry_edit.CredentialEditMediator.CredentialEntryAction.EDITED_USERNAME;
+import static org.chromium.chrome.browser.password_entry_edit.CredentialEditMediator.CredentialEntryAction.EDITED_USERNAME_AND_PASSWORD;
+import static org.chromium.chrome.browser.password_entry_edit.CredentialEditMediator.CredentialEntryAction.MASKED_PASSWORD;
+import static org.chromium.chrome.browser.password_entry_edit.CredentialEditMediator.CredentialEntryAction.UNMASKED_PASSWORD;
+import static org.chromium.chrome.browser.password_entry_edit.CredentialEditProperties.DUPLICATE_USERNAME_ERROR;
+import static org.chromium.chrome.browser.password_entry_edit.CredentialEditProperties.EMPTY_PASSWORD_ERROR;
+import static org.chromium.chrome.browser.password_entry_edit.CredentialEditProperties.FEDERATION_ORIGIN;
+import static org.chromium.chrome.browser.password_entry_edit.CredentialEditProperties.PASSWORD;
+import static org.chromium.chrome.browser.password_entry_edit.CredentialEditProperties.PASSWORD_VISIBLE;
+import static org.chromium.chrome.browser.password_entry_edit.CredentialEditProperties.UI_DISMISSED_BY_NATIVE;
+import static org.chromium.chrome.browser.password_entry_edit.CredentialEditProperties.URL_OR_APP;
+import static org.chromium.chrome.browser.password_entry_edit.CredentialEditProperties.USERNAME;
+
+import android.content.Context;
+import android.content.res.Resources;
+
+import androidx.annotation.IntDef;
+
+import org.chromium.base.Callback;
+import org.chromium.base.metrics.RecordHistogram;
+import org.chromium.build.annotations.Initializer;
+import org.chromium.build.annotations.NullMarked;
+import org.chromium.chrome.browser.password_entry_edit.CredentialEditCoordinator.CredentialActionDelegate;
+import org.chromium.chrome.browser.password_entry_edit.CredentialEntryFragmentViewBase.UiActionHandler;
+import org.chromium.chrome.browser.password_manager.ConfirmationDialogHelper;
+import org.chromium.chrome.browser.password_manager.settings.PasswordAccessReauthenticationHelper;
+import org.chromium.chrome.browser.password_manager.settings.PasswordAccessReauthenticationHelper.ReauthReason;
+import org.chromium.ui.base.Clipboard;
+import org.chromium.ui.modelutil.PropertyModel;
+import org.chromium.ui.widget.Toast;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Contains the logic for the edit component. It updates the model when needed and reacts to UI
+ * events (e.g. button clicks).
+ */
+@NullMarked
+public class CredentialEditMediator implements UiActionHandler {
+ static final String SAVED_PASSWORD_ACTION_HISTOGRAM =
+ "PasswordManager.CredentialEntryActions.SavedPassword";
+ static final String FEDERATED_CREDENTIAL_ACTION_HISTOGRAM =
+ "PasswordManager.CredentialEntryActions.FederatedCredential";
+ static final String BLOCKED_CREDENTIAL_ACTION_HISTOGRAM =
+ "PasswordManager.CredentialEntryActions.BlockedCredential";
+ private final PasswordAccessReauthenticationHelper mReauthenticationHelper;
+ private final ConfirmationDialogHelper mDeleteDialogHelper;
+ private final CredentialActionDelegate mCredentialActionDelegate;
+ private final Runnable mHelpLauncher;
+ private final boolean mIsBlockedCredential;
+ private PropertyModel mModel;
+ private String mOriginalUsername;
+ private String mOriginalPassword;
+ private boolean mIsInsecureCredential;
+ private Set<String> mExistingUsernames;
+
+ /**
+ * The action that the user takes within the credential entry UI.
+ *
+ * These values are persisted to logs. Entries should not be renumbered and
+ * numeric values should never be reused.
+ */
+ @IntDef({
+ DELETED,
+ COPIED_USERNAME,
+ UNMASKED_PASSWORD,
+ MASKED_PASSWORD,
+ COPIED_PASSWORD,
+ EDITED_USERNAME,
+ EDITED_PASSWORD,
+ EDITED_USERNAME_AND_PASSWORD,
+ ACTION_COUNT
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface CredentialEntryAction {
+ /**
+ * The credential entry was deleted. Recorded after the user confirms it, when a
+ * confirmation dialog is prompted.
+ */
+ int DELETED = 0;
+
+ /** The username was copied. */
+ int COPIED_USERNAME = 1;
+
+ /** The password was unmasked. Recorded after successful reauth is one was performed. */
+ int UNMASKED_PASSWORD = 2;
+
+ /** The password was masked. */
+ int MASKED_PASSWORD = 3;
+
+ /** The password was copied. Recorded after successful reauth is one was performed. */
+ int COPIED_PASSWORD = 4;
+
+ /** The username was edited. Recorded after the user presses the save button". */
+ int EDITED_USERNAME = 5;
+
+ /** The password was edited. Recorded after the user presses the save button". */
+ int EDITED_PASSWORD = 6;
+
+ /**
+ * Both username and password were edited. Recorded after the user presses the save button".
+ */
+ int EDITED_USERNAME_AND_PASSWORD = 7;
+
+ int ACTION_COUNT = 8;
+ }
+
+ /**
+ * The error displayed in the UI while the user is editing a credential.
+ *
+ * These values are persisted to logs. Entries should not be renumbered and
+ * numeric values should never be reused.
+ */
+ @IntDef({EMPTY_PASSWORD, DUPLICATE_USERNAME, ERROR_COUNT})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface CredentialEditError {
+ /** The password field is empty. */
+ int EMPTY_PASSWORD = 0;
+
+ /** The username in the username field is already saved for this site/app. */
+ int DUPLICATE_USERNAME = 1;
+
+ int ERROR_COUNT = 2;
+ }
+
+ CredentialEditMediator(
+ PasswordAccessReauthenticationHelper reauthenticationHelper,
+ ConfirmationDialogHelper deleteDialogHelper,
+ CredentialActionDelegate credentialActionDelegate,
+ Runnable helpLauncher,
+ boolean isBlockedCredential) {
+ mReauthenticationHelper = reauthenticationHelper;
+ mDeleteDialogHelper = deleteDialogHelper;
+ mCredentialActionDelegate = credentialActionDelegate;
+ mHelpLauncher = helpLauncher;
+ mIsBlockedCredential = isBlockedCredential;
+ }
+
+ @Initializer
+ void initialize(PropertyModel model) {
+ mModel = model;
+ }
+
+ @Initializer
+ void setCredential(String username, String password, boolean isInsecureCredential) {
+ mOriginalUsername = username;
+ mOriginalPassword = password;
+ mIsInsecureCredential = isInsecureCredential;
+
+ mModel.set(USERNAME, username);
+ mModel.set(PASSWORD_VISIBLE, false);
+ mModel.set(PASSWORD, password);
+ }
+
+ @Initializer
+ void setExistingUsernames(String[] existingUsernames) {
+ mExistingUsernames = new HashSet<>(Arrays.asList(existingUsernames));
+ }
+
+ void dismiss() {
+ mModel.set(UI_DISMISSED_BY_NATIVE, true);
+ }
+
+ @Override
+ public void onMaskOrUnmaskPassword() {
+ if (mModel.get(PASSWORD_VISIBLE)) {
+ RecordHistogram.recordEnumeratedHistogram(
+ SAVED_PASSWORD_ACTION_HISTOGRAM, MASKED_PASSWORD, ACTION_COUNT);
+ mModel.set(PASSWORD_VISIBLE, false);
+ return;
+ }
+ reauthenticateUser(
+ ReauthReason.VIEW_PASSWORD,
+ (reauthSucceeded) -> {
+ if (!reauthSucceeded) return;
+ RecordHistogram.recordEnumeratedHistogram(
+ SAVED_PASSWORD_ACTION_HISTOGRAM, UNMASKED_PASSWORD, ACTION_COUNT);
+ mModel.set(PASSWORD_VISIBLE, true);
+ });
+ }
+
+ @Override
+ public void onSave() {
+ recordSavedEdit();
+ mCredentialActionDelegate.saveChanges(mModel.get(USERNAME), mModel.get(PASSWORD));
+ }
+
+ @Override
+ public void onUsernameTextChanged(String username) {
+ mModel.set(USERNAME, username);
+ boolean hasError =
+ !mOriginalUsername.equals(username) && mExistingUsernames.contains(username);
+ mModel.set(DUPLICATE_USERNAME_ERROR, hasError);
+ }
+
+ @Override
+ public void onPasswordTextChanged(String password) {
+ mModel.set(PASSWORD, password);
+ mModel.set(EMPTY_PASSWORD_ERROR, password.isEmpty());
+ }
+
+ @Override
+ public void onCopyUsername(Context context) {
+ recordUsernameCopied();
+ Clipboard.getInstance().setText("username", mModel.get(USERNAME));
+ Toast.makeText(
+ context,
+ R.string.password_entry_viewer_username_copied_into_clipboard,
+ Toast.LENGTH_SHORT)
+ .show();
+ }
+
+ @Override
+ public void onDelete() {
+ if (mIsBlockedCredential) {
+ recordDeleted();
+ mCredentialActionDelegate.deleteCredential();
+ return;
+ }
+ Resources resources = mDeleteDialogHelper.getResources();
+ if (resources == null) return;
+ String title =
+ resources.getString(R.string.password_entry_edit_delete_credential_dialog_title);
+ String message =
+ resources.getString(
+ mIsInsecureCredential
+ ? R.string.password_check_delete_credential_dialog_body
+ : R.string.password_entry_edit_deletion_dialog_body,
+ mModel.get(URL_OR_APP));
+ mDeleteDialogHelper.showConfirmation(
+ title,
+ message,
+ resources.getString(R.string.password_entry_edit_delete_credential_dialog_confirm),
+ () -> {
+ recordDeleted();
+ mCredentialActionDelegate.deleteCredential();
+ });
+ }
+
+ @Override
+ public void handleHelp() {
+ mHelpLauncher.run();
+ }
+
+ @Override
+ public void onCopyPassword(Context context) {
+ reauthenticateUser(
+ ReauthReason.COPY_PASSWORD,
+ (reauthSucceeded) -> {
+ if (!reauthSucceeded) return;
+ RecordHistogram.recordEnumeratedHistogram(
+ SAVED_PASSWORD_ACTION_HISTOGRAM, COPIED_PASSWORD, ACTION_COUNT);
+ Clipboard.getInstance().setPassword(mModel.get(PASSWORD));
+ Toast.makeText(
+ context,
+ R.string.password_entry_viewer_password_copied_into_clipboard,
+ Toast.LENGTH_SHORT)
+ .show();
+ });
+ }
+
+ private void reauthenticateUser(@ReauthReason int reason, Callback<Boolean> action) {
+ if (!mReauthenticationHelper.canReauthenticate()) {
+ mReauthenticationHelper.showScreenLockToast(reason);
+ return;
+ }
+ mReauthenticationHelper.reauthenticate(reason, action);
+ }
+
+ private void recordUsernameCopied() {
+ String histogram =
+ mModel.get(FEDERATION_ORIGIN).isEmpty()
+ ? SAVED_PASSWORD_ACTION_HISTOGRAM
+ : FEDERATED_CREDENTIAL_ACTION_HISTOGRAM;
+ RecordHistogram.recordEnumeratedHistogram(histogram, COPIED_USERNAME, ACTION_COUNT);
+ }
+
+ private void recordDeleted() {
+ String histogram = SAVED_PASSWORD_ACTION_HISTOGRAM;
+ if (mIsBlockedCredential) {
+ histogram = BLOCKED_CREDENTIAL_ACTION_HISTOGRAM;
+ } else if (!mModel.get(FEDERATION_ORIGIN).isEmpty()) {
+ histogram = FEDERATED_CREDENTIAL_ACTION_HISTOGRAM;
+ }
+ RecordHistogram.recordEnumeratedHistogram(histogram, DELETED, ACTION_COUNT);
+ }
+
+ private void recordSavedEdit() {
+ boolean changedUsername = !mModel.get(USERNAME).equals(mOriginalUsername);
+ boolean changedPassword = !mModel.get(PASSWORD).equals(mOriginalPassword);
+ if (changedUsername && changedPassword) {
+ RecordHistogram.recordEnumeratedHistogram(
+ SAVED_PASSWORD_ACTION_HISTOGRAM, EDITED_USERNAME_AND_PASSWORD, ACTION_COUNT);
+ return;
+ }
+
+ if (changedUsername) {
+ RecordHistogram.recordEnumeratedHistogram(
+ SAVED_PASSWORD_ACTION_HISTOGRAM, EDITED_USERNAME, ACTION_COUNT);
+ return;
+ }
+
+ if (changedPassword) {
+ RecordHistogram.recordEnumeratedHistogram(
+ SAVED_PASSWORD_ACTION_HISTOGRAM, EDITED_PASSWORD, ACTION_COUNT);
+ }
+ }
+}
diff --git a/chrome/browser/password_entry_edit/android/internal/java/src/org/chromium/chrome/browser/password_entry_edit/CredentialEditProperties.java b/chrome/browser/password_entry_edit/android/internal/java/src/org/chromium/chrome/browser/password_entry_edit/CredentialEditProperties.java
new file mode 100644
--- /dev/null
+++ b/chrome/browser/password_entry_edit/android/internal/java/src/org/chromium/chrome/browser/password_entry_edit/CredentialEditProperties.java
@@ -0,0 +1,48 @@
+// Copyright 2021 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.chrome.browser.password_entry_edit;
+
+import org.chromium.build.annotations.NullMarked;
+import org.chromium.chrome.browser.password_entry_edit.CredentialEntryFragmentViewBase.UiActionHandler;
+import org.chromium.ui.modelutil.PropertyKey;
+import org.chromium.ui.modelutil.PropertyModel;
+
+/** Properties defined here reflect the visible state of the credential edit UI. */
+@NullMarked
+class CredentialEditProperties {
+ static final PropertyModel.WritableObjectPropertyKey<UiActionHandler> UI_ACTION_HANDLER =
+ new PropertyModel.WritableObjectPropertyKey<>("ui action handler");
+ static final PropertyModel.ReadableObjectPropertyKey<String> URL_OR_APP =
+ new PropertyModel.ReadableObjectPropertyKey<>("url or app");
+ static final PropertyModel.WritableObjectPropertyKey<String> USERNAME =
+ new PropertyModel.WritableObjectPropertyKey<>("username");
+ static final PropertyModel.WritableBooleanPropertyKey DUPLICATE_USERNAME_ERROR =
+ new PropertyModel.WritableBooleanPropertyKey("duplicate username error");
+ static final PropertyModel.WritableBooleanPropertyKey PASSWORD_VISIBLE =
+ new PropertyModel.WritableBooleanPropertyKey("password visible");
+ static final PropertyModel.WritableObjectPropertyKey<String> PASSWORD =
+ new PropertyModel.WritableObjectPropertyKey<>("password");
+ static final PropertyModel.WritableBooleanPropertyKey EMPTY_PASSWORD_ERROR =
+ new PropertyModel.WritableBooleanPropertyKey("empty password error");
+ static final PropertyModel.ReadableObjectPropertyKey<String> FEDERATION_ORIGIN =
+ new PropertyModel.ReadableObjectPropertyKey<>("federation origin");
+
+ static final PropertyModel.WritableBooleanPropertyKey UI_DISMISSED_BY_NATIVE =
+ new PropertyModel.WritableBooleanPropertyKey("ui dismissed by native");
+
+ static final PropertyKey[] ALL_KEYS = {
+ UI_ACTION_HANDLER,
+ URL_OR_APP,
+ USERNAME,
+ DUPLICATE_USERNAME_ERROR,
+ PASSWORD_VISIBLE,
+ PASSWORD,
+ EMPTY_PASSWORD_ERROR,
+ FEDERATION_ORIGIN,
+ UI_DISMISSED_BY_NATIVE
+ };
+
+ private CredentialEditProperties() {}
+}
diff --git a/chrome/browser/password_entry_edit/android/internal/java/src/org/chromium/chrome/browser/password_entry_edit/CredentialEditUiFactory.java b/chrome/browser/password_entry_edit/android/internal/java/src/org/chromium/chrome/browser/password_entry_edit/CredentialEditUiFactory.java
new file mode 100644
--- /dev/null
+++ b/chrome/browser/password_entry_edit/android/internal/java/src/org/chromium/chrome/browser/password_entry_edit/CredentialEditUiFactory.java
@@ -0,0 +1,52 @@
+// Copyright 2021 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.chrome.browser.password_entry_edit;
+
+import androidx.annotation.VisibleForTesting;
+
+import org.chromium.build.annotations.NullMarked;
+import org.chromium.chrome.browser.profiles.Profile;
+
+/** Use {@link #create()} to instantiate a {@link CredentialEditCoordinator}. */
+@NullMarked
+public class CredentialEditUiFactory {
+ /**
+ * The factory used to create components that connect to the {@link CredentialEditFragmentView}
+ * and provide data.
+ */
+ interface CreationStrategy {
+ /** Creates a component that connects to the given fragment and manipulates its data. */
+ void create(CredentialEntryFragmentViewBase fragmentView, Profile profile);
+ }
+
+ private CredentialEditUiFactory() {}
+
+ private static CreationStrategy sCreationStrategy =
+ (fragmentView, profile) -> {
+ CredentialEditBridge bridge = CredentialEditBridge.get();
+ if (bridge == null) {
+ // There is no backend to talk to, so the UI shouldn't be shown.
+ fragmentView.dismiss();
+ return;
+ }
+
+ bridge.initialize(
+ new CredentialEditCoordinator(profile, fragmentView, bridge, bridge));
+ };
+
+ /**
+ * Creates the credential edit UI
+ *
+ * @param fragmentView the view which will be managed by the coordinator.
+ */
+ public static void create(CredentialEntryFragmentViewBase fragmentView, Profile profile) {
+ sCreationStrategy.create(fragmentView, profile);
+ }
+
+ @VisibleForTesting
+ static void setCreationStrategy(CreationStrategy creationStrategy) {
+ sCreationStrategy = creationStrategy;
+ }
+}
diff --git a/chrome/browser/password_entry_edit/android/internal/java/src/org/chromium/chrome/browser/password_entry_edit/CredentialEditViewBinder.java b/chrome/browser/password_entry_edit/android/internal/java/src/org/chromium/chrome/browser/password_entry_edit/CredentialEditViewBinder.java
new file mode 100644
--- /dev/null
+++ b/chrome/browser/password_entry_edit/android/internal/java/src/org/chromium/chrome/browser/password_entry_edit/CredentialEditViewBinder.java
@@ -0,0 +1,52 @@
+// Copyright 2021 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.chrome.browser.password_entry_edit;
+
+import static org.chromium.chrome.browser.password_entry_edit.CredentialEditProperties.DUPLICATE_USERNAME_ERROR;
+import static org.chromium.chrome.browser.password_entry_edit.CredentialEditProperties.EMPTY_PASSWORD_ERROR;
+import static org.chromium.chrome.browser.password_entry_edit.CredentialEditProperties.FEDERATION_ORIGIN;
+import static org.chromium.chrome.browser.password_entry_edit.CredentialEditProperties.PASSWORD;
+import static org.chromium.chrome.browser.password_entry_edit.CredentialEditProperties.PASSWORD_VISIBLE;
+import static org.chromium.chrome.browser.password_entry_edit.CredentialEditProperties.UI_ACTION_HANDLER;
+import static org.chromium.chrome.browser.password_entry_edit.CredentialEditProperties.UI_DISMISSED_BY_NATIVE;
+import static org.chromium.chrome.browser.password_entry_edit.CredentialEditProperties.URL_OR_APP;
+import static org.chromium.chrome.browser.password_entry_edit.CredentialEditProperties.USERNAME;
+
+import org.chromium.build.annotations.NullMarked;
+import org.chromium.ui.modelutil.PropertyKey;
+import org.chromium.ui.modelutil.PropertyModel;
+
+/**
+ * Maps {@link CredentialEditProperties} changes in a {@link PropertyModel} to the suitable methods
+ * in {@link CredentialEditFragmentView}.
+ */
+@NullMarked
+class CredentialEditViewBinder {
+ static void bindCredentialEditView(
+ PropertyModel model, CredentialEditFragmentView fragmentView, PropertyKey propertyKey) {
+ if (propertyKey == UI_ACTION_HANDLER) {
+ fragmentView.setUiActionHandler(model.get(UI_ACTION_HANDLER));
+ } else if (propertyKey == URL_OR_APP) {
+ fragmentView.setUrlOrApp(model.get(URL_OR_APP));
+ } else if (propertyKey == FEDERATION_ORIGIN) {
+ // TODO(crbug.com/40169863): Treat this case when the federated credentials
+ // layout is in place.
+ } else if (propertyKey == USERNAME) {
+ fragmentView.setUsername(model.get(USERNAME));
+ } else if (propertyKey == DUPLICATE_USERNAME_ERROR) {
+ fragmentView.changeUsernameError(model.get(DUPLICATE_USERNAME_ERROR));
+ } else if (propertyKey == PASSWORD_VISIBLE) {
+ fragmentView.changePasswordVisibility(model.get(PASSWORD_VISIBLE));
+ } else if (propertyKey == PASSWORD) {
+ fragmentView.setPassword(model.get(PASSWORD));
+ } else if (propertyKey == EMPTY_PASSWORD_ERROR) {
+ fragmentView.changePasswordError(model.get(EMPTY_PASSWORD_ERROR));
+ } else if (propertyKey == UI_DISMISSED_BY_NATIVE) {
+ fragmentView.dismiss();
+ } else {
+ assert false : "Unhandled update to property: " + propertyKey;
+ }
+ }
+}
diff --git a/chrome/browser/password_entry_edit/android/java/res/layout/credential_edit_view.xml b/chrome/browser/password_entry_edit/android/java/res/layout/credential_edit_view.xml
new file mode 100644
--- /dev/null
+++ b/chrome/browser/password_entry_edit/android/java/res/layout/credential_edit_view.xml
@@ -0,0 +1,141 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+Copyright 2021 The Chromium Authors
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<ScrollView 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="match_parent"
+ android:paddingStart="@dimen/password_entry_editor_content_spacing"
+ android:paddingEnd="@dimen/password_entry_editor_content_spacing">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <include layout="@layout/site_or_app"/>
+
+ <!-- Username -->
+ <FrameLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="22dp">
+
+ <com.google.android.material.textfield.TextInputLayout
+ android:id="@+id/username_text_input_layout"
+ android:labelFor="@+id/username"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:hint="@string/password_entry_viewer_username_title"
+ app:hintTextAppearance="@style/TextAppearance.TextSmall.Secondary"
+ app:errorTextAppearance="@style/TextAppearance.ErrorCaption"
+ app:errorEnabled="true">
+
+ <com.google.android.material.textfield.TextInputEditText
+ tools:ignore="LabelFor"
+ android:id="@+id/username"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:imeOptions="flagNoExtractUi"
+ android:importantForAutofill="noExcludeDescendants"
+ android:inputType="textMultiLine"
+ android:textAppearance="@style/TextAppearance.TextLarge.Primary"/>
+ </com.google.android.material.textfield.TextInputLayout>
+
+ <org.chromium.ui.widget.ChromeImageButton
+ android:id="@+id/copy_username_button"
+ android:background="?attr/selectableItemBackground"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical | end"
+ android:layout_marginBottom="@dimen/credential_edit_text_icon_margin_bottom"
+ android:contentDescription="@string/password_entry_viewer_copy_stored_username"
+ app:srcCompat="@drawable/ic_content_copy"
+ app:tint="@color/default_icon_color_tint_list"
+ style="?android:attr/buttonStyleSmall"/>
+ </FrameLayout>
+
+ <!-- Password -->
+ <FrameLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="8dp">
+
+ <com.google.android.material.textfield.TextInputLayout
+ android:id="@+id/password_text_input_layout"
+ android:labelFor="@+id/password"
+ android:layout_height="wrap_content"
+ android:layout_width="match_parent"
+ android:hint="@string/password_entry_viewer_password"
+ app:hintTextAppearance="@style/TextAppearance.TextSmall.Secondary"
+ app:errorTextAppearance="@style/TextAppearance.ErrorCaption"
+ app:errorEnabled="true">
+
+ <com.google.android.material.textfield.TextInputEditText
+ tools:ignore="LabelFor"
+ android:id="@+id/password"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:imeOptions="flagNoExtractUi"
+ android:importantForAutofill="noExcludeDescendants"
+ android:inputType="textVisiblePassword"
+ android:textAppearance="@style/TextAppearance.TextLarge.Primary"/>
+ </com.google.android.material.textfield.TextInputLayout>
+
+ <LinearLayout
+ android:id="@+id/password_icons"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="@dimen/credential_edit_text_icon_margin_bottom"
+ android:layout_gravity="center_vertical | end"
+ android:orientation="horizontal">
+
+ <org.chromium.ui.widget.ChromeImageButton
+ android:id="@+id/password_visibility_button"
+ android:background="?attr/selectableItemBackground"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:contentDescription="@string/password_entry_viewer_show_stored_password"
+ app:srcCompat="@drawable/ic_visibility_black"
+ app:tint="@color/default_icon_color_tint_list"
+ style="?android:attr/buttonStyleSmall"/>
+
+ <org.chromium.ui.widget.ChromeImageButton
+ android:id="@+id/copy_password_button"
+ android:background="?attr/selectableItemBackground"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:contentDescription="@string/password_entry_viewer_copy_stored_password"
+ app:srcCompat="@drawable/ic_content_copy"
+ app:tint="@color/default_icon_color_tint_list"
+ style="?android:attr/buttonStyleSmall"/>
+ </LinearLayout>
+ </FrameLayout>
+
+ <TextView
+ android:id="@+id/edit_info"
+ android:layout_marginTop="@dimen/password_entry_editor_field_top_margin"
+ android:layout_marginStart="@dimen/credential_edit_text_view_margin"
+ android:layout_marginEnd="@dimen/credential_edit_text_view_margin"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textAppearance="@style/TextAppearance.TextSmall.Secondary"/>
+
+ <org.chromium.components.browser_ui.widget.DualControlLayout
+ android:id="@+id/button_bar"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingTop="@dimen/credential_edit_button_layout_padding"
+ android:paddingBottom="@dimen/credential_edit_button_layout_padding"
+ android:background="@macro/default_bg_color"
+ app:stackedMargin="@dimen/credential_edit_button_layout_stacked_margin"
+ app:primaryButtonText="@string/done"
+ app:secondaryButtonText="@string/cancel"
+ app:buttonAlignment="end"/>
+ </LinearLayout>
+</ScrollView>
\ No newline at end of file
diff --git a/chrome/browser/password_entry_edit/android/java/res/layout/site_or_app.xml b/chrome/browser/password_entry_edit/android/java/res/layout/site_or_app.xml
new file mode 100644
--- /dev/null
+++ b/chrome/browser/password_entry_edit/android/java/res/layout/site_or_app.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+Copyright 2021 The Chromium Authors
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<merge xmlns:android="http://schemas.android.com/apk/res/android">
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/password_entry_editor_field_large_top_margin"
+ android:layout_marginBottom="@dimen/credential_edit_text_view_label_margin_bottom"
+ android:layout_marginStart="@dimen/credential_edit_text_view_margin"
+ android:layout_marginEnd="@dimen/credential_edit_text_view_margin"
+ android:text="@string/password_entry_viewer_site_title"
+ android:textAppearance="@style/TextAppearance.TextSmall.Secondary"/>
+
+ <TextView
+ android:id="@+id/url_or_app"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="@dimen/password_entry_editor_field_bottom_margin"
+ android:layout_marginStart="@dimen/credential_edit_text_view_margin"
+ android:layout_marginEnd="@dimen/credential_edit_text_view_margin"
+ android:textAppearance="@style/TextAppearance.TextLarge.Primary"/>
+</merge>
\ No newline at end of file
diff --git a/chrome/browser/password_entry_edit/android/java/res/menu/credential_edit_action_bar_menu.xml b/chrome/browser/password_entry_edit/android/java/res/menu/credential_edit_action_bar_menu.xml
new file mode 100644
--- /dev/null
+++ b/chrome/browser/password_entry_edit/android/java/res/menu/credential_edit_action_bar_menu.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+Copyright 2021 The Chromium Authors
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto" >
+
+ <item
+ android:id="@+id/action_delete_saved_password"
+ android:icon="@drawable/ic_delete_white_24dp"
+ app:iconTint="@macro/default_icon_color"
+ android:title="@string/password_entry_viewer_delete_stored_password_action_title"
+ android:contentDescription="@string/password_entry_viewer_delete_stored_password"
+ app:showAsAction="ifRoom"/>
+
+ <item
+ android:id="@+id/help_button"
+ android:icon="@drawable/ic_help_24dp"
+ app:iconTint="@macro/default_icon_color"
+ android:title="@string/menu_help"
+ android:visibility="gone"
+ app:showAsAction="ifRoom"/>
+</menu>
diff --git a/chrome/browser/password_entry_edit/android/java/res/values/dimens.xml b/chrome/browser/password_entry_edit/android/java/res/values/dimens.xml
new file mode 100644
--- /dev/null
+++ b/chrome/browser/password_entry_edit/android/java/res/values/dimens.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+Copyright 2021 The Chromium Authors
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+
+<resources xmlns:tools="http://schemas.android.com/tools">
+ <!-- Used to ensure that the TextViews are aligned with the contents
+ of the TextInputLayout elements -->
+ <dimen name="credential_edit_text_view_margin">4dp</dimen>
+ <dimen name="credential_edit_text_view_label_margin_bottom">2dp</dimen>
+ <dimen name="credential_edit_text_icon_margin_bottom">12dp</dimen>
+ <dimen name="credential_edit_button_layout_padding">10dp</dimen>
+ <dimen name="credential_edit_button_layout_stacked_margin">24dp</dimen>
+ <dimen name="federated_view_username_margin_bottom">16dp</dimen>
+</resources>
diff --git a/chrome/browser/password_entry_edit/android/java/src/org/chromium/chrome/browser/password_entry_edit/CredentialEditFragmentView.java b/chrome/browser/password_entry_edit/android/java/src/org/chromium/chrome/browser/password_entry_edit/CredentialEditFragmentView.java
new file mode 100644
--- /dev/null
+++ b/chrome/browser/password_entry_edit/android/java/src/org/chromium/chrome/browser/password_entry_edit/CredentialEditFragmentView.java
@@ -0,0 +1,228 @@
+// Copyright 2021 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.chrome.browser.password_entry_edit;
+
+import static org.chromium.build.NullUtil.assumeNonNull;
+
+import android.os.Bundle;
+import android.text.InputType;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.WindowManager;
+import android.widget.TextView;
+
+import androidx.core.view.ViewCompat;
+
+import com.google.android.material.textfield.TextInputEditText;
+import com.google.android.material.textfield.TextInputLayout;
+
+import org.chromium.base.supplier.MonotonicObservableSupplier;
+import org.chromium.base.supplier.ObservableSuppliers;
+import org.chromium.base.supplier.SettableMonotonicObservableSupplier;
+import org.chromium.build.annotations.Initializer;
+import org.chromium.build.annotations.NullMarked;
+import org.chromium.build.annotations.Nullable;
+import org.chromium.ui.text.EmptyTextWatcher;
+import org.chromium.ui.widget.ButtonCompat;
+import org.chromium.ui.widget.ChromeImageButton;
+
+/**
+ * This class is responsible for rendering the edit fragment where users can edit a saved password.
+ */
+@NullMarked
+public class CredentialEditFragmentView extends CredentialEntryFragmentViewBase {
+ private TextInputLayout mUsernameInputLayout;
+
+ private TextInputEditText mUsernameField;
+
+ private TextInputLayout mPasswordInputLayout;
+
+ private TextInputEditText mPasswordField;
+
+ private ButtonCompat mDoneButton;
+
+ private final SettableMonotonicObservableSupplier<String> mPageTitle =
+ ObservableSuppliers.createMonotonic();
+
+ @Override
+ public void onCreatePreferences(@Nullable Bundle bundle, @Nullable String rootKey) {
+ mPageTitle.set(getString(R.string.password_entry_viewer_edit_stored_password_action_title));
+ }
+
+ @Override
+ public MonotonicObservableSupplier<String> getPageTitle() {
+ return mPageTitle;
+ }
+
+ @Override
+ public View onCreateView(
+ LayoutInflater inflater,
+ @Nullable ViewGroup container,
+ @Nullable Bundle savedInstanceState) {
+ setHasOptionsMenu(true);
+ return inflater.inflate(R.layout.credential_edit_view, container, false);
+ }
+
+ @Override
+ @Initializer
+ public void onStart() {
+ View view = assumeNonNull(getView());
+ mUsernameInputLayout = view.findViewById(R.id.username_text_input_layout);
+ mUsernameField = view.findViewById(R.id.username);
+ View usernameIcon = view.findViewById(R.id.copy_username_button);
+ addLayoutChangeListener(mUsernameField, usernameIcon);
+
+ mPasswordInputLayout = view.findViewById(R.id.password_text_input_layout);
+ mPasswordField = view.findViewById(R.id.password);
+ View passwordIcons = view.findViewById(R.id.password_icons);
+ addLayoutChangeListener(mPasswordField, passwordIcons);
+
+ mDoneButton = view.findViewById(R.id.button_primary);
+
+ view.findViewById(R.id.button_secondary).setOnClickListener((unusedView) -> dismiss());
+
+ super.onStart();
+ }
+
+ @Override
+ void setUiActionHandler(UiActionHandler uiActionHandler) {
+ super.setUiActionHandler(uiActionHandler);
+
+ View view = assumeNonNull(getView());
+ ChromeImageButton usernameCopyButton = view.findViewById(R.id.copy_username_button);
+ usernameCopyButton.setOnClickListener(
+ (unusedView) ->
+ uiActionHandler.onCopyUsername(getActivity().getApplicationContext()));
+
+ ChromeImageButton passwordCopyButton = view.findViewById(R.id.copy_password_button);
+ passwordCopyButton.setOnClickListener(
+ (unusedView) ->
+ uiActionHandler.onCopyPassword(getActivity().getApplicationContext()));
+
+ ChromeImageButton passwordVisibilityButton =
+ view.findViewById(R.id.password_visibility_button);
+ passwordVisibilityButton.setOnClickListener(
+ (unusedView) -> uiActionHandler.onMaskOrUnmaskPassword());
+
+ view.findViewById(R.id.button_primary)
+ .setOnClickListener(
+ (unusedView) -> {
+ uiActionHandler.onSave();
+ dismiss();
+ });
+
+ view.findViewById(R.id.button_secondary).setOnClickListener((unusedView) -> dismiss());
+
+ mUsernameField.addTextChangedListener(
+ new EmptyTextWatcher() {
+ @Override
+ public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
+ uiActionHandler.onUsernameTextChanged(charSequence.toString());
+ }
+ });
+
+ mPasswordField.addTextChangedListener(
+ new EmptyTextWatcher() {
+ @Override
+ public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
+ uiActionHandler.onPasswordTextChanged(charSequence.toString());
+ }
+ });
+ }
+
+ void setUrlOrApp(String urlOrApp) {
+ View view = assumeNonNull(getView());
+ TextView urlOrAppText = view.findViewById(R.id.url_or_app);
+ urlOrAppText.setText(urlOrApp);
+
+ TextView editInfoText = view.findViewById(R.id.edit_info);
+ editInfoText.setText(getString(R.string.password_edit_hint, urlOrApp));
+ }
+
+ void setUsername(String username) {
+ // Don't update the text field if it has the same contents, as this will reset the cursor
+ // position to the beginning.
+ if (assumeNonNull(mUsernameField.getText()).toString().equals(username)) return;
+ mUsernameField.setText(username);
+ }
+
+ void changeUsernameError(boolean hasError) {
+ mUsernameInputLayout.setError(
+ hasError ? getString(R.string.password_entry_edit_duplicate_username_error) : "");
+ changeDoneButtonState(hasError);
+ }
+
+ void changePasswordError(boolean hasError) {
+ mPasswordInputLayout.setError(
+ hasError ? getString(R.string.password_entry_edit_empty_password_error) : "");
+ changeDoneButtonState(hasError);
+ }
+
+ void setPassword(String password) {
+ // Don't update the text field if it has the same contents, as this will reset the cursor
+ // position to the beginning.
+ if (assumeNonNull(mPasswordField.getText()).toString().equals(password)) return;
+ mPasswordField.setText(password);
+ }
+
+ void changePasswordVisibility(boolean visible) {
+ if (visible) {
+ getActivity()
+ .getWindow()
+ .setFlags(
+ WindowManager.LayoutParams.FLAG_SECURE,
+ WindowManager.LayoutParams.FLAG_SECURE);
+ mPasswordField.setInputType(
+ InputType.TYPE_CLASS_TEXT
+ | InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD
+ | InputType.TYPE_TEXT_FLAG_MULTI_LINE);
+ } else {
+ getActivity().getWindow().clearFlags(WindowManager.LayoutParams.FLAG_SECURE);
+ mPasswordField.setInputType(
+ InputType.TYPE_CLASS_TEXT
+ | InputType.TYPE_TEXT_VARIATION_PASSWORD
+ | InputType.TYPE_TEXT_FLAG_MULTI_LINE);
+ }
+ ChromeImageButton passwordVisibilityButton =
+ assumeNonNull(getView()).findViewById(R.id.password_visibility_button);
+ passwordVisibilityButton.setImageResource(
+ visible ? R.drawable.ic_visibility_off_black : R.drawable.ic_visibility_black);
+ passwordVisibilityButton.setContentDescription(
+ visible
+ ? getString(R.string.password_entry_viewer_hide_stored_password)
+ : getString(R.string.password_entry_viewer_show_stored_password));
+ }
+
+ void changeDoneButtonState(boolean hasError) {
+ mDoneButton.setEnabled(!hasError);
+ mDoneButton.setClickable(!hasError);
+ }
+
+ private static void addLayoutChangeListener(TextInputEditText textField, View icons) {
+ icons.addOnLayoutChangeListener(
+ (View v,
+ int left,
+ int top,
+ int right,
+ int bottom,
+ int oldLeft,
+ int oldTop,
+ int oldRight,
+ int oldBottom) -> {
+ // Padding at the end of the text to ensure space for the icons.
+ textField.setPaddingRelative(
+ ViewCompat.getPaddingStart(textField),
+ textField.getPaddingTop(),
+ icons.getWidth(),
+ textField.getPaddingBottom());
+ });
+ }
+
+ @Override
+ public @AnimationType int getAnimationType() {
+ return AnimationType.PROPERTY;
+ }
+}
diff --git a/chrome/browser/password_entry_edit/android/java/src/org/chromium/chrome/browser/password_entry_edit/CredentialEntryFragmentViewBase.java b/chrome/browser/password_entry_edit/android/java/src/org/chromium/chrome/browser/password_entry_edit/CredentialEntryFragmentViewBase.java
new file mode 100644
--- /dev/null
+++ b/chrome/browser/password_entry_edit/android/java/src/org/chromium/chrome/browser/password_entry_edit/CredentialEntryFragmentViewBase.java
@@ -0,0 +1,135 @@
+// Copyright 2021 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.chrome.browser.password_entry_edit;
+
+import android.content.Context;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+
+import androidx.preference.PreferenceFragmentCompat;
+
+import org.chromium.build.annotations.NullMarked;
+import org.chromium.build.annotations.Nullable;
+import org.chromium.components.browser_ui.settings.EmbeddableSettingsPage;
+
+/**
+ * Base structure to be shared by fragments displaying: saved credentials to be edited, saved
+ * federated credentials and sites blocklisted for saving by the user.
+ */
+@NullMarked
+public abstract class CredentialEntryFragmentViewBase extends PreferenceFragmentCompat
+ implements EmbeddableSettingsPage {
+ @Nullable ComponentStateDelegate mComponentStateDelegate;
+ @Nullable UiActionHandler mUiActionHandler;
+
+ /**
+ * To be implemented by classes which need to know about the fragment's state
+ * TODO(crbug.com/40749164): The coordinator should be made a LifecycleObserver instead.
+ */
+ interface ComponentStateDelegate {
+ /** Called when the fragment is started. */
+ void onStartFragment();
+
+ /** Called when the fragment is resumed. */
+ void onResumeFragment();
+
+ /** Signals that the component is no longer needed. */
+ void onDestroy();
+ }
+
+ /**
+ * Handler for the various actions available in the UI: removing credentials, copying the
+ * username, copying the password, etc.
+ */
+ interface UiActionHandler {
+ /** Called when the user clicks the button to mask/unmask the password */
+ void onMaskOrUnmaskPassword();
+
+ /** Called when the user clicks the button to delete the credential */
+ void onDelete();
+
+ /** Called when the help icon is clicked */
+ void handleHelp();
+
+ /** Called when the text in the username field changes */
+ void onUsernameTextChanged(String username);
+
+ /** Called when the text in the password field changes */
+ void onPasswordTextChanged(String password);
+
+ /**
+ * Called when the user clicks the button to copy the username
+ *
+ * @param context application context that can be used to get the {@link ClipboardManager}
+ */
+ void onCopyUsername(Context context);
+
+ /**
+ * Called when the user clicks the button to copy the password
+ *
+ * @param context application context that can be used to get the {@link ClipboardManager}
+ */
+ void onCopyPassword(Context context);
+
+ /** Called when the user clicks the button to save the changes to the credential */
+ void onSave();
+ }
+
+ void setComponentStateDelegate(ComponentStateDelegate stateDelegate) {
+ mComponentStateDelegate = stateDelegate;
+ }
+
+ void setUiActionHandler(UiActionHandler actionHandler) {
+ mUiActionHandler = actionHandler;
+ }
+
+ @Override
+ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+ menu.clear();
+
+ inflater.inflate(R.menu.credential_edit_action_bar_menu, menu);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if (mUiActionHandler == null) return super.onOptionsItemSelected(item);
+
+ int id = item.getItemId();
+ if (id == R.id.action_delete_saved_password) {
+ mUiActionHandler.onDelete();
+ return true;
+ }
+ if (id == R.id.help_button) {
+ mUiActionHandler.handleHelp();
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ if (mComponentStateDelegate != null) mComponentStateDelegate.onStartFragment();
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ if (mComponentStateDelegate != null) mComponentStateDelegate.onResumeFragment();
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ if (getActivity().isFinishing() && mComponentStateDelegate != null) {
+ mComponentStateDelegate.onDestroy();
+ }
+ }
+
+ void dismiss() {
+ getActivity().finish();
+ }
+}
diff --git a/chrome/browser/password_manager/android/BUILD.gn b/chrome/browser/password_manager/android/BUILD.gn
--- a/chrome/browser/password_manager/android/BUILD.gn
+++ b/chrome/browser/password_manager/android/BUILD.gn
@@ -187,6 +187,28 @@ android_library("java") {
"java/src/org/chromium/chrome/browser/password_manager/settings/PasswordsPreference.java",
]
+ # android_library("java")
+ sources += [
+ "java/src/org/chromium/chrome/browser/password_manager/ConfirmationDialogHelper.java",
+ "java/src/org/chromium/chrome/browser/password_manager/settings/PasswordManagerHandler.java",
+ "java/src/org/chromium/chrome/browser/password_manager/settings/PasswordManagerHandlerProvider.java",
+ "java/src/org/chromium/chrome/browser/password_manager/settings/PasswordUiView.java",
+ "java/src/org/chromium/chrome/browser/password_manager/settings/SavedPasswordEntry.java",
+ "java/src/org/chromium/chrome/browser/password_manager/settings/PasswordListObserver.java",
+ "java/src/org/chromium/chrome/browser/password_manager/settings/PasswordAccessReauthenticationHelper.java",
+ "java/src/org/chromium/chrome/browser/password_manager/settings/ReauthenticationManager.java",
+ "java/src/org/chromium/chrome/browser/password_manager/settings/PasswordReauthenticationFragment.java",
+ "java/src/org/chromium/chrome/browser/password_manager/settings/SingleThreadBarrierClosure.java",
+ "java/src/org/chromium/chrome/browser/password_manager/settings/CallbackDelayer.java",
+ "java/src/org/chromium/chrome/browser/password_manager/settings/TimedCallbackDelayer.java",
+ "java/src/org/chromium/chrome/browser/password_manager/settings/DialogManager.java",
+ "java/src/org/chromium/chrome/browser/password_manager/settings/ProgressBarDialogFragment.java",
+ "java/src/org/chromium/chrome/browser/password_manager/settings/NonCancelableProgressBar.java",
+ "java/src/org/chromium/chrome/browser/password_manager/settings/ExportErrorDialogFragment.java",
+ "java/src/org/chromium/chrome/browser/password_manager/settings/ExportFlow.java",
+ "java/src/org/chromium/chrome/browser/password_manager/settings/ExportFlowInterface.java",
+ ]
+
resources_package = "org.chromium.chrome.browser.password_manager"
}
@@ -208,6 +230,8 @@ generate_jni("jni_headers") {
"java/src/org/chromium/chrome/browser/password_manager/PasswordStoreCredential.java",
"java/src/org/chromium/chrome/browser/password_manager/PasswordSyncControllerDelegateBridgeImpl.java",
]
+ # generate_jni("jni_headers")
+ sources += [ "java/src/org/chromium/chrome/browser/password_manager/settings/PasswordUiView.java" ]
}
android_library("utils_java") {
diff --git a/chrome/browser/password_manager/android/java/src/org/chromium/chrome/browser/password_manager/ConfirmationDialogHelper.java b/chrome/browser/password_manager/android/java/src/org/chromium/chrome/browser/password_manager/ConfirmationDialogHelper.java
new file mode 100644
--- /dev/null
+++ b/chrome/browser/password_manager/android/java/src/org/chromium/chrome/browser/password_manager/ConfirmationDialogHelper.java
@@ -0,0 +1,146 @@
+// Copyright 2021 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.chrome.browser.password_manager;
+
+import static org.chromium.build.NullUtil.assumeNonNull;
+
+import android.content.Context;
+import android.content.res.Resources;
+
+import org.chromium.base.CallbackUtils;
+import org.chromium.build.annotations.NullMarked;
+import org.chromium.build.annotations.Nullable;
+import org.chromium.components.browser_ui.modaldialog.AppModalPresenter;
+import org.chromium.ui.modaldialog.DialogDismissalCause;
+import org.chromium.ui.modaldialog.ModalDialogManager;
+import org.chromium.ui.modaldialog.ModalDialogManager.ModalDialogType;
+import org.chromium.ui.modaldialog.ModalDialogProperties;
+import org.chromium.ui.modaldialog.SimpleModalDialogController;
+import org.chromium.ui.modelutil.PropertyModel;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Helps to show a confirmation.
+ *
+ * @deprecated use {@link ActionConfirmationDialog} instead - see crbug.com/440257087.
+ */
+@Deprecated
+@NullMarked
+public class ConfirmationDialogHelper {
+ private final Context mContext;
+ private ModalDialogManager mModalDialogManager;
+ private @Nullable PropertyModel mDialogModel;
+ private @Nullable Runnable mConfirmedCallback;
+ private @Nullable Runnable mDeclinedCallback;
+
+ public ConfirmationDialogHelper(Context context) {
+ mContext = context;
+ mModalDialogManager =
+ new ModalDialogManager(new AppModalPresenter(context), ModalDialogType.APP);
+ }
+
+ /** Hides the dialog. */
+ public void dismiss() {
+ if (mDialogModel != null) {
+ mModalDialogManager.dismissDialog(mDialogModel, DialogDismissalCause.UNKNOWN);
+ }
+ mDialogModel = null;
+ }
+
+ // used by CredentialEditMediator.java
+ /** Returns the resources associated with the context used to launch the dialog. */
+ public Resources getResources() {
+ return mContext.getResources();
+ }
+
+ /**
+ * Shows an dialog to confirm the deletion.
+ *
+ * @param title A {@link String} used as title.
+ * @param message A {@link CharSequence} used message body.
+ * @param confirmButtonText A {@link String} for confirmation button label.
+ * @param confirmedCallback A callback to run when the dialog is accepted.
+ * @param declinedCallback A callback to run when the dialog is declined.
+ */
+ public void showConfirmation(
+ String title,
+ CharSequence message,
+ String confirmButtonText,
+ Runnable confirmedCallback) {
+ showConfirmation(
+ title,
+ message,
+ confirmButtonText,
+ confirmedCallback,
+ CallbackUtils.emptyRunnable());
+ }
+
+ /**
+ * Shows an dialog to confirm the deletion.
+ *
+ * @param title A {@link String} used as title.
+ * @param message A {@link CharSequence} used message body.
+ * @param confirmButtonText A {@link String} for confirmation button label.
+ * @param confirmedCallback A callback to run when the dialog is accepted.
+ * @param declinedCallback A callback to run when the dialog is declined.
+ */
+ public void showConfirmation(
+ String title,
+ CharSequence message,
+ String confirmButtonText,
+ Runnable confirmedCallback,
+ Runnable declinedCallback) {
+ assert title != null;
+ assert message != null;
+ assert confirmedCallback != null;
+
+ mConfirmedCallback = confirmedCallback;
+ mDeclinedCallback = declinedCallback;
+
+ mDialogModel =
+ new PropertyModel.Builder(ModalDialogProperties.ALL_KEYS)
+ .with(
+ ModalDialogProperties.CONTROLLER,
+ new SimpleModalDialogController(
+ mModalDialogManager, this::onDismiss))
+ .with(
+ ModalDialogProperties.BUTTON_STYLES,
+ ModalDialogProperties.ButtonStyles.PRIMARY_FILLED_NEGATIVE_OUTLINE)
+ .with(ModalDialogProperties.TITLE, title)
+ .with(
+ ModalDialogProperties.MESSAGE_PARAGRAPHS,
+ new ArrayList<>(List.of(message)))
+ .with(ModalDialogProperties.POSITIVE_BUTTON_TEXT, confirmButtonText)
+ .with(
+ ModalDialogProperties.NEGATIVE_BUTTON_TEXT,
+ mContext.getString(R.string.cancel))
+ .build();
+
+ mModalDialogManager.showDialog(mDialogModel, ModalDialogType.APP);
+ }
+
+ private void onDismiss(@DialogDismissalCause int dismissalCause) {
+ switch (dismissalCause) {
+ case DialogDismissalCause.POSITIVE_BUTTON_CLICKED:
+ assumeNonNull(mConfirmedCallback);
+ mConfirmedCallback.run();
+ break;
+ case DialogDismissalCause.NEGATIVE_BUTTON_CLICKED:
+ assumeNonNull(mDeclinedCallback);
+ mDeclinedCallback.run();
+ break;
+ default:
+ // No explicit user decision.
+ break;
+ }
+ mDialogModel = null;
+ }
+
+ void setModalDialogManagerForTesting(ModalDialogManager modalDialogManager) {
+ mModalDialogManager = modalDialogManager;
+ }
+}
diff --git a/chrome/browser/password_manager/android/java/src/org/chromium/chrome/browser/password_manager/PasswordManagerHelper.java b/chrome/browser/password_manager/android/java/src/org/chromium/chrome/browser/password_manager/PasswordManagerHelper.java
--- a/chrome/browser/password_manager/android/java/src/org/chromium/chrome/browser/password_manager/PasswordManagerHelper.java
+++ b/chrome/browser/password_manager/android/java/src/org/chromium/chrome/browser/password_manager/PasswordManagerHelper.java
@@ -35,8 +35,9 @@ import org.chromium.chrome.browser.password_manager.PasswordCheckupClientHelper.
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.profiles.ProfileKeyedMap;
import org.chromium.chrome.browser.pwm_disabled.PasswordCsvDownloadFlowControllerFactory;
-import org.chromium.chrome.browser.pwm_disabled.PasswordManagerUnavailableDialogCoordinator;
import org.chromium.chrome.browser.sync.SyncServiceFactory;
+import org.chromium.chrome.browser.settings.SettingsNavigationFactory;
+import org.chromium.components.browser_ui.settings.SettingsNavigation;
import org.chromium.components.browser_ui.settings.SettingsCustomTabLauncher;
import org.chromium.components.sync.SyncService;
import org.chromium.ui.modaldialog.ModalDialogManager;
@@ -134,7 +135,7 @@ public class PasswordManagerHelper {
context, modalDialogManager, settingsCustomTabLauncher)) {
LoadingModalDialogCoordinator loadingDialogCoordinator =
LoadingModalDialogCoordinator.create(modalDialogManager, context);
- launchTheCredentialManager(referrer, syncService, loadingDialogCoordinator, account);
+ launchTheCredentialManager(context, referrer, syncService, loadingDialogCoordinator, account);
}
}
@@ -163,6 +164,7 @@ public class PasswordManagerHelper {
Context context,
ModalDialogManager modalDialogManager,
SettingsCustomTabLauncher settingsCustomTabLauncher) {
+ if ((true)) return false;
// Automotive doesn't support the export flow.
if (!DeviceInfo.isAutomotive()
&& LoginDbDeprecationUtilBridge.hasPasswordsInCsv(mProfile)) {
@@ -170,16 +172,6 @@ public class PasswordManagerHelper {
return true;
}
- if (!PasswordManagerUtilBridge.isPasswordManagerAvailable()) {
- new PasswordManagerUnavailableDialogCoordinator()
- .showDialog(
- context,
- modalDialogManager,
- PasswordManagerUtilBridge.isGooglePlayServicesUpdatable()
- ? GmsUpdateLauncher::launch
- : null);
- return true;
- }
return false;
}
@@ -395,10 +387,16 @@ public class PasswordManagerHelper {
@VisibleForTesting
void launchTheCredentialManager(
+ Context context,
@ManagePasswordsReferrer int referrer,
@Nullable SyncService syncService,
LoadingModalDialogCoordinator loadingDialogCoordinator,
@Nullable String account) {
+ SettingsNavigation settingsNavigation =
+ SettingsNavigationFactory.createSettingsNavigation();
+ settingsNavigation.startSettings(
+ context, SettingsNavigation.SettingsFragment.PASSWORDS);
+ if ((true)) return;
assert syncService != null;
assert PasswordManagerUtilBridge.isPasswordManagerAvailable();
CredentialManagerLauncher credentialManagerLauncher =
diff --git a/chrome/browser/password_manager/android/java/src/org/chromium/chrome/browser/password_manager/PasswordManagerUtilBridge.java b/chrome/browser/password_manager/android/java/src/org/chromium/chrome/browser/password_manager/PasswordManagerUtilBridge.java
--- a/chrome/browser/password_manager/android/java/src/org/chromium/chrome/browser/password_manager/PasswordManagerUtilBridge.java
+++ b/chrome/browser/password_manager/android/java/src/org/chromium/chrome/browser/password_manager/PasswordManagerUtilBridge.java
@@ -39,8 +39,7 @@ public class PasswordManagerUtilBridge {
*/
@CalledByNative
public static boolean isGooglePlayServicesUpdatable() {
- return PackageUtils.isPackageInstalled("com.google.android.gms")
- && PackageUtils.getPackageInfo("com.android.vending", 0) != null;
+ return false;
}
@NativeMethods
diff --git a/chrome/browser/password_manager/android/java/src/org/chromium/chrome/browser/password_manager/settings/CallbackDelayer.java b/chrome/browser/password_manager/android/java/src/org/chromium/chrome/browser/password_manager/settings/CallbackDelayer.java
new file mode 100644
--- /dev/null
+++ b/chrome/browser/password_manager/android/java/src/org/chromium/chrome/browser/password_manager/settings/CallbackDelayer.java
@@ -0,0 +1,18 @@
+// Copyright 2018 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.chrome.browser.password_manager.settings;
+
+import org.chromium.build.annotations.NullMarked;
+
+/** This is an interface for delaying running of callbacks. */
+@NullMarked
+public interface CallbackDelayer {
+ /**
+ * Run a callback after a delay specific to a particular implementation. The callback is always
+ * run asynchronously.
+ * @param callback The callback to be run.
+ */
+ void delay(Runnable callback);
+}
diff --git a/chrome/browser/password_manager/android/java/src/org/chromium/chrome/browser/password_manager/settings/DialogManager.java b/chrome/browser/password_manager/android/java/src/org/chromium/chrome/browser/password_manager/settings/DialogManager.java
new file mode 100644
--- /dev/null
+++ b/chrome/browser/password_manager/android/java/src/org/chromium/chrome/browser/password_manager/settings/DialogManager.java
@@ -0,0 +1,168 @@
+// Copyright 2018 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.chrome.browser.password_manager.settings;
+
+import androidx.annotation.IntDef;
+import androidx.fragment.app.DialogFragment;
+import androidx.fragment.app.FragmentManager;
+
+import org.chromium.base.task.PostTask;
+import org.chromium.base.task.TaskTraits;
+import org.chromium.build.annotations.NullMarked;
+import org.chromium.build.annotations.Nullable;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * This class manages a {@link DialogFragment}.
+ * In particular, it ensures that the dialog stays visible for a minimum time period, so that
+ * earlier calls to hide it are delayed appropriately. It also allows to override the delaying for
+ * testing purposes.
+ */
+@NullMarked
+public final class DialogManager {
+ /**
+ * Contains the reference to a {@link android.app.DialogFragment} between the call to {@link
+ * show} and dismissing the dialog.
+ */
+ private @Nullable DialogFragment mDialogFragment;
+
+ /**
+ * The least amout of time for which {@link mDialogFragment} should stay visible to avoid
+ * flickering. It was chosen so that it is enough to read the approx. 3 words on it, but not too
+ * long (to avoid the user waiting while Chrome is already idle).
+ */
+ private static final long MINIMUM_LIFE_SPAN_MILLIS = 1000L;
+
+ /** This is used to post the unblocking signal for hiding the dialog fragment. */
+ private CallbackDelayer mDelayer = new TimedCallbackDelayer(MINIMUM_LIFE_SPAN_MILLIS);
+
+ /**
+ * Used to gate hiding of a dialog on two actions: one automatic delayed signal and one manual
+ * call to {@link hide}. This is not null between the calls to {@link show} and {@link hide}.
+ */
+ private @Nullable SingleThreadBarrierClosure mBarrierClosure;
+
+ /** Callback to run after the dialog was hidden. Can be null if no hiding was requested.*/
+ private @Nullable Runnable mCallback;
+
+ private boolean mShowingRequested;
+
+ /** Possible actions taken on the dialog during {@link #hide}. */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({HideActions.NO_OP, HideActions.HIDDEN_IMMEDIATELY, HideActions.HIDING_DELAYED})
+ public @interface HideActions {
+ /** The dialog has not been shown, so it is not being hidden. */
+ int NO_OP = 0;
+
+ /** {@link #mBarrierClosure} was signalled so the dialog is hidden now. */
+ int HIDDEN_IMMEDIATELY = 1;
+
+ /** The hiding is being delayed until {@link #mBarrierClosure} is signalled further. */
+ int HIDING_DELAYED = 2;
+ }
+
+ /** Interface to notify, during @{link #hide}, which action was taken. */
+ public interface ActionsConsumer {
+ void consume(@HideActions int action);
+ }
+
+ /** The callback called everytime {@link #hide} is executed. */
+ private final @Nullable ActionsConsumer mActionsConsumer;
+
+ /**
+ * Constructs a DialogManager, optionally with a callback to report which action was taken on
+ * hiding.
+ * @param actionsConsumer The callback called everytime {@link #hide} is executed.
+ */
+ public DialogManager(@Nullable ActionsConsumer actionsConsumer) {
+ mActionsConsumer = actionsConsumer;
+ }
+
+ /**
+ * Shows the dialog after the specified delay.
+ *
+ * @param dialog to be shown.
+ * @param fragmentManager needed to call {@link android.app.DialogFragment#show}.
+ * @param delay the delay in ms after which the dialog will be displayed (if not canceled during
+ * this delay).
+ */
+ public void showWithDelay(DialogFragment dialog, FragmentManager fragmentManager, int delay) {
+ mShowingRequested = true;
+ new TimedCallbackDelayer(delay)
+ .delay(
+ () -> {
+ // hide() might have been called during the delay.
+ if (mShowingRequested) {
+ show(dialog, fragmentManager);
+ }
+ });
+ }
+
+ /**
+ * Shows the dialog.
+ * @param dialog to be shown.
+ * @param fragmentManager needed to call {@link android.app.DialogFragment#show}
+ */
+ public void show(DialogFragment dialog, FragmentManager fragmentManager) {
+ mShowingRequested = true;
+ mDialogFragment = dialog;
+ mDialogFragment.show(fragmentManager, null);
+ // Initiate the barrier closure, expecting 2 runs: one automatic but delayed, and one
+ // explicit, to hide the dialog.
+ mBarrierClosure = new SingleThreadBarrierClosure(2, this::hideImmediately);
+ // This is the automatic but delayed signal.
+ mDelayer.delay(mBarrierClosure);
+ }
+
+ /**
+ * Hides the dialog as soon as possible, but not sooner than {@link MINIMUM_LIFE_SPAN_MILLIS}
+ * milliseconds after it was shown. Attempts to hide the dialog when none is shown are
+ * gracefully ignored but the callback is called in any case.
+ * @param callback is asynchronously called as soon as the dialog is no longer visible.
+ */
+ public void hide(@Nullable Runnable callback) {
+ if (mActionsConsumer != null) {
+ @HideActions final int action;
+ if (mBarrierClosure == null) {
+ action = HideActions.NO_OP;
+ } else if (mBarrierClosure.isReady()) {
+ action = HideActions.HIDDEN_IMMEDIATELY;
+ } else {
+ action = HideActions.HIDING_DELAYED;
+ }
+ mActionsConsumer.consume(action);
+ }
+ mCallback = callback;
+ // The barrier closure is null if the dialog was not shown. In that case don't wait before
+ // confirming the hidden state.
+ if (mBarrierClosure == null) {
+ hideImmediately();
+ } else {
+ mBarrierClosure.run();
+ }
+ }
+
+ /**
+ * Synchronously hides the dialog without any delay. Attempts to hide the dialog when
+ * none is shown are gracefully ignored but |mCallback| is called in any case if present.
+ */
+ private void hideImmediately() {
+ if (mDialogFragment != null) mDialogFragment.dismiss();
+ // Post the callback to ensure that it is always run asynchronously, even if hide() took a
+ // shortcut for a missing shown().
+ if (mCallback != null) PostTask.postTask(TaskTraits.UI_DEFAULT, mCallback);
+ reset();
+ }
+
+ /** Resets the dialog reference and metadata related to it.*/
+ private void reset() {
+ mDialogFragment = null;
+ mCallback = null;
+ mBarrierClosure = null;
+ mShowingRequested = false;
+ }
+}
diff --git a/chrome/browser/password_manager/android/java/src/org/chromium/chrome/browser/password_manager/settings/ExportErrorDialogFragment.java b/chrome/browser/password_manager/android/java/src/org/chromium/chrome/browser/password_manager/settings/ExportErrorDialogFragment.java
new file mode 100644
--- /dev/null
+++ b/chrome/browser/password_manager/android/java/src/org/chromium/chrome/browser/password_manager/settings/ExportErrorDialogFragment.java
@@ -0,0 +1,110 @@
+// Copyright 2018 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.chrome.browser.password_manager.settings;
+
+import android.app.Dialog;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import android.view.View;
+import android.widget.TextView;
+
+import androidx.appcompat.app.AlertDialog;
+import androidx.fragment.app.DialogFragment;
+
+import org.chromium.build.annotations.NullMarked;
+import org.chromium.build.annotations.Nullable;
+import org.chromium.chrome.browser.password_manager.R;
+
+/**
+ * Shows the dialog that explains to the user the error which just happened during exporting and
+ * optionally helps them to take actions to fix that (learning more, retrying export).
+ */
+@NullMarked
+public class ExportErrorDialogFragment extends DialogFragment {
+ /** Parameters to fill in the strings in the dialog. Pass them through {@link #initialize()}. */
+ public static class ErrorDialogParams {
+ /**
+ * The string resource ID for the label of the positive button. If it's 0, no positive
+ * button will be displayed.
+ */
+ public int positiveButtonLabelId;
+
+ /** The main description of the error. Required. */
+ public @Nullable String description;
+
+ /**
+ * An optional detailed description. Will be prefixed with "Details:" and displayed below
+ * the main one.
+ */
+ public @Nullable String detailedDescription;
+ }
+
+ // This handler is used to answer the user actions on the dialog.
+ private DialogInterface.@Nullable OnClickListener mHandler;
+
+ /** Defines the strings to be shown. Set in {@link #initialize()}. */
+ private @Nullable ErrorDialogParams mParams;
+
+ /**
+ * Sets the click handler for the dialog buttons.
+ * @param mHandler The handler to use.
+ */
+ public void setExportErrorHandler(DialogInterface.OnClickListener handler) {
+ mHandler = handler;
+ }
+
+ /**
+ * Opens the dialog with the warning and sets the button listener to a fragment identified by ID
+ * passed in arguments.
+ */
+ @Override
+ public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
+ assert mParams != null;
+ final View dialog =
+ getActivity().getLayoutInflater().inflate(R.layout.passwords_error_dialog, null);
+ final TextView mainDescription = dialog.findViewById(R.id.passwords_error_main_description);
+ mainDescription.setText(mParams.description);
+ final TextView detailedDescription =
+ dialog.findViewById(R.id.passwords_error_detailed_description);
+ if (mParams.detailedDescription != null) {
+ detailedDescription.setText(mParams.detailedDescription);
+ } else {
+ detailedDescription.setVisibility(View.GONE);
+ }
+ AlertDialog.Builder dialogBuilder =
+ new AlertDialog.Builder(
+ getActivity(),
+ R.style.ThemeOverlay_BrowserUI_AlertDialog_NoActionBar)
+ .setView(dialog)
+ .setTitle(R.string.password_settings_export_error_title)
+ .setNegativeButton(R.string.close, mHandler);
+ if (mParams.positiveButtonLabelId != 0) {
+ dialogBuilder.setPositiveButton(mParams.positiveButtonLabelId, mHandler);
+ }
+ return dialogBuilder.create();
+ }
+
+ @Override
+ public void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ // If there is savedInstanceState, then the dialog is being recreated by Android and will
+ // lack the necessary callbacks. The user likely already saw it first and then replaced the
+ // current activity. Therefore just close the dialog.
+ if (savedInstanceState != null) {
+ dismiss();
+ return;
+ }
+ }
+
+ /**
+ * Set the parameters for the strings to be shown. Must be called exactly once, before the
+ * dialog is shown.
+ */
+ public void initialize(ErrorDialogParams params) {
+ assert mParams == null && params != null;
+ assert params.description != null;
+ mParams = params;
+ }
+}
diff --git a/chrome/browser/password_manager/android/java/src/org/chromium/chrome/browser/password_manager/settings/ExportFlow.java b/chrome/browser/password_manager/android/java/src/org/chromium/chrome/browser/password_manager/settings/ExportFlow.java
new file mode 100644
--- /dev/null
+++ b/chrome/browser/password_manager/android/java/src/org/chromium/chrome/browser/password_manager/settings/ExportFlow.java
@@ -0,0 +1,583 @@
+// Copyright 2018 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.chrome.browser.password_manager.settings;
+
+import static org.chromium.build.NullUtil.assumeNonNull;
+
+import android.content.ActivityNotFoundException;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.VisibleForTesting;
+import androidx.appcompat.app.AlertDialog;
+
+import org.chromium.base.CallbackUtils;
+import org.chromium.base.ContextUtils;
+import org.chromium.base.FileProviderUtils;
+import org.chromium.base.FileUtils;
+import org.chromium.base.task.AsyncTask;
+import org.chromium.build.annotations.Initializer;
+import org.chromium.build.annotations.NullMarked;
+import org.chromium.build.annotations.Nullable;
+import org.chromium.chrome.browser.password_manager.R;
+import org.chromium.ui.widget.Toast;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * This class allows to trigger and complete the UX flow for exporting passwords. A {@link Fragment}
+ * can use it to display the flow UI over the fragment.
+ *
+ * <pre>
+ * Internally, the flow is represented by the following calls:
+ * (1) {@link #startExporting}, which triggers both preparing of stored passwords in the background
+ * and reauthentication of the user.
+ * (2a) {@link #shareSerializedPasswords}, which is the final part of the preparation of passwords
+ * which otherwise runs in the native code.
+ * (2b) {@link #exportAfterReauth} is the user-visible next step after reauthentication.
+ * (3) {@link #tryExporting} merges the flow of the in-parallel-running (2a) and (2b). In the rare
+ * case when (2b) finishes before (2a), it also displays a progress bar.
+ * (4) {@link #sendExportIntent} creates an intent chooser for sharing the exported passwords with
+ * an app of user's choice.
+ * </pre>
+ */
+@NullMarked
+public class ExportFlow implements ExportFlowInterface {
+ @IntDef({ExportState.INACTIVE, ExportState.REQUESTED, ExportState.CONFIRMED})
+ @Retention(RetentionPolicy.SOURCE)
+ private @interface ExportState {
+ /**
+ * INACTIVE: there is no currently running export. Either the user did not request
+ * one, or the last one completed (i.e., a share intent picker or an error message were
+ * displayed or the user cancelled it).
+ */
+ int INACTIVE = 0;
+
+ /**
+ * REQUESTED: the user requested the export in the menu but did not authenticate
+ * and confirm it yet.
+ */
+ int REQUESTED = 1;
+
+ /**
+ * CONFIRMED: the user confirmed the export and Chrome is still busy preparing the
+ * data for the share intent.
+ */
+ int CONFIRMED = 2;
+ }
+
+ /** Describes at which state the password export flow is. */
+ @ExportState private int mExportState;
+
+ /** Name of the subdirectory in cache which stores the exported passwords file. */
+ private static final String PASSWORDS_CACHE_DIR = "/passwords";
+
+ /** The key for saving {@link #mExportState} to instance bundle. */
+ private static final String SAVED_STATE_EXPORT_STATE = "saved-state-export-state";
+
+ /** The key for saving {@link #mEntriesCount}|to instance bundle. */
+ private static final String SAVED_STATE_ENTRIES_COUNT = "saved-state-entries-count";
+
+ /** The key for saving {@link #mExportFileUri} to instance bundle. */
+ private static final String SAVED_STATE_EXPORT_FILE_URI = "saved-state-export-file-uri";
+
+ /** The delay after which the progress bar will be displayed. */
+ private static final int PROGRESS_BAR_DELAY_MS = 500;
+
+ // Values of the histogram recording password export related events.
+ @IntDef({
+ PasswordExportEvent.EXPORT_OPTION_SELECTED,
+ PasswordExportEvent.EXPORT_DISMISSED,
+ PasswordExportEvent.EXPORT_CONFIRMED,
+ PasswordExportEvent.COUNT
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface PasswordExportEvent {
+ int EXPORT_OPTION_SELECTED = 0;
+ int EXPORT_DISMISSED = 1;
+ int EXPORT_CONFIRMED = 2;
+ int COUNT = 3;
+ }
+
+ /**
+ * When the user requests that passwords are exported and once the passwords are sent over from
+ * native code and stored in a cache file, this variable contains the content:// URI for that
+ * cache file, or an empty URI if there was a problem with storing to that file. During all
+ * other times, this variable is null. In particular, after the export is requested, the
+ * variable being null means that the passwords have not arrived from the native code yet.
+ */
+ private @Nullable Uri mExportFileUri;
+
+ /**
+ * The number of password entries contained in the most recent serialized data for password
+ * export. The null value indicates that serialization has not completed since the last request
+ * (or there was no request at all).
+ */
+ private @Nullable Integer mEntriesCount;
+
+ // Histogram values for "PasswordManager.Android.ExportPasswordsProgressBarUsage". Never remove
+ // or reuse them, only add new ones if needed to keep past and future UMA reports compatible.
+ @VisibleForTesting public static final int PROGRESS_NOT_SHOWN = 0;
+ @VisibleForTesting public static final int PROGRESS_HIDDEN_DIRECTLY = 1;
+ @VisibleForTesting public static final int PROGRESS_HIDDEN_DELAYED = 2;
+
+ // Takes care of displaying and hiding the progress bar for exporting, while avoiding
+ // flickering.
+ private final DialogManager mProgressBarManager = new DialogManager(null);
+
+ /**
+ * If an error dialog should be shown, this contains the arguments for it, such as the error
+ * message. If no error dialog should be shown, this is null.
+ */
+ private ExportErrorDialogFragment.@Nullable ErrorDialogParams mErrorDialogParams;
+
+ /** The concrete delegate instance. It is (re)set in {@link #onCreate}. */
+ private Delegate mDelegate;
+
+ /** Histogram names for metrics logging. */
+ private String mCallerMetricsId;
+
+ private boolean mPasswordSerializationStarted;
+
+ private boolean mExportFLowFinalStepLogged;
+
+ public ExportFlow() {}
+
+ @Initializer
+ @Override
+ public void onCreate(Bundle savedInstanceState, Delegate delegate, String callerMetricsId) {
+ mDelegate = delegate;
+ mCallerMetricsId = callerMetricsId;
+
+ if (savedInstanceState == null) return;
+
+ if (savedInstanceState.containsKey(SAVED_STATE_EXPORT_STATE)) {
+ mExportState = savedInstanceState.getInt(SAVED_STATE_EXPORT_STATE);
+ if (mExportState == ExportState.CONFIRMED) {
+ // If export is underway, ensure that the UI is updated.
+ tryExporting();
+ }
+ }
+ if (savedInstanceState.containsKey(SAVED_STATE_EXPORT_FILE_URI)) {
+ String uriString = savedInstanceState.getString(SAVED_STATE_EXPORT_FILE_URI);
+ assumeNonNull(uriString);
+ if (uriString.isEmpty()) {
+ mExportFileUri = Uri.EMPTY;
+ } else {
+ mExportFileUri = Uri.parse(uriString);
+ }
+ }
+ if (savedInstanceState.containsKey(SAVED_STATE_ENTRIES_COUNT)) {
+ mEntriesCount = savedInstanceState.getInt(SAVED_STATE_ENTRIES_COUNT);
+ }
+ }
+
+ /**
+ * Returns true if the export flow is in progress, i.e., when the user interacts with some of
+ * its UI.
+ * @return True if in progress, false otherwise.
+ */
+ public boolean isActive() {
+ return mExportState != ExportState.INACTIVE;
+ }
+
+ /**
+ * A helper method which processes the signal that serialized passwords have been stored in the
+ * temporary file. It produces a sharing URI for that file, registers that file for deletion at
+ * the shutdown of the Java VM, logs some metrics and continues the flow.
+ * @param pathToPasswordsFile The filesystem path to the file containing the serialized
+ * passwords.
+ */
+ private void shareSerializedPasswords(String pathToPasswordsFile) {
+ // Don't display any UI if the user cancelled the export in the meantime.
+ if (mExportState == ExportState.INACTIVE) return;
+
+ File passwordsFile = new File(pathToPasswordsFile);
+ passwordsFile.deleteOnExit();
+
+ try {
+ mExportFileUri = FileProviderUtils.getContentUriFromFile(passwordsFile);
+ } catch (IllegalArgumentException e) {
+ showExportErrorAndAbort(
+ R.string.password_settings_export_tips,
+ e.getMessage(),
+ getPositiveButtonLabelId());
+ return;
+ }
+
+ tryExporting();
+ }
+
+ /**
+ * Returns the path to the directory where serialized passwords are stored.
+ *
+ * @return A subdirectory of the cache, where serialized passwords are stored.
+ */
+ @VisibleForTesting
+ public static String getTargetDirectory() {
+ return ContextUtils.getApplicationContext().getCacheDir() + PASSWORDS_CACHE_DIR;
+ }
+
+ @Override
+ public void startExporting() {
+ assert mExportState == ExportState.INACTIVE;
+ mPasswordSerializationStarted = false;
+ mExportFLowFinalStepLogged = false;
+ // Disable re-triggering exporting until the current exporting finishes.
+ mExportState = ExportState.REQUESTED;
+
+ // Start fetching the serialized passwords now to use the time the user spends
+ // reauthenticating and reading the warning message. If the user cancels the export or
+ // fails the reauthentication, the serialized passwords will simply get ignored when
+ // they arrive.
+ mEntriesCount = null;
+ PasswordManagerHandler handler =
+ PasswordManagerHandlerProvider.getForProfile(mDelegate.getProfile())
+ .getPasswordManagerHandler();
+ assumeNonNull(handler);
+ if (!handler.isWaitingForPasswordStore()) {
+ serializePasswords();
+ }
+ if (!ReauthenticationManager.isScreenLockSetUp(
+ mDelegate.getActivity().getApplicationContext())) {
+ Toast.makeText(
+ mDelegate.getActivity().getApplicationContext(),
+ R.string.password_export_set_lock_screen,
+ Toast.LENGTH_LONG)
+ .show();
+ // Re-enable exporting, the current one was cancelled by Chrome.
+ mExportState = ExportState.INACTIVE;
+ } else {
+ // Always trigger reauthentication at the start of the exporting flow, even if the last
+ // one succeeded recently.
+ ReauthenticationManager.displayReauthenticationFragment(
+ R.string.lockscreen_description_export,
+ mDelegate.getViewId(),
+ mDelegate.getFragmentManager(),
+ ReauthenticationManager.ReauthScope.BULK);
+ }
+ }
+
+ /** Starts fetching the serialized passwords. */
+ void serializePasswords() {
+ if (mPasswordSerializationStarted) return;
+ mPasswordSerializationStarted = true;
+ PasswordManagerHandler handler =
+ PasswordManagerHandlerProvider.getForProfile(mDelegate.getProfile())
+ .getPasswordManagerHandler();
+ assumeNonNull(handler);
+ handler.serializePasswords(
+ getTargetDirectory(),
+ (int entriesCount, String pathToPasswordsFile) -> {
+ mEntriesCount = entriesCount;
+ shareSerializedPasswords(pathToPasswordsFile);
+ },
+ (String errorMessage) -> {
+ showExportErrorAndAbort(
+ R.string.password_settings_export_tips,
+ errorMessage,
+ getPositiveButtonLabelId());
+ });
+ }
+
+ @Override
+ public void passwordsAvailable() {
+ if (mExportState == ExportState.REQUESTED) {
+ serializePasswords();
+ }
+ }
+
+ /**
+ * Continues with the password export flow after the user successfully reauthenticated. Current
+ * state of export flow: the user tapped the menu item for export and passed reauthentication.
+ * The next steps are: confirming the export, waiting for exported data (if needed) and choosing
+ * a consumer app for the data.
+ */
+ private void exportAfterReauth() {
+ mExportState = ExportState.CONFIRMED;
+ // If the error dialog has been waiting (e. g. because password serialization failed while
+ // the user was authenticating), display it now, otherwise continue the export flow.
+ if (mErrorDialogParams != null) {
+ showExportErrorDialogFragment();
+ } else {
+ tryExporting();
+ }
+ }
+
+ /**
+ * Starts the exporting intent if both blocking events are completed: serializing and the
+ * confirmation flow. At this point, the user has tapped the menu item for export and passed
+ * reauthentication. Upon calling this method, the user has either also confirmed the export, or
+ * the exported data have been prepared. The method is called twice, once for each of those
+ * events. The next step after both the export is confirmed and the data is ready is to create
+ * document intent.
+ */
+ private void tryExporting() {
+ if (mExportState != ExportState.CONFIRMED) return;
+ if (mEntriesCount == null) {
+ // The serialization has not finished. Until this finishes, a progress bar is
+ // displayed with an option to cancel the export.
+ ProgressBarDialogFragment progressBarDialogFragment = new ProgressBarDialogFragment();
+ progressBarDialogFragment.setCancelProgressHandler(
+ (unusedDialogInterface, button) -> {
+ if (button == AlertDialog.BUTTON_NEGATIVE) {
+ mExportState = ExportState.INACTIVE;
+ }
+ });
+ mProgressBarManager.show(progressBarDialogFragment, mDelegate.getFragmentManager());
+ } else {
+ // Note: if the serialization is quicker than the user interacting with the
+ // confirmation dialog, then there is no progress bar shown, in which case hide() is
+ // just calling the callback synchronously.
+ mProgressBarManager.hide(this::sendExportIntent);
+ }
+ }
+
+ /**
+ * Call this to abort the export UI flow and display an error description to the user.
+ *
+ * @param descriptionId The resource ID of a string with a brief explanation of the error.
+ * @param detailedDescription An optional string with more technical details about the error.
+ * @param positiveButtonLabelId The resource ID of the label of the positive button in the error
+ * dialog.
+ */
+ @VisibleForTesting
+ void showExportErrorAndAbort(
+ int descriptionId,
+ @Nullable String detailedDescription,
+ int positiveButtonLabelId) {
+ assert mErrorDialogParams == null;
+ mDelegate.onExportFlowFailed();
+ mProgressBarManager.hide(
+ () -> {
+ showExportErrorAndAbortImmediately(
+ descriptionId,
+ detailedDescription,
+ positiveButtonLabelId);
+ });
+ }
+
+ public void showExportErrorAndAbortImmediately(
+ int descriptionId,
+ @Nullable String detailedDescription,
+ int positiveButtonLabelId) {
+ mErrorDialogParams = new ExportErrorDialogFragment.ErrorDialogParams();
+ mErrorDialogParams.positiveButtonLabelId = positiveButtonLabelId;
+ mErrorDialogParams.description =
+ mDelegate.getActivity().getResources().getString(descriptionId);
+
+ if (detailedDescription != null) {
+ mErrorDialogParams.detailedDescription =
+ mDelegate
+ .getActivity()
+ .getResources()
+ .getString(
+ R.string.password_settings_export_error_details,
+ detailedDescription);
+ }
+
+ showExportErrorDialogFragment();
+ }
+
+ /**
+ * This is a helper method to {@link #showExportErrorAndAbort}, responsible for showing the
+ * actual UI.
+ */
+ private void showExportErrorDialogFragment() {
+ assert mErrorDialogParams != null;
+
+ ExportErrorDialogFragment exportErrorDialogFragment = new ExportErrorDialogFragment();
+ int positiveButtonLabelId = mErrorDialogParams.positiveButtonLabelId;
+ exportErrorDialogFragment.initialize(mErrorDialogParams);
+ mErrorDialogParams = null;
+
+ exportErrorDialogFragment.setExportErrorHandler(
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ if (which == AlertDialog.BUTTON_POSITIVE) {
+ if (positiveButtonLabelId
+ == R.string.password_settings_export_learn_google_drive) {
+ // Link to the help article about how to use Google Drive.
+ Intent intent =
+ new Intent(
+ Intent.ACTION_VIEW,
+ Uri.parse(
+ "https://support.google.com/drive/answer/2424384"));
+ intent.setPackage(mDelegate.getActivity().getPackageName());
+ mDelegate.getActivity().startActivity(intent);
+ } else if (positiveButtonLabelId == R.string.try_again) {
+ mExportState = ExportState.REQUESTED;
+ // If `mExportFileUri` is null, it means that serialization has
+ // failed. Need to restart it too.
+ if (mExportFileUri == null) {
+ mPasswordSerializationStarted = false;
+ serializePasswords();
+ }
+ exportAfterReauth();
+ }
+ } else if (which == AlertDialog.BUTTON_NEGATIVE) {
+ // Re-enable exporting, the current one was just cancelled.
+ mDelegate.onExportFlowCanceled();
+ mProgressBarManager.hide(CallbackUtils.emptyRunnable());
+ mExportState = ExportState.INACTIVE;
+ mExportFileUri = null;
+ }
+ }
+ });
+ exportErrorDialogFragment.show(mDelegate.getFragmentManager(), null);
+ }
+
+ /**
+ * If the URI of the file with exported passwords is not null, passes it into an implicit
+ * intent, so that the user can use a storage app to save the exported passwords.
+ */
+ private void sendExportIntent() {
+ assert mExportState == ExportState.CONFIRMED;
+ mExportState = ExportState.INACTIVE;
+
+ if (mExportFileUri != null && mExportFileUri.equals(Uri.EMPTY)) return;
+
+ runCreateFileOnDiskIntent();
+ }
+
+ private void runCreateFileOnDiskIntent() {
+ Intent saveToDownloads = new Intent(Intent.ACTION_CREATE_DOCUMENT);
+ saveToDownloads.setType("text/csv");
+ saveToDownloads.addCategory(Intent.CATEGORY_OPENABLE);
+ saveToDownloads.putExtra(
+ Intent.EXTRA_TITLE,
+ mDelegate
+ .getActivity()
+ .getResources()
+ .getString(R.string.password_manager_default_export_filename));
+ try {
+ mDelegate.runCreateFileOnDiskIntent(saveToDownloads);
+ } catch (ActivityNotFoundException e) {
+ showExportErrorAndAbort(
+ R.string.password_settings_export_no_app,
+ e.getMessage(),
+ getPositiveButtonLabelId());
+ }
+ }
+
+ @Override
+ public void savePasswordsToDownloads(Uri passwordsFile) {
+ if (passwordsFile == null) {
+ showExportErrorAndAbort(
+ R.string.password_settings_export_tips,
+ "Could not create file.",
+ getPositiveButtonLabelId());
+ return;
+ }
+ new AsyncTask<@Nullable String>() {
+ @Override
+ protected @Nullable String doInBackground() {
+ assumeNonNull(mExportFileUri);
+ try {
+ writeToInternalStorage(mExportFileUri, passwordsFile);
+ } catch (IOException e) {
+ // This metric should be logged in onPostExecute in case of an exception.
+ // Since that happens in a callback, to be absolutely sure it's logged,
+ // it's already logged here. It won't be logged as a duplicate because the
+ // logging method checks if the metric was prevoiusly logged for the current
+ // export flow.
+ return e.getMessage();
+ }
+ return null;
+ }
+
+ @Override
+ protected void onPostExecute(@Nullable String exceptionMessage) {
+ mProgressBarManager.hide(
+ () -> {
+ if (exceptionMessage != null) {
+ showExportErrorAndAbort(
+ R.string.password_settings_export_tips,
+ exceptionMessage,
+ getPositiveButtonLabelId());
+ } else {
+ mDelegate.onExportFlowSucceeded();
+ mExportFileUri = null;
+ }
+ });
+ }
+ }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ mProgressBarManager.showWithDelay(
+ new NonCancelableProgressBar(R.string.passwords_export_in_progress_title),
+ mDelegate.getFragmentManager(),
+ PROGRESS_BAR_DELAY_MS);
+ }
+
+ private static void writeToInternalStorage(Uri exportedPasswordsUri, Uri savedPasswordsFileUri)
+ throws IOException {
+ try (InputStream fileInputStream =
+ ContextUtils.getApplicationContext()
+ .getContentResolver()
+ .openInputStream(exportedPasswordsUri)) {
+ try (OutputStream fileOutputStream =
+ ContextUtils.getApplicationContext()
+ .getContentResolver()
+ .openOutputStream(savedPasswordsFileUri)) {
+ assumeNonNull(fileInputStream);
+ assumeNonNull(fileOutputStream);
+ FileUtils.copyStream(fileInputStream, fileOutputStream);
+ }
+ }
+ }
+
+ @Override
+ public void onResume() {
+ if (mExportState == ExportState.REQUESTED) {
+ if (ReauthenticationManager.authenticationStillValid(
+ ReauthenticationManager.ReauthScope.BULK)) {
+ exportAfterReauth();
+ } else {
+ mExportState = ExportState.INACTIVE;
+ }
+ }
+ }
+
+ /**
+ * A hook to be used in a {@link Fragment}'s onSaveInstanceState method. I saves the state of
+ * the flow.
+ */
+ public void onSaveInstanceState(Bundle outState) {
+ outState.putInt(SAVED_STATE_EXPORT_STATE, mExportState);
+ if (mEntriesCount != null) {
+ outState.putInt(SAVED_STATE_ENTRIES_COUNT, mEntriesCount);
+ }
+ if (mExportFileUri != null) {
+ outState.putString(SAVED_STATE_EXPORT_FILE_URI, mExportFileUri.toString());
+ }
+ }
+
+ /**
+ * Returns whether the password export feature is ready to use.
+ * @return Returns true if the Reauthentication Api is available.
+ */
+ public static boolean providesPasswordExport() {
+ return ReauthenticationManager.isReauthenticationApiAvailable();
+ }
+
+ private int getPositiveButtonLabelId() {
+ // Don't allow to try restarting the export flow from password access loss warning. The
+ // reason: it won't be able to create the file for saving passwords on disk because the
+ // dialog, which owns the export flow would be dismissed. There is a workaround: clicking on
+ // Google Password Manager will propose to restart the export flow.
+ // TODO (crbug.com/364530583): returning 0 here means there should be only one "Close"
+ // button in the dialog. Make error dialog configurable instead of passing a 0 resource into
+ // it.
+ return 0;
+ }
+}
diff --git a/chrome/browser/password_manager/android/java/src/org/chromium/chrome/browser/password_manager/settings/ExportFlowInterface.java b/chrome/browser/password_manager/android/java/src/org/chromium/chrome/browser/password_manager/settings/ExportFlowInterface.java
new file mode 100644
--- /dev/null
+++ b/chrome/browser/password_manager/android/java/src/org/chromium/chrome/browser/password_manager/settings/ExportFlowInterface.java
@@ -0,0 +1,90 @@
+// Copyright 2023 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.chrome.browser.password_manager.settings;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentManager;
+
+import org.chromium.build.annotations.NullMarked;
+import org.chromium.chrome.browser.profiles.Profile;
+
+/** An interface for the implementations of {@link ExportFlow}. */
+@NullMarked
+public interface ExportFlowInterface {
+ /** The delegate to provide ExportFlow with essential information from the owning fragment. */
+ public interface Delegate {
+ /**
+ * @return The activity associated with the owning fragment.
+ */
+ Activity getActivity();
+
+ /**
+ * @return The fragment manager associated with the owning fragment.
+ */
+ FragmentManager getFragmentManager();
+
+ /**
+ * @return The ID of the root view of the owning fragment.
+ */
+ int getViewId();
+
+ /**
+ * Runs the activity on the fragment owning the export flow.
+ *
+ * @param intent The intent to start an activity.
+ */
+ void runCreateFileOnDiskIntent(Intent intent);
+
+ /**
+ * Performs the actions that should happen after the export flow has successfully finished.
+ */
+ default void onExportFlowSucceeded() {}
+
+ /** Performs the actions that should happen after the export flow has failed. */
+ default void onExportFlowFailed() {}
+
+ /** Notifies about that export flow has been canceled. */
+ default void onExportFlowCanceled() {}
+
+ /** Return the {@link Profile} associated with the passwords. */
+ Profile getProfile();
+ }
+
+ /**
+ * A hook to be used in the onCreate method of the owning {@link Fragment}. I restores the state
+ * of the flow.
+ *
+ * @param savedInstanceState The {@link Bundle} passed from the fragment's onCreate method.
+ * @param delegate The {@link Delegate} for this ExportFlow.
+ * @param callerMetricsId The unique string, which identifies the caller. This will be used as
+ * the prefix for metrics histograms names.
+ */
+ public void onCreate(Bundle savedInstanceState, Delegate delegate, String callerMetricsId);
+
+ /** Starts the password export flow. */
+ public void startExporting();
+
+ /**
+ * A hook to be used in a {@link Fragment}'s onResume method. I processes the result of the
+ * reauthentication.
+ */
+ public void onResume();
+
+ /** Continues the export flow when password list is available. */
+ public void passwordsAvailable();
+
+ /**
+ * Saves the passwords into the file (in the form of Uri) passed in.
+ *
+ * @param passwordsFile The file into which the passwords will be written (expected to be a file
+ * Uri).
+ */
+ void savePasswordsToDownloads(Uri passwordsFile);
+}
diff --git a/chrome/browser/password_manager/android/java/src/org/chromium/chrome/browser/password_manager/settings/NonCancelableProgressBar.java b/chrome/browser/password_manager/android/java/src/org/chromium/chrome/browser/password_manager/settings/NonCancelableProgressBar.java
new file mode 100644
--- /dev/null
+++ b/chrome/browser/password_manager/android/java/src/org/chromium/chrome/browser/password_manager/settings/NonCancelableProgressBar.java
@@ -0,0 +1,55 @@
+// Copyright 2023 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.chrome.browser.password_manager.settings;
+
+import android.app.Activity;
+import android.app.Dialog;
+import android.os.Bundle;
+import android.view.View;
+
+import androidx.appcompat.app.AlertDialog;
+import androidx.fragment.app.DialogFragment;
+
+import org.chromium.build.annotations.NullMarked;
+import org.chromium.build.annotations.Nullable;
+import org.chromium.chrome.browser.password_manager.R;
+import org.chromium.components.browser_ui.widget.MaterialProgressBar;
+
+/**
+ * Shows the dialog that informs the user that some operation is ongoing without indicating the
+ * progress.
+ */
+@NullMarked
+public class NonCancelableProgressBar extends DialogFragment {
+ private final int mTitleStringId;
+
+ public NonCancelableProgressBar() {
+ mTitleStringId = R.string.please_wait_progress_message;
+ }
+
+ public NonCancelableProgressBar(int titleStringId) {
+ mTitleStringId = titleStringId;
+ }
+
+ /**
+ * Opens the dialog with the progress bar 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(@Nullable Bundle savedInstanceState) {
+ Activity activity = getActivity();
+ View dialog =
+ activity.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(
+ activity, R.style.ThemeOverlay_BrowserUI_AlertDialog_NoActionBar)
+ .setView(dialog)
+ .setTitle(mTitleStringId)
+ .create();
+ }
+}
diff --git a/chrome/browser/password_manager/android/java/src/org/chromium/chrome/browser/password_manager/settings/PasswordAccessReauthenticationHelper.java b/chrome/browser/password_manager/android/java/src/org/chromium/chrome/browser/password_manager/settings/PasswordAccessReauthenticationHelper.java
new file mode 100644
--- /dev/null
+++ b/chrome/browser/password_manager/android/java/src/org/chromium/chrome/browser/password_manager/settings/PasswordAccessReauthenticationHelper.java
@@ -0,0 +1,143 @@
+// Copyright 2020 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.chrome.browser.password_manager.settings;
+
+import android.content.Context;
+import android.view.View;
+
+import androidx.annotation.IntDef;
+import androidx.fragment.app.FragmentManager;
+
+import org.chromium.base.Callback;
+import org.chromium.base.metrics.RecordHistogram;
+import org.chromium.build.annotations.NullMarked;
+import org.chromium.build.annotations.Nullable;
+import org.chromium.chrome.browser.device_reauth.ReauthResult;
+import org.chromium.chrome.browser.password_manager.R;
+import org.chromium.chrome.browser.password_manager.settings.ReauthenticationManager.ReauthScope;
+import org.chromium.ui.widget.Toast;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * A helper to perform a user's reauthentication for a specific {@link ReauthReason}. Only a single
+ * reauthentication can happen at a given time.
+ */
+@NullMarked
+public class PasswordAccessReauthenticationHelper {
+ public static final String SETTINGS_REAUTHENTICATION_HISTOGRAM =
+ "PasswordManager.ReauthToAccessPasswordInSettings";
+
+ /**
+ * The reason for the reauthentication.
+ *
+ * <p>TODO(crbug.com/40170183): Remove the edit reason once the password check credential editor
+ * is completely replaced with the new one.
+ */
+ @IntDef({ReauthReason.VIEW_PASSWORD, ReauthReason.EDIT_PASSWORD, ReauthReason.COPY_PASSWORD})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface ReauthReason {
+ /** A reauthentication is required for viewing a password. */
+ int VIEW_PASSWORD = 0;
+
+ /** A reauthentication is required for editing a password. */
+ int EDIT_PASSWORD = 1;
+
+ /** Reauthentication is required in order to copy a password. */
+ int COPY_PASSWORD = 2;
+ }
+
+ private final Context mContext;
+ private final FragmentManager mFragmentManager;
+ private @Nullable Callback<Boolean> mCallback;
+
+ public PasswordAccessReauthenticationHelper(Context context, FragmentManager fragmentManager) {
+ mContext = context;
+ mFragmentManager = fragmentManager;
+ }
+
+ public boolean canReauthenticate() {
+ return ReauthenticationManager.isScreenLockSetUp(mContext);
+ }
+
+ /**
+ * Asks the user to reauthenticate. Requires {@link #canReauthenticate()}.
+ * @param reason The {@link ReauthReason} for the reauth.
+ * @param callback A {@link Callback}. Will invoke {@link Callback#onResult} with whether the
+ * user passed or dismissed the reauth screen.
+ */
+ public void reauthenticate(@ReauthReason int reason, Callback<Boolean> callback) {
+ assert canReauthenticate();
+ assert mCallback == null;
+
+ // Invoke the handler immediately if an authentication is still valid.
+ if (ReauthenticationManager.authenticationStillValid(ReauthScope.ONE_AT_A_TIME)) {
+ RecordHistogram.recordEnumeratedHistogram(
+ SETTINGS_REAUTHENTICATION_HISTOGRAM,
+ ReauthResult.SKIPPED,
+ ReauthResult.MAX_VALUE);
+
+ callback.onResult(true);
+ return;
+ }
+
+ mCallback = callback;
+
+ switch (reason) {
+ case ReauthReason.VIEW_PASSWORD:
+ ReauthenticationManager.displayReauthenticationFragment(
+ R.string.lockscreen_description_view,
+ View.NO_ID,
+ mFragmentManager,
+ ReauthScope.ONE_AT_A_TIME);
+ break;
+ case ReauthReason.EDIT_PASSWORD:
+ ReauthenticationManager.displayReauthenticationFragment(
+ R.string.lockscreen_description_edit,
+ View.NO_ID,
+ mFragmentManager,
+ ReauthScope.ONE_AT_A_TIME);
+ break;
+ case ReauthReason.COPY_PASSWORD:
+ ReauthenticationManager.displayReauthenticationFragment(
+ R.string.lockscreen_description_copy,
+ View.NO_ID,
+ mFragmentManager,
+ ReauthScope.ONE_AT_A_TIME);
+ break;
+ }
+ }
+
+ /**
+ * Shows a toast to the user nudging them to set up a screen lock. Intended to be called in case
+ * {@link #canReauthenticate()} returns false.
+ */
+ public void showScreenLockToast(@ReauthReason int reason) {
+ if (reason == ReauthReason.COPY_PASSWORD) {
+ Toast.makeText(
+ mContext,
+ R.string.password_entry_copy_set_screen_lock,
+ Toast.LENGTH_LONG)
+ .show();
+ return;
+ }
+ Toast.makeText(mContext, R.string.password_entry_view_set_screen_lock, Toast.LENGTH_LONG)
+ .show();
+ }
+
+ /**
+ * Invoked when a reauthentication might have happened. Invokes {@link Callback#onResult}
+ * with whether the user passed the reauthentication challenge.
+ * No-op if {@link #mCallback} is null.
+ */
+ public void onReauthenticationMaybeHappened() {
+ if (mCallback != null) {
+ mCallback.onResult(
+ ReauthenticationManager.authenticationStillValid(ReauthScope.ONE_AT_A_TIME));
+ mCallback = null;
+ }
+ }
+}
diff --git a/chrome/browser/password_manager/android/java/src/org/chromium/chrome/browser/password_manager/settings/PasswordListObserver.java b/chrome/browser/password_manager/android/java/src/org/chromium/chrome/browser/password_manager/settings/PasswordListObserver.java
new file mode 100644
--- /dev/null
+++ b/chrome/browser/password_manager/android/java/src/org/chromium/chrome/browser/password_manager/settings/PasswordListObserver.java
@@ -0,0 +1,28 @@
+// Copyright 2023 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.chrome.browser.password_manager.settings;
+
+import org.chromium.build.annotations.NullMarked;
+
+/**
+ * An interface which a client can use to listen to changes to password and password exception
+ * lists.
+ */
+@NullMarked
+public interface PasswordListObserver {
+ /**
+ * Called when passwords list is updated.
+ *
+ * @param count Number of entries in the password list.
+ */
+ void passwordListAvailable(int count);
+
+ /**
+ * Called when password exceptions list is updated.
+ *
+ * @param count Number of entries in the password exception list.
+ */
+ void passwordExceptionListAvailable(int count);
+}
diff --git a/chrome/browser/password_manager/android/java/src/org/chromium/chrome/browser/password_manager/settings/PasswordManagerHandler.java b/chrome/browser/password_manager/android/java/src/org/chromium/chrome/browser/password_manager/settings/PasswordManagerHandler.java
new file mode 100644
--- /dev/null
+++ b/chrome/browser/password_manager/android/java/src/org/chromium/chrome/browser/password_manager/settings/PasswordManagerHandler.java
@@ -0,0 +1,86 @@
+// Copyright 2017 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.chrome.browser.password_manager.settings;
+
+import android.content.Context;
+
+import org.chromium.base.Callback;
+import org.chromium.base.IntStringCallback;
+import org.chromium.build.annotations.NullMarked;
+import org.chromium.ui.base.WindowAndroid;
+
+/**
+ * Interface for retrieving passwords and password exceptions (websites for which Chrome should not
+ * save password) from native code.
+ */
+@NullMarked
+public interface PasswordManagerHandler {
+ /** Called to insert a password entry into the password store. */
+ public void insertPasswordEntryForTesting(String origin, String username, String password);
+
+ /** Called to start fetching password and exception lists. */
+ void updatePasswordLists();
+
+ /**
+ * Get the saved password entry at index.
+ *
+ * @param index Index of Password.
+ * @return SavedPasswordEntry at index.
+ */
+ SavedPasswordEntry getSavedPasswordEntry(int index);
+
+ /**
+ * Get saved password exception at index.
+ *
+ * @param index of exception
+ * @return Origin of password exception.
+ */
+ String getSavedPasswordException(int index);
+
+ /**
+ * Remove saved password entry at index.
+ *
+ * @param index of password entry to remove.
+ */
+ void removeSavedPasswordEntry(int index);
+
+ /**
+ * Remove saved exception entry at index.
+ *
+ * @param index of exception entry.
+ */
+ void removeSavedPasswordException(int index);
+
+ /**
+ * Trigger serializing the saved passwords in the background.
+ *
+ * @param targetPath is the file to which the serialized passwords should be written.
+ * @param successCallback is called on successful completion, with the count of the serialized
+ * passwords and the path to the file containing them as argument.
+ * @param errorCallback is called on failure, with the error message as argument.
+ */
+ void serializePasswords(
+ String targetPath, IntStringCallback successCallback, Callback<String> errorCallback);
+
+ /**
+ * Show the UI that allows to edit saved credentials.
+ *
+ * @param context the current Activity to launch the edit view from, or an application context
+ * if no Activity is available.
+ * @param index the index of the password entry to edit
+ * @param isBlockedCredential whether this credential is blocked for saving
+ */
+ void showPasswordEntryEditingView(Context context, int index, boolean isBlockedCredential);
+
+ /**
+ * Calls C++ to check whether the PasswordManagerHandler is still waiting for the passwords to
+ * be fetched from the password store.
+ *
+ * @return Returns true if the request to fetch the passwords is still pending.
+ */
+ boolean isWaitingForPasswordStore();
+
+ boolean startImporting(WindowAndroid window);
+}
diff --git a/chrome/browser/password_manager/android/java/src/org/chromium/chrome/browser/password_manager/settings/PasswordManagerHandlerProvider.java b/chrome/browser/password_manager/android/java/src/org/chromium/chrome/browser/password_manager/settings/PasswordManagerHandlerProvider.java
new file mode 100644
--- /dev/null
+++ b/chrome/browser/password_manager/android/java/src/org/chromium/chrome/browser/password_manager/settings/PasswordManagerHandlerProvider.java
@@ -0,0 +1,144 @@
+// Copyright 2017 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.chrome.browser.password_manager.settings;
+
+import static org.chromium.build.NullUtil.assumeNonNull;
+
+import org.chromium.base.ObserverList;
+import org.chromium.base.ThreadUtils;
+import org.chromium.base.lifetime.Destroyable;
+import org.chromium.build.annotations.NullMarked;
+import org.chromium.build.annotations.Nullable;
+import org.chromium.chrome.browser.profiles.Profile;
+import org.chromium.chrome.browser.profiles.ProfileKeyedMap;
+
+/**
+ * A provider for PasswordManagerHandler implementations, handling the choice of the proper one
+ * (production vs. testing), its lifetime and multiple observers.
+ *
+ * <p>This class is used by the code responsible for Chrome's passwords settings. There can only be
+ * one instance of Chrome's passwords settings opened at a time (although more clients of
+ * PasswordManagerHandler can live as nested settings pages).
+ */
+@NullMarked
+public class PasswordManagerHandlerProvider implements PasswordListObserver, Destroyable {
+ private static @Nullable ProfileKeyedMap<PasswordManagerHandlerProvider> sProfileMap;
+
+ /** Return the {@link PasswordManagerHandlerProvider} for the given {@link Profile}. */
+ public static PasswordManagerHandlerProvider getForProfile(Profile profile) {
+ if (sProfileMap == null) {
+ sProfileMap =
+ ProfileKeyedMap.createMapOfDestroyables(
+ ProfileKeyedMap.ProfileSelection.REDIRECTED_TO_ORIGINAL);
+ }
+ return sProfileMap.getForProfile(profile, PasswordManagerHandlerProvider::new);
+ }
+
+ private final Profile mProfile;
+
+ // The production implementation of PasswordManagerHandler is |sPasswordUiView|, instantiated on
+ // demand. Tests might want to override that by providing a fake implementation through
+ // setPasswordManagerHandlerForTest, which is then kept in |mTestPasswordManagerHandler|.
+ private @Nullable PasswordUiView mPasswordUiView;
+ private @Nullable PasswordManagerHandler mTestPasswordManagerHandler;
+
+ // This class is itself a PasswordListObserver, listening directly to a PasswordManagerHandler
+ // implementation. But it also keeps a list of other observers, to which it forwards the events.
+ private final ObserverList<PasswordListObserver> mObservers = new ObserverList<>();
+
+ private PasswordManagerHandlerProvider(Profile profile) {
+ mProfile = profile;
+ }
+
+ /**
+ * Sets a testing implementation of PasswordManagerHandler to be used. It overrides the
+ * production one even if it exists. The caller needs to ensure that |this| becomes an observer
+ * of |passwordManagerHandler|. Also, this must not be called when there are already some
+ * observers in |mObservers|, because of special handling of the production implementation of
+ * PasswordManagerHandler on removing the last observer.
+ */
+ public void setPasswordManagerHandlerForTest(PasswordManagerHandler passwordManagerHandler) {
+ ThreadUtils.assertOnUiThread();
+ assert mObservers.isEmpty();
+ mTestPasswordManagerHandler = passwordManagerHandler;
+ }
+
+ /**
+ * Resets the testing implementation of PasswordManagerHandler, clears all observers and ensures
+ * that the view is cleaned up properly.
+ */
+ public void resetPasswordManagerHandlerForTest() {
+ ThreadUtils.assertOnUiThread();
+ mObservers.clear();
+ mTestPasswordManagerHandler = null;
+ if (mPasswordUiView != null) {
+ mPasswordUiView.destroy();
+ mPasswordUiView = null;
+ }
+ }
+
+ @Override
+ public void destroy() {
+ if (mPasswordUiView != null) {
+ mPasswordUiView.destroy();
+ mPasswordUiView = null;
+ }
+ }
+
+ /**
+ * A convenience function to choose between the production and test PasswordManagerHandler
+ * implementation.
+ */
+ public @Nullable PasswordManagerHandler getPasswordManagerHandler() {
+ ThreadUtils.assertOnUiThread();
+ if (mTestPasswordManagerHandler != null) return mTestPasswordManagerHandler;
+ return mPasswordUiView;
+ }
+
+ /**
+ * This method creates the production implementation of PasswordManagerHandler and saves it into
+ * mPasswordUiView.
+ */
+ private void createPasswordManagerHandler() {
+ ThreadUtils.assertOnUiThread();
+ assert mPasswordUiView == null;
+ mPasswordUiView = new PasswordUiView(this, mProfile);
+ }
+
+ /** Starts forwarding events from the PasswordManagerHandler implementation to |observer|. */
+ public void addObserver(PasswordListObserver observer) {
+ ThreadUtils.assertOnUiThread();
+ if (getPasswordManagerHandler() == null) createPasswordManagerHandler();
+ mObservers.addObserver(observer);
+ }
+
+ public void removeObserver(PasswordListObserver observer) {
+ ThreadUtils.assertOnUiThread();
+ mObservers.removeObserver(observer);
+ // If this was the last observer of the production implementation of PasswordManagerHandler,
+ // call destroy on it to close the connection to the native C++ code.
+ if (mObservers.isEmpty() && mTestPasswordManagerHandler == null) {
+ assumeNonNull(mPasswordUiView);
+ mPasswordUiView.destroy();
+ mPasswordUiView = null;
+ }
+ }
+
+ @Override
+ public void passwordListAvailable(int count) {
+ ThreadUtils.assertOnUiThread();
+ for (PasswordListObserver observer : mObservers) {
+ observer.passwordListAvailable(count);
+ }
+ }
+
+ @Override
+ public void passwordExceptionListAvailable(int count) {
+ ThreadUtils.assertOnUiThread();
+ for (PasswordListObserver observer : mObservers) {
+ observer.passwordExceptionListAvailable(count);
+ }
+ }
+}
diff --git a/chrome/browser/password_manager/android/java/src/org/chromium/chrome/browser/password_manager/settings/PasswordReauthenticationFragment.java b/chrome/browser/password_manager/android/java/src/org/chromium/chrome/browser/password_manager/settings/PasswordReauthenticationFragment.java
new file mode 100644
--- /dev/null
+++ b/chrome/browser/password_manager/android/java/src/org/chromium/chrome/browser/password_manager/settings/PasswordReauthenticationFragment.java
@@ -0,0 +1,111 @@
+// Copyright 2017 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.chrome.browser.password_manager.settings;
+
+import static org.chromium.build.NullUtil.assertNonNull;
+import static org.chromium.chrome.browser.password_manager.settings.PasswordAccessReauthenticationHelper.SETTINGS_REAUTHENTICATION_HISTOGRAM;
+
+import android.app.Activity;
+import android.app.KeyguardManager;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentManager;
+
+import org.chromium.base.metrics.RecordHistogram;
+import org.chromium.build.annotations.NullMarked;
+import org.chromium.build.annotations.Nullable;
+import org.chromium.chrome.browser.device_reauth.ReauthResult;
+
+/** Show the lock screen confirmation and lock the screen. */
+@NullMarked
+public class PasswordReauthenticationFragment extends Fragment {
+ /**
+ * The key for the description argument, which is used to retrieve an explanation of the
+ * reauthentication prompt to the user.
+ */
+ public static final String DESCRIPTION_ID = "description";
+
+ /**
+ * The key for the scope, with values from {@link ReauthenticationManager.ReauthScope}. The
+ * scope enum value corresponds to what is indicated in the description message for the user
+ * (e.g., if the message mentions "export passwords", the scope should be BULK, but for "view
+ * password" it should be ONE_AT_A_TIME).
+ */
+ public static final String SCOPE_ID = "scope";
+
+ protected static final int CONFIRM_DEVICE_CREDENTIAL_REQUEST_CODE = 2;
+
+ protected static final String HAS_BEEN_SUSPENDED_KEY = "has_been_suspended";
+
+ private static boolean sPreventLockDevice;
+
+ private FragmentManager mFragmentManager;
+
+ @Override
+ public void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ mFragmentManager = assertNonNull(getFragmentManager());
+ boolean isFirstTime = savedInstanceState == null;
+ if (!sPreventLockDevice && isFirstTime) {
+ lockDevice();
+ }
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ // On Android L, an empty |outState| would degrade to null in |onCreate|, making Chrome
+ // unable to distinguish the first time launch. Insert a value into |outState| to prevent
+ // that.
+ outState.putBoolean(HAS_BEEN_SUSPENDED_KEY, true);
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+ if (requestCode == CONFIRM_DEVICE_CREDENTIAL_REQUEST_CODE) {
+ if (resultCode == Activity.RESULT_OK) {
+ RecordHistogram.recordEnumeratedHistogram(
+ SETTINGS_REAUTHENTICATION_HISTOGRAM,
+ ReauthResult.SUCCESS,
+ ReauthResult.MAX_VALUE);
+ ReauthenticationManager.recordLastReauth(
+ System.currentTimeMillis(), getArguments().getInt(SCOPE_ID));
+ } else {
+ RecordHistogram.recordEnumeratedHistogram(
+ SETTINGS_REAUTHENTICATION_HISTOGRAM,
+ ReauthResult.FAILURE,
+ ReauthResult.MAX_VALUE);
+ ReauthenticationManager.resetLastReauth();
+ }
+ mFragmentManager.popBackStack();
+ }
+ }
+
+ /** Prevent calling the {@link #lockDevice} method in {@link #onCreate}. */
+ public static void preventLockingForTesting() {
+ sPreventLockDevice = true;
+ }
+
+ private void lockDevice() {
+ KeyguardManager keyguardManager =
+ (KeyguardManager) getActivity().getSystemService(Context.KEYGUARD_SERVICE);
+ final int resourceId = getArguments().getInt(DESCRIPTION_ID, 0);
+ // Forgetting to set the DESCRIPTION_ID is an error on the callsite.
+ assert resourceId != 0;
+ // Set title to null to use the system default title which is adapted to the particular type
+ // of device lock which the user set up.
+ Intent intent =
+ keyguardManager.createConfirmDeviceCredentialIntent(null, getString(resourceId));
+ if (intent != null) {
+ startActivityForResult(intent, CONFIRM_DEVICE_CREDENTIAL_REQUEST_CODE);
+ return;
+ }
+ mFragmentManager.popBackStackImmediate();
+ }
+}
diff --git a/chrome/browser/password_manager/android/java/src/org/chromium/chrome/browser/password_manager/settings/PasswordUiView.java b/chrome/browser/password_manager/android/java/src/org/chromium/chrome/browser/password_manager/settings/PasswordUiView.java
new file mode 100644
--- /dev/null
+++ b/chrome/browser/password_manager/android/java/src/org/chromium/chrome/browser/password_manager/settings/PasswordUiView.java
@@ -0,0 +1,223 @@
+// Copyright 2013 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.chrome.browser.password_manager.settings;
+
+import android.content.Context;
+
+import org.jni_zero.CalledByNative;
+import org.jni_zero.JniType;
+import org.jni_zero.NativeMethods;
+
+import org.chromium.base.Callback;
+import org.chromium.base.IntStringCallback;
+import org.chromium.ui.base.WindowAndroid;
+import org.chromium.build.annotations.NullMarked;
+import org.chromium.chrome.browser.profiles.Profile;
+
+/**
+ * Production implementation of PasswordManagerHandler, making calls to native C++ code to retrieve
+ * the data.
+ */
+@NullMarked
+public final class PasswordUiView implements PasswordManagerHandler {
+ @CalledByNative
+ private static SavedPasswordEntry createSavedPasswordEntry(
+ @JniType("std::string") String url,
+ @JniType("std::u16string") String name,
+ @JniType("std::u16string") String password) {
+ return new SavedPasswordEntry(url, name, password);
+ }
+
+ // Pointer to native implementation, set to 0 in destroy().
+ private long mNativePasswordUiViewAndroid;
+
+ // This class has exactly one observer, set on construction and expected to last at least as
+ // long as this object (a good candidate is the owner of this object).
+ private final PasswordListObserver mObserver;
+
+ /**
+ * Constructor creates the native object as well. Callers should call destroy() after usage.
+ *
+ * @param observer The only observer.
+ * @param profile The {@link Profile} associated with these passwords.
+ */
+ public PasswordUiView(PasswordListObserver observer, Profile profile) {
+ mNativePasswordUiViewAndroid = PasswordUiViewJni.get().init(PasswordUiView.this, profile);
+ mObserver = observer;
+ }
+
+ @CalledByNative
+ private void passwordListAvailable(int count) {
+ mObserver.passwordListAvailable(count);
+ }
+
+ @CalledByNative
+ private void passwordExceptionListAvailable(int count) {
+ mObserver.passwordExceptionListAvailable(count);
+ }
+
+ @Override
+ public void insertPasswordEntryForTesting(String origin, String username, String password) {
+ PasswordUiViewJni.get()
+ .insertPasswordEntryForTesting(
+ mNativePasswordUiViewAndroid, origin, username, password);
+ }
+
+ // Calls native to refresh password and exception lists. The native code calls back into
+ // passwordListAvailable and passwordExceptionListAvailable.
+ @Override
+ public void updatePasswordLists() {
+ PasswordUiViewJni.get()
+ .updatePasswordLists(mNativePasswordUiViewAndroid, PasswordUiView.this);
+ }
+
+ @Override
+ public SavedPasswordEntry getSavedPasswordEntry(int index) {
+ return PasswordUiViewJni.get()
+ .getSavedPasswordEntry(mNativePasswordUiViewAndroid, PasswordUiView.this, index);
+ }
+
+ @Override
+ public String getSavedPasswordException(int index) {
+ return PasswordUiViewJni.get()
+ .getSavedPasswordException(
+ mNativePasswordUiViewAndroid, PasswordUiView.this, index);
+ }
+
+ @Override
+ public void removeSavedPasswordEntry(int index) {
+ PasswordUiViewJni.get()
+ .handleRemoveSavedPasswordEntry(
+ mNativePasswordUiViewAndroid, PasswordUiView.this, index);
+ }
+
+ @Override
+ public void removeSavedPasswordException(int index) {
+ PasswordUiViewJni.get()
+ .handleRemoveSavedPasswordException(
+ mNativePasswordUiViewAndroid, PasswordUiView.this, index);
+ }
+
+ @Override
+ public void serializePasswords(
+ String targetPath, IntStringCallback successCallback, Callback<String> errorCallback) {
+ PasswordUiViewJni.get()
+ .handleSerializePasswords(
+ mNativePasswordUiViewAndroid,
+ PasswordUiView.this,
+ targetPath,
+ successCallback,
+ errorCallback);
+ }
+
+ @Override
+ public void showPasswordEntryEditingView(
+ Context context, int index, boolean isBlockedCredential) {
+ if (isBlockedCredential) {
+ PasswordUiViewJni.get()
+ .handleShowBlockedCredentialView(
+ mNativePasswordUiViewAndroid, context, index, PasswordUiView.this);
+ return;
+ }
+ PasswordUiViewJni.get()
+ .handleShowPasswordEntryEditingView(
+ mNativePasswordUiViewAndroid, context, index, PasswordUiView.this);
+ }
+
+ /**
+ * Returns the URL for the website for managing one's passwords without the need to use Chrome
+ * with the user's profile signed in.
+ *
+ * @return The string with the URL.
+ */
+ public static String getAccountDashboardURL() {
+ return PasswordUiViewJni.get().getAccountDashboardURL();
+ }
+
+ /**
+ * Returns the URL of the help center article about trusted vault encryption.
+ *
+ * @return The string with the URL.
+ */
+ public static String getTrustedVaultLearnMoreURL() {
+ return PasswordUiViewJni.get().getTrustedVaultLearnMoreURL();
+ }
+
+ @Override
+ public boolean isWaitingForPasswordStore() {
+ return PasswordUiViewJni.get()
+ .isWaitingForPasswordStore(mNativePasswordUiViewAndroid, PasswordUiView.this);
+ }
+
+ /** Destroy the native object. */
+ public void destroy() {
+ if (mNativePasswordUiViewAndroid != 0) {
+ PasswordUiViewJni.get().destroy(mNativePasswordUiViewAndroid, PasswordUiView.this);
+ mNativePasswordUiViewAndroid = 0;
+ }
+ }
+
+ @Override
+ public boolean startImporting(WindowAndroid window) {
+ return PasswordUiViewJni.get()
+ .startImporting(mNativePasswordUiViewAndroid, PasswordUiView.this, window);
+ }
+
+ @NativeMethods
+ interface Natives {
+ long init(PasswordUiView caller, @JniType("Profile*") Profile profile);
+
+ void insertPasswordEntryForTesting(
+ long nativePasswordUiViewAndroid,
+ @JniType("std::u16string") String origin,
+ @JniType("std::u16string") String username,
+ @JniType("std::u16string") String password);
+
+ void updatePasswordLists(long nativePasswordUiViewAndroid, PasswordUiView caller);
+
+ SavedPasswordEntry getSavedPasswordEntry(
+ long nativePasswordUiViewAndroid, PasswordUiView caller, int index);
+
+ @JniType("std::string")
+ String getSavedPasswordException(
+ long nativePasswordUiViewAndroid, PasswordUiView caller, int index);
+
+ void handleRemoveSavedPasswordEntry(
+ long nativePasswordUiViewAndroid, PasswordUiView caller, int index);
+
+ void handleRemoveSavedPasswordException(
+ long nativePasswordUiViewAndroid, PasswordUiView caller, int index);
+
+ @JniType("std::string")
+ String getAccountDashboardURL();
+
+ @JniType("std::string")
+ String getTrustedVaultLearnMoreURL();
+
+ boolean isWaitingForPasswordStore(long nativePasswordUiViewAndroid, PasswordUiView caller);
+ boolean startImporting(long nativePasswordUiViewAndroid, PasswordUiView caller, WindowAndroid window);
+
+ void destroy(long nativePasswordUiViewAndroid, PasswordUiView caller);
+
+ void handleSerializePasswords(
+ long nativePasswordUiViewAndroid,
+ PasswordUiView caller,
+ @JniType("std::string") String targetPath,
+ IntStringCallback successCallback,
+ Callback<String> errorCallback);
+
+ void handleShowPasswordEntryEditingView(
+ long nativePasswordUiViewAndroid,
+ Context context,
+ int index,
+ PasswordUiView caller);
+
+ void handleShowBlockedCredentialView(
+ long nativePasswordUiViewAndroid,
+ Context context,
+ int index,
+ PasswordUiView caller);
+ }
+}
diff --git a/chrome/browser/password_manager/android/java/src/org/chromium/chrome/browser/password_manager/settings/PasswordsPreference.java b/chrome/browser/password_manager/android/java/src/org/chromium/chrome/browser/password_manager/settings/PasswordsPreference.java
--- a/chrome/browser/password_manager/android/java/src/org/chromium/chrome/browser/password_manager/settings/PasswordsPreference.java
+++ b/chrome/browser/password_manager/android/java/src/org/chromium/chrome/browser/password_manager/settings/PasswordsPreference.java
@@ -87,6 +87,7 @@ public class PasswordsPreference extends ChromeBasePreference implements Profile
}
private void setUpPostDeprecationWarning(PreferenceViewHolder holder) {
+ if ((true)) return;
assert mProfile != null : "Profile is not set!";
boolean isPasswordManagerAvailable = PasswordManagerUtilBridge.isPasswordManagerAvailable();
diff --git a/chrome/browser/password_manager/android/java/src/org/chromium/chrome/browser/password_manager/settings/ProgressBarDialogFragment.java b/chrome/browser/password_manager/android/java/src/org/chromium/chrome/browser/password_manager/settings/ProgressBarDialogFragment.java
new file mode 100644
--- /dev/null
+++ b/chrome/browser/password_manager/android/java/src/org/chromium/chrome/browser/password_manager/settings/ProgressBarDialogFragment.java
@@ -0,0 +1,70 @@
+// Copyright 2018 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.chrome.browser.password_manager.settings;
+import org.chromium.build.annotations.Initializer;
+import org.chromium.build.annotations.NullMarked;
+
+import android.app.Dialog;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import android.view.View;
+
+import androidx.appcompat.app.AlertDialog;
+import androidx.fragment.app.DialogFragment;
+
+//import org.chromium.chrome.R;
+import org.chromium.chrome.browser.password_manager.R;
+import org.chromium.components.browser_ui.widget.MaterialProgressBar;
+import org.chromium.build.annotations.Nullable;
+
+/**
+ * Shows the dialog that informs the user about the progress of preparing passwords for export and
+ * allows the user to cancel that operation.
+ */
+@NullMarked
+public class ProgressBarDialogFragment extends DialogFragment {
+ // This handler is used to perform the user-triggered cancellation of the password preparation.
+ private DialogInterface.OnClickListener mHandler;
+
+ @Initializer
+ 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(@Nullable Bundle savedInstanceState) {
+ View dialog =
+ getActivity().getLayoutInflater().inflate(R.layout.passwords_progress_dialog, null);
+ MaterialProgressBar bar = dialog.findViewById(R.id.passwords_progress_bar);
+ bar.setIndeterminate(true);
+ return new AlertDialog.Builder(
+ getActivity(), R.style.ThemeOverlay_BrowserUI_AlertDialog_NoActionBar)
+ .setView(dialog)
+ .setNegativeButton(R.string.cancel, mHandler)
+ .setTitle(
+ getActivity()
+ .getResources()
+ .getString(R.string.settings_passwords_preparing_export))
+ .create();
+ }
+
+ @Override
+ public void onCreate(@Nullable 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;
+ }
+ }
+}
diff --git a/chrome/browser/password_manager/android/java/src/org/chromium/chrome/browser/password_manager/settings/ReauthenticationManager.java b/chrome/browser/password_manager/android/java/src/org/chromium/chrome/browser/password_manager/settings/ReauthenticationManager.java
new file mode 100644
--- /dev/null
+++ b/chrome/browser/password_manager/android/java/src/org/chromium/chrome/browser/password_manager/settings/ReauthenticationManager.java
@@ -0,0 +1,198 @@
+// Copyright 2017 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.chrome.browser.password_manager.settings;
+
+import android.app.KeyguardManager;
+import android.content.Context;
+import android.os.Bundle;
+import android.view.View;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.VisibleForTesting;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentManager;
+import androidx.fragment.app.FragmentTransaction;
+
+import org.chromium.build.annotations.NullMarked;
+import org.chromium.build.annotations.Nullable;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * This collection of static methods provides reauthentication primitives for passwords settings UI.
+ */
+@NullMarked
+public final class ReauthenticationManager {
+ // Used for various ways to override checks provided by this class.
+ @IntDef({OverrideState.NOT_OVERRIDDEN, OverrideState.AVAILABLE, OverrideState.UNAVAILABLE})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface OverrideState {
+ int NOT_OVERRIDDEN = 0;
+ int AVAILABLE = 1;
+ int UNAVAILABLE = 2;
+ }
+
+ // Used to specify the scope of the reauthentication -- either to grant bulk access like, e.g.,
+ // exporting passwords, or just one-at-a-time, like, e.g., viewing a single password.
+ @IntDef({ReauthScope.ONE_AT_A_TIME, ReauthScope.BULK})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface ReauthScope {
+ int ONE_AT_A_TIME = 0;
+ int BULK = 1;
+ }
+
+ // Useful for retrieving the fragment in tests.
+ @VisibleForTesting
+ public static final String FRAGMENT_TAG = "reauthentication-manager-fragment";
+
+ // Defines how long a successful reauthentication remains valid.
+ @VisibleForTesting public static final int VALID_REAUTHENTICATION_TIME_INTERVAL_MILLIS = 60000;
+
+ // Used for verifying if the last successful reauthentication is still valid. The null value
+ // means there was no successful reauthentication yet.
+ private static @Nullable Long sLastReauthTimeMillis;
+
+ // Stores the reauth scope used when |sLastReauthTimeMillis| was reset last time.
+ private static @ReauthScope int sLastReauthScope = ReauthScope.ONE_AT_A_TIME;
+
+ // Used in tests to override the result of checking for screen lock set-up. This allows the
+ // tests to be independent of a particular device configuration.
+ private static @OverrideState int sScreenLockSetUpOverride = OverrideState.NOT_OVERRIDDEN;
+
+ // Used in tests to override the result of checking for availability of the screen-locking API.
+ // This allows the tests to be independent of a particular device configuration.
+ private static @OverrideState int sApiOverride = OverrideState.NOT_OVERRIDDEN;
+
+ // Used in tests to avoid displaying the OS reauth dialog.
+ private static boolean sSkipSystemReauth;
+
+ /**
+ * Clears the record of the last reauth so that a call to authenticationStillValid will return
+ * false.
+ */
+ public static void resetLastReauth() {
+ sLastReauthTimeMillis = null;
+ sLastReauthScope = ReauthScope.ONE_AT_A_TIME;
+ }
+
+ /**
+ * Stores the timestamp of last reauthentication of the user.
+ * @param timeStampMillis The time of the most recent successful user reauthentication.
+ * @param scope The scope of the reauthentication as advertised to the user via UI.
+ */
+ public static void recordLastReauth(long timeStampMillis, @ReauthScope int scope) {
+ sLastReauthTimeMillis = timeStampMillis;
+ sLastReauthScope = scope;
+ }
+
+ @VisibleForTesting
+ public static void setScreenLockSetUpOverride(@OverrideState int screenLockSetUpOverride) {
+ sScreenLockSetUpOverride = screenLockSetUpOverride;
+ }
+
+ @VisibleForTesting
+ public static void setApiOverride(@OverrideState int apiOverride) {
+ // Ensure that tests don't accidentally try to launch the OS-provided lock screen.
+ if (apiOverride == OverrideState.AVAILABLE) {
+ PasswordReauthenticationFragment.preventLockingForTesting();
+ }
+
+ sApiOverride = apiOverride;
+ }
+
+ @VisibleForTesting
+ public static void setSkipSystemReauth(boolean skipSystemReauth) {
+ sSkipSystemReauth = skipSystemReauth;
+ }
+
+ /**
+ * Checks whether reauthentication is available on the device at all.
+ * @return The result of the check.
+ */
+ public static boolean isReauthenticationApiAvailable() {
+ switch (sApiOverride) {
+ case OverrideState.NOT_OVERRIDDEN:
+ case OverrideState.AVAILABLE:
+ return true;
+ case OverrideState.UNAVAILABLE:
+ return false;
+ }
+ // This branch is not reachable.
+ assert false;
+ return false;
+ }
+
+ /**
+ * Initiates the reauthentication prompt with a given description.
+ *
+ * @param descriptionId The resource ID of the string to be displayed to explain the reason for
+ * the reauthentication.
+ * @param containerViewId The ID of the container, fragments of which will get replaced with the
+ * reauthentication prompt. It may be equal to View.NO_ID in tests or when coming from
+ * password check.
+ * @param fragmentManager For putting the lock screen on the transaction stack.
+ */
+ public static void displayReauthenticationFragment(
+ int descriptionId,
+ int containerViewId,
+ FragmentManager fragmentManager,
+ @ReauthScope int scope) {
+ if (sSkipSystemReauth) return;
+
+ Fragment passwordReauthentication = new PasswordReauthenticationFragment();
+ Bundle args = new Bundle();
+ args.putInt(PasswordReauthenticationFragment.DESCRIPTION_ID, descriptionId);
+ args.putInt(PasswordReauthenticationFragment.SCOPE_ID, scope);
+ passwordReauthentication.setArguments(args);
+
+ FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
+ if (containerViewId == View.NO_ID) {
+ fragmentTransaction.add(passwordReauthentication, FRAGMENT_TAG);
+ } else {
+ fragmentTransaction.replace(containerViewId, passwordReauthentication, FRAGMENT_TAG);
+ }
+ fragmentTransaction.addToBackStack(null);
+ fragmentTransaction.commit();
+ }
+
+ /**
+ * Checks whether authentication is recent enough to be valid. The authentication is valid as
+ * long as the user authenticated less than {@code VALID_REAUTHENTICATION_TIME_INTERVAL_MILLIS}
+ * milliseconds ago, for a scope including the passed {@code scope} argument. The {@code BULK}
+ * scope includes the {@code ONE_AT_A_TIME} scope.
+ *
+ * @param scope The scope the reauth should be valid for.
+ */
+ public static boolean authenticationStillValid(@ReauthScope int scope) {
+ final boolean scopeIncluded =
+ scope == sLastReauthScope || sLastReauthScope == ReauthScope.BULK;
+ return sLastReauthTimeMillis != null
+ && scopeIncluded
+ && (System.currentTimeMillis() - sLastReauthTimeMillis)
+ < VALID_REAUTHENTICATION_TIME_INTERVAL_MILLIS;
+ }
+
+ /**
+ * Checks whether the user set up screen lock so that it can be used for reauthentication. Can
+ * be overridden in tests.
+ *
+ * @param context The context to retrieve the KeyguardManager to find out.
+ */
+ public static boolean isScreenLockSetUp(Context context) {
+ switch (sScreenLockSetUpOverride) {
+ case OverrideState.NOT_OVERRIDDEN:
+ return ((KeyguardManager) context.getSystemService(Context.KEYGUARD_SERVICE))
+ .isKeyguardSecure();
+ case OverrideState.AVAILABLE:
+ return true;
+ case OverrideState.UNAVAILABLE:
+ return false;
+ }
+ // This branch is not reachable.
+ assert false;
+ return false;
+ }
+}
diff --git a/chrome/browser/password_manager/android/java/src/org/chromium/chrome/browser/password_manager/settings/SavedPasswordEntry.java b/chrome/browser/password_manager/android/java/src/org/chromium/chrome/browser/password_manager/settings/SavedPasswordEntry.java
new file mode 100644
--- /dev/null
+++ b/chrome/browser/password_manager/android/java/src/org/chromium/chrome/browser/password_manager/settings/SavedPasswordEntry.java
@@ -0,0 +1,41 @@
+// Copyright 2017 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.chrome.browser.password_manager.settings;
+
+import org.chromium.build.annotations.NullMarked;
+
+/**
+ * A class representing information about a saved password entry in Chrome's settngs.
+ *
+ * <p>Note: This could be a nested class in the PasswordManagerHandler interface, but that would
+ * mean that PasswordUiView, which implements that interface and references SavedPasswordEntry in
+ * some of its JNI-registered methods, would need an explicit import of PasswordManagerHandler. That
+ * again would violate our presubmit checks, and https://crbug.com/424792 indicates that the
+ * preferred solution is to move the nested class to top-level.
+ */
+@NullMarked
+public final class SavedPasswordEntry {
+ private final String mUrl;
+ private final String mName;
+ private final String mPassword;
+
+ public SavedPasswordEntry(String url, String name, String password) {
+ mUrl = url;
+ mName = name;
+ mPassword = password;
+ }
+
+ public String getUrl() {
+ return mUrl;
+ }
+
+ public String getUserName() {
+ return mName;
+ }
+
+ public String getPassword() {
+ return mPassword;
+ }
+}
diff --git a/chrome/browser/password_manager/android/java/src/org/chromium/chrome/browser/password_manager/settings/SingleThreadBarrierClosure.java b/chrome/browser/password_manager/android/java/src/org/chromium/chrome/browser/password_manager/settings/SingleThreadBarrierClosure.java
new file mode 100644
--- /dev/null
+++ b/chrome/browser/password_manager/android/java/src/org/chromium/chrome/browser/password_manager/settings/SingleThreadBarrierClosure.java
@@ -0,0 +1,49 @@
+// Copyright 2018 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.chrome.browser.password_manager.settings;
+
+import org.chromium.build.annotations.NullMarked;
+
+/**
+ * A Runnable which postpones running a given callback until it is itself run for a pre-defined
+ * number of times. It is inspired by the native //base/barrier_closure.*. Unlike the native code,
+ * SingleThreadBarrierClosure is only meant to be used on a single thread and is not thread-safe.
+ */
+@NullMarked
+public final class SingleThreadBarrierClosure implements Runnable {
+ /** Counts the remaining number of runs. */
+ private int mRemainingRuns;
+
+ /** The callback to be run when {@link #mRemainingRuns} reaches 0.*/
+ private final Runnable mCallback;
+
+ /**
+ * Construct a {@link Runnable} such that the first {@code runsExpected-1} calls to {@link #run}
+ * are a no-op and the last one runs {@code callback}.
+ * @param runsExpected The number of total {@link #run} calls expected.
+ * @param callback The callback to be run once called enough times.
+ */
+ public SingleThreadBarrierClosure(int runsExpected, Runnable callback) {
+ assert runsExpected > 0;
+ assert callback != null;
+ mRemainingRuns = runsExpected;
+ mCallback = callback;
+ }
+
+ @Override
+ public void run() {
+ if (mRemainingRuns == 0) return;
+ --mRemainingRuns;
+ if (mRemainingRuns == 0) mCallback.run();
+ }
+
+ /**
+ * Whether the next call to {@link #run} runs {@link #mCallback}.
+ * @return True if the next call to {@link #run} runs {@link #mCallback}, false otherwise.
+ */
+ public boolean isReady() {
+ return mRemainingRuns == 1;
+ }
+}
diff --git a/chrome/browser/password_manager/android/java/src/org/chromium/chrome/browser/password_manager/settings/TimedCallbackDelayer.java b/chrome/browser/password_manager/android/java/src/org/chromium/chrome/browser/password_manager/settings/TimedCallbackDelayer.java
new file mode 100644
--- /dev/null
+++ b/chrome/browser/password_manager/android/java/src/org/chromium/chrome/browser/password_manager/settings/TimedCallbackDelayer.java
@@ -0,0 +1,33 @@
+// Copyright 2018 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.chrome.browser.password_manager.settings;
+
+import android.os.Handler;
+
+import org.chromium.build.annotations.NullMarked;
+
+/** An implementation of {@link CallbackDelayer} which runs callbacks after a fixed time delay. */
+@NullMarked
+public final class TimedCallbackDelayer implements CallbackDelayer {
+ /** The {@link Handler} used to delay the callbacks. */
+ private final Handler mHandler = new Handler();
+
+ /** How long to delay callbacks, in milliseconds. */
+ private final long mDelayMillis;
+
+ /**
+ * Constructs a delayer which posts callbacks with a fixed time delay.
+ * @param delayMillis The common delay of the callbacks, in milliseconds.
+ */
+ public TimedCallbackDelayer(long delayMillis) {
+ assert delayMillis >= 0;
+ mDelayMillis = delayMillis;
+ }
+
+ @Override
+ public void delay(Runnable callback) {
+ mHandler.postDelayed(callback, mDelayMillis);
+ }
+}
diff --git a/chrome/browser/password_manager/android/password_manager_android_util.cc b/chrome/browser/password_manager/android/password_manager_android_util.cc
--- a/chrome/browser/password_manager/android/password_manager_android_util.cc
+++ b/chrome/browser/password_manager/android/password_manager_android_util.cc
@@ -16,6 +16,7 @@ namespace password_manager_android_util {
namespace {
bool HasMinGmsVersionForFullUpmSupport() {
+ if ((true)) return false;
std::string gms_version_str = base::android::device_info::gms_version_code();
int gms_version = 0;
// gms_version_code() must be converted to int for comparison, because it can
diff --git a/chrome/browser/password_manager/android/password_ui_view_android.cc b/chrome/browser/password_manager/android/password_ui_view_android.cc
new file mode 100644
--- /dev/null
+++ b/chrome/browser/password_manager/android/password_ui_view_android.cc
@@ -0,0 +1,450 @@
+// Copyright 2013 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/password_manager/android/password_ui_view_android.h"
+
+#include <algorithm>
+#include <memory>
+#include <vector>
+
+#include "base/android/callback_android.h"
+#include "base/android/int_string_callback.h"
+#include "base/android/jni_string.h"
+#include "base/android/jni_weak_ref.h"
+#include "base/android/scoped_java_ref.h"
+#include "base/files/file.h"
+#include "base/files/file_path.h"
+#include "base/files/file_util.h"
+#include "base/functional/bind.h"
+#include "base/functional/callback_helpers.h"
+#include "base/logging.h"
+#include "base/metrics/field_trial.h"
+#include "base/metrics/histogram_functions.h"
+#include "base/metrics/user_metrics.h"
+#include "base/metrics/user_metrics_action.h"
+#include "base/numerics/safe_conversions.h"
+#include "base/task/thread_pool.h"
+#include "base/threading/scoped_blocking_call.h"
+#include "chrome/browser/profiles/profile_manager.h"
+#include "chrome/common/url_constants.h"
+#include "chrome/grit/generated_resources.h"
+#include "components/password_manager/core/browser/export/password_csv_writer.h"
+#include "components/password_manager/core/browser/form_parsing/form_data_parser.h"
+#include "components/password_manager/core/browser/leak_detection/leak_detection_check_impl.h"
+#include "components/password_manager/core/browser/password_form.h"
+#include "components/password_manager/core/browser/password_ui_utils.h"
+#include "components/password_manager/core/browser/ui/credential_provider_interface.h"
+#include "components/password_manager/core/browser/ui/credential_ui_entry.h"
+#include "content/public/browser/browser_thread.h"
+#include "ui/base/l10n/l10n_util.h"
+#include "url/gurl.h"
+
+#include "chrome/browser/ui/select_file_policy/chrome_select_file_policy.h"
+#include "ui/shell_dialogs/selected_file_info.h"
+#include "ui/shell_dialogs/select_file_dialog.h"
+#include "ui/android/window_android.h"
+
+// Must come after other includes, because FromJniType() uses Profile.
+#include "chrome/browser/password_manager/android/jni_headers/PasswordUiView_jni.h"
+
+namespace {
+
+using base::android::JavaRef;
+using base::android::ScopedJavaLocalRef;
+using IsInsecureCredential = CredentialEditBridge::IsInsecureCredential;
+
+PasswordUiViewAndroid::SerializationResult SerializePasswords(
+ const base::FilePath& target_directory,
+ std::vector<password_manager::CredentialUIEntry> credentials) {
+ // The UI should not trigger serialization if there are no passwords.
+ base::UmaHistogramBoolean(
+ "PasswordManager.ExportAndroid.MoreThanZeroPasswords",
+ credentials.size() > 0);
+
+ // Creating a file will block the execution on I/O.
+ base::ScopedBlockingCall scoped_blocking_call(FROM_HERE,
+ base::BlockingType::WILL_BLOCK);
+
+ // Ensure that the target directory exists.
+ base::File::Error error = base::File::FILE_OK;
+ if (!base::CreateDirectoryAndGetError(target_directory, &error)) {
+ base::UmaHistogramExactLinear(
+ "PasswordManager.ExportAndroid.CreateDirectoryError", -error,
+ -base::File::Error::FILE_ERROR_MAX);
+ return {0, std::string(), base::File::ErrorToString(error)};
+ }
+
+ // Create a temporary file in the target directory to hold the serialized
+ // passwords.
+ base::FilePath export_file;
+ if (!base::CreateTemporaryFileInDir(target_directory, &export_file)) {
+ logging::SystemErrorCode error_code = logging::GetLastSystemErrorCode();
+ base::UmaHistogramExactLinear(
+ "PasswordManager.ExportAndroid.CreateTempFileError",
+ -base::File::OSErrorToFileError(error_code),
+ -base::File::Error::FILE_ERROR_MAX);
+ return {0, std::string(), logging::SystemErrorCodeToString(error_code)};
+ }
+
+ // Write the serialized data in CSV.
+ std::string data =
+ password_manager::PasswordCSVWriter::SerializePasswords(credentials);
+ if (!base::WriteFile(export_file, data)) {
+ logging::SystemErrorCode error_code = logging::GetLastSystemErrorCode();
+ base::UmaHistogramExactLinear(
+ "PasswordManager.ExportAndroid.WriteToTempFileError",
+ -base::File::OSErrorToFileError(error_code),
+ -base::File::Error::FILE_ERROR_MAX);
+ return {0, std::string(), logging::SystemErrorCodeToString(error_code)};
+ }
+
+ return {static_cast<int>(credentials.size()), export_file.value(),
+ std::string()};
+}
+
+} // namespace
+
+PasswordUiViewAndroid::PasswordUiViewAndroid(
+ JNIEnv* env,
+ const jni_zero::JavaRef<jobject>& obj,
+ Profile* profile)
+ : profile_(profile),
+ profile_store_(ProfilePasswordStoreFactory::GetForProfile(
+ profile,
+ ServiceAccessType::EXPLICIT_ACCESS)),
+ saved_passwords_presenter_(
+ AffiliationServiceFactory::GetForProfile(profile),
+ profile_store_,
+ AccountPasswordStoreFactory::GetForProfile(
+ profile,
+ ServiceAccessType::EXPLICIT_ACCESS)),
+ weak_java_ui_controller_(env, obj) {
+ saved_passwords_presenter_.AddObserver(this);
+ saved_passwords_presenter_.Init();
+}
+
+PasswordUiViewAndroid::~PasswordUiViewAndroid() {
+ saved_passwords_presenter_.RemoveObserver(this);
+ if (select_file_dialog_)
+ select_file_dialog_->ListenerDestroyed();
+}
+
+void PasswordUiViewAndroid::Destroy(JNIEnv*, const JavaRef<jobject>&) {
+ switch (state_) {
+ case State::ALIVE:
+ delete this;
+ break;
+ case State::ALIVE_SERIALIZATION_PENDING:
+ // Postpone the deletion until the pending tasks are completed, so that
+ // the tasks do not attempt a use after free while reading data from
+ // |this|.
+ state_ = State::DELETION_PENDING;
+ break;
+ case State::DELETION_PENDING:
+ NOTREACHED();
+ }
+}
+
+void PasswordUiViewAndroid::InsertPasswordEntryForTesting(
+ JNIEnv* env,
+ const std::u16string& origin,
+ const std::u16string& username,
+ const std::u16string& password) {
+ password_manager::PasswordForm form;
+ form.url = GURL(origin);
+ form.signon_realm = password_manager::GetSignonRealm(form.url);
+ form.username_value = username;
+ form.password_value = password;
+
+ profile_store_->AddLogin(form);
+}
+
+void PasswordUiViewAndroid::UpdatePasswordLists(JNIEnv* env,
+ const JavaRef<jobject>&) {
+ DCHECK_EQ(State::ALIVE, state_);
+ UpdatePasswordLists();
+}
+
+ScopedJavaLocalRef<jobject> PasswordUiViewAndroid::GetSavedPasswordEntry(
+ JNIEnv* env,
+ const JavaRef<jobject>&,
+ int index) {
+ DCHECK_EQ(State::ALIVE, state_);
+ if (static_cast<size_t>(index) >= passwords_.size()) {
+ return Java_PasswordUiView_createSavedPasswordEntry(
+ env, std::string(), std::u16string(), std::u16string());
+ }
+ return Java_PasswordUiView_createSavedPasswordEntry(
+ env, password_manager::GetShownOrigin(passwords_[index]),
+ passwords_[index].username, passwords_[index].password);
+}
+
+std::string PasswordUiViewAndroid::GetSavedPasswordException(
+ JNIEnv* env,
+ const JavaRef<jobject>&,
+ int index) {
+ DCHECK_EQ(State::ALIVE, state_);
+ if (static_cast<size_t>(index) >= blocked_sites_.size()) {
+ return "";
+ }
+ return password_manager::GetShownOrigin(blocked_sites_[index]);
+}
+
+void PasswordUiViewAndroid::HandleRemoveSavedPasswordEntry(
+ JNIEnv* env,
+ const JavaRef<jobject>&,
+ int index) {
+ DCHECK_EQ(State::ALIVE, state_);
+ if (static_cast<size_t>(index) >= passwords_.size()) {
+ return;
+ }
+ if (saved_passwords_presenter_.RemoveCredential(passwords_[index])) {
+ base::RecordAction(
+ base::UserMetricsAction("PasswordManager_RemoveSavedPassword"));
+ }
+}
+
+void PasswordUiViewAndroid::HandleRemoveSavedPasswordException(
+ JNIEnv* env,
+ const JavaRef<jobject>&,
+ int index) {
+ DCHECK_EQ(State::ALIVE, state_);
+ if (static_cast<size_t>(index) >= passwords_.size()) {
+ return;
+ }
+ if (saved_passwords_presenter_.RemoveCredential(passwords_[index])) {
+ base::RecordAction(
+ base::UserMetricsAction("PasswordManager_RemovePasswordException"));
+ }
+}
+
+void PasswordUiViewAndroid::HandleSerializePasswords(
+ JNIEnv* env,
+ const JavaRef<jobject>&,
+ const std::string& java_target_directory,
+ const JavaRef<jobject>& success_callback,
+ const JavaRef<jobject>& error_callback) {
+ switch (state_) {
+ case State::ALIVE:
+ state_ = State::ALIVE_SERIALIZATION_PENDING;
+ break;
+ case State::ALIVE_SERIALIZATION_PENDING:
+ // The UI should not allow the user to re-request export before finishing
+ // or cancelling the pending one.
+ NOTREACHED();
+ case State::DELETION_PENDING:
+ // The Java part should not first request destroying of |this| and then
+ // ask |this| for serialized passwords.
+ NOTREACHED();
+ }
+ std::vector<password_manager::CredentialUIEntry> credentials =
+ saved_passwords_presenter_.GetSavedCredentials();
+ std::erase_if(credentials, [](const auto& credential) {
+ return credential.blocked_by_user;
+ });
+
+ // The tasks are posted with base::Unretained, because deletion is postponed
+ // until the reply arrives (look for the occurrences of DELETION_PENDING in
+ // this file). The background processing is not expected to take very long,
+ // but still long enough not to block the UI thread. The main concern here is
+ // not to avoid the background computation if |this| is about to be deleted
+ // but to simply avoid use after free from the background task runner.
+ base::ThreadPool::PostTaskAndReplyWithResult(
+ FROM_HERE, {base::TaskPriority::USER_VISIBLE, base::MayBlock()},
+ base::BindOnce(&SerializePasswords, base::FilePath(java_target_directory),
+ std::move(credentials)),
+ base::BindOnce(&PasswordUiViewAndroid::PostSerializedPasswords,
+ base::Unretained(this),
+ base::android::ScopedJavaGlobalRef<jobject>(
+ env, success_callback),
+ base::android::ScopedJavaGlobalRef<jobject>(
+ env, error_callback)));
+}
+
+void PasswordUiViewAndroid::HandleShowPasswordEntryEditingView(
+ JNIEnv* env,
+ const base::android::JavaRef<jobject>& context,
+ int index,
+ const JavaRef<jobject>& obj) {
+ if (static_cast<size_t>(index) >= passwords_.size() ||
+ credential_edit_bridge_) {
+ return;
+ }
+ bool is_using_account_store = passwords_[index].stored_in.contains(
+ password_manager::PasswordForm::Store::kAccountStore);
+ credential_edit_bridge_ = CredentialEditBridge::MaybeCreate(
+ passwords_[index], IsInsecureCredential(false),
+ GetUsernamesForRealm(saved_passwords_presenter_.GetSavedCredentials(),
+ passwords_[index].GetFirstSignonRealm(),
+ is_using_account_store),
+ &saved_passwords_presenter_,
+ base::BindOnce(&PasswordUiViewAndroid::OnEditUIDismissed,
+ base::Unretained(this)),
+ context);
+}
+
+void PasswordUiViewAndroid::HandleShowBlockedCredentialView(
+ JNIEnv* env,
+ const base::android::JavaRef<jobject>& context,
+ int index,
+ const JavaRef<jobject>& obj) {
+ if (static_cast<size_t>(index) >= blocked_sites_.size() ||
+ credential_edit_bridge_) {
+ return;
+ }
+ credential_edit_bridge_ = CredentialEditBridge::MaybeCreate(
+ blocked_sites_[index], IsInsecureCredential(false),
+ std::vector<std::u16string>(), &saved_passwords_presenter_,
+ base::BindOnce(&PasswordUiViewAndroid::OnEditUIDismissed,
+ base::Unretained(this)),
+ context);
+}
+
+void PasswordUiViewAndroid::OnEditUIDismissed() {
+ credential_edit_bridge_.reset();
+}
+
+std::string JNI_PasswordUiView_GetAccountDashboardURL(JNIEnv* env) {
+ return l10n_util::GetStringUTF8(IDS_PASSWORDS_WEB_LINK);
+}
+
+std::string JNI_PasswordUiView_GetTrustedVaultLearnMoreURL(JNIEnv* env) {
+ return chrome::kSyncTrustedVaultLearnMoreURL;
+}
+
+jboolean PasswordUiViewAndroid::IsWaitingForPasswordStore(
+ JNIEnv* env,
+ const base::android::JavaRef<jobject>&) {
+ return saved_passwords_presenter_.IsWaitingForPasswordStore();
+}
+
+void PasswordUiViewAndroid::ImportFinished(
+ const password_manager::ImportResults& results) {
+ std::string error;
+ if (results.status == password_manager::ImportResults::Status::SUCCESS)
+ error = "Success.";
+ else if (results.status == password_manager::ImportResults::Status::IO_ERROR)
+ error = "Failed to read provided file.";
+ else if (results.status == password_manager::ImportResults::Status::BAD_FORMAT)
+ error = "Header is missing, invalid or could not be read.";
+ else if (results.status == password_manager::ImportResults::Status::NUM_PASSWORDS_EXCEEDED)
+ error = "Too many passwords.";
+ else
+ error = "UNKNOWN ERROR.";
+
+ std::stringstream message;
+ message << error << " Imported " << results.number_imported << " passwords.";
+ auto result = message.str();
+
+ select_file_dialog_->ShowToast(result);
+
+ importer_.reset();
+}
+
+jboolean PasswordUiViewAndroid::StartImporting(
+ JNIEnv* env,
+ const base::android::JavaRef<jobject>& obj,
+ const JavaRef<jobject>& java_window) {
+ ui::WindowAndroid* window =
+ ui::WindowAndroid::FromJavaWindowAndroid(java_window);
+ CHECK(window);
+
+ select_file_dialog_ = ui::SelectFileDialog::Create(
+ this, std::make_unique<ChromeSelectFilePolicy>(nullptr));
+
+ ui::SelectFileDialog::FileTypeInfo file_type_info;
+
+ const std::vector<std::u16string> v_accept_types = { u"application/octet-stream" };
+ select_file_dialog_->SetAcceptTypes(v_accept_types);
+
+ select_file_dialog_->SelectFile(
+ ui::SelectFileDialog::SELECT_OPEN_FILE,
+ std::u16string(),
+ base::FilePath(),
+ &file_type_info,
+ 0,
+ base::FilePath::StringType(),
+ window);
+
+ return true;
+}
+
+void PasswordUiViewAndroid::FileSelectionCanceled() {}
+
+void PasswordUiViewAndroid::FileSelected(const ui::SelectedFileInfo& file, int index) {
+ base::FilePath path = file.path();
+
+ importer_ =
+ std::make_unique<password_manager::PasswordImporter>(&saved_passwords_presenter_);
+ importer_->Import(path,
+ password_manager::PasswordForm::Store::kProfileStore,
+ base::BindOnce(&PasswordUiViewAndroid::ImportFinished,
+ base::Unretained(this)));
+}
+
+// static
+static jlong JNI_PasswordUiView_Init(JNIEnv* env,
+ const JavaRef<jobject>& obj,
+ Profile* profile) {
+ PasswordUiViewAndroid* controller =
+ new PasswordUiViewAndroid(env, obj, profile);
+ return reinterpret_cast<intptr_t>(controller);
+}
+
+void PasswordUiViewAndroid::OnSavedPasswordsChanged(
+ const password_manager::PasswordStoreChangeList& changes) {
+ UpdatePasswordLists();
+}
+
+void PasswordUiViewAndroid::UpdatePasswordLists() {
+ passwords_.clear();
+ blocked_sites_.clear();
+ for (auto& credential : saved_passwords_presenter_.GetSavedCredentials()) {
+ if (credential.blocked_by_user) {
+ blocked_sites_.push_back(std::move(credential));
+ } else {
+ passwords_.push_back(std::move(credential));
+ }
+ }
+ JNIEnv* env = base::android::AttachCurrentThread();
+ ScopedJavaLocalRef<jobject> ui_controller = weak_java_ui_controller_.get(env);
+ if (!ui_controller.is_null()) {
+ Java_PasswordUiView_passwordListAvailable(
+ env, ui_controller, static_cast<int>(passwords_.size()));
+ Java_PasswordUiView_passwordExceptionListAvailable(
+ env, ui_controller, static_cast<int>(blocked_sites_.size()));
+ }
+}
+
+void PasswordUiViewAndroid::PostSerializedPasswords(
+ const base::android::JavaRef<jobject>& success_callback,
+ const base::android::JavaRef<jobject>& error_callback,
+ SerializationResult serialization_result) {
+ DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
+ switch (state_) {
+ case State::ALIVE:
+ NOTREACHED();
+ case State::ALIVE_SERIALIZATION_PENDING: {
+ state_ = State::ALIVE;
+ if (export_target_for_testing_) {
+ *export_target_for_testing_ = serialization_result;
+ } else {
+ if (serialization_result.entries_count) {
+ base::android::RunIntStringCallbackAndroid(
+ success_callback, serialization_result.entries_count,
+ serialization_result.exported_file_path);
+ } else {
+ base::android::RunStringCallbackAndroid(error_callback,
+ serialization_result.error);
+ }
+ }
+ break;
+ }
+ case State::DELETION_PENDING:
+ delete this;
+ break;
+ }
+}
+
+DEFINE_JNI(PasswordUiView)
diff --git a/chrome/browser/password_manager/android/password_ui_view_android.h b/chrome/browser/password_manager/android/password_ui_view_android.h
new file mode 100644
--- /dev/null
+++ b/chrome/browser/password_manager/android/password_ui_view_android.h
@@ -0,0 +1,196 @@
+// Copyright 2013 The Chromium Authors
+// 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_ANDROID_PASSWORD_UI_VIEW_ANDROID_H_
+#define CHROME_BROWSER_PASSWORD_MANAGER_ANDROID_PASSWORD_UI_VIEW_ANDROID_H_
+
+#include <stddef.h>
+
+#include <memory>
+#include <string>
+#include <vector>
+
+#include "base/android/jni_weak_ref.h"
+#include "base/android/scoped_java_ref.h"
+#include "base/memory/raw_ptr.h"
+#include "chrome/browser/affiliations/affiliation_service_factory.h"
+#include "chrome/browser/password_entry_edit/android/credential_edit_bridge.h"
+#include "chrome/browser/password_manager/factories/account_password_store_factory.h"
+#include "chrome/browser/password_manager/factories/profile_password_store_factory.h"
+#include "chrome/browser/signin/identity_manager_factory.h"
+#include "components/password_manager/core/browser/password_store/password_store_consumer.h"
+#include "components/password_manager/core/browser/ui/saved_passwords_presenter.h"
+#include "components/password_manager/core/browser/import/password_importer.h"
+#include "ui/shell_dialogs/select_file_dialog.h"
+
+class Profile;
+
+namespace password_manager {
+class CredentialProviderInterface;
+}
+
+// PasswordUIView for Android, contains jni hooks that allows Android UI to
+// display passwords and route UI commands back to SavedPasswordsPresenter.
+class PasswordUiViewAndroid
+ : public password_manager::SavedPasswordsPresenter::Observer,
+ public ui::SelectFileDialog::Listener {
+ public:
+ // Result of transforming a vector of PasswordForms into their CSV
+ // description and writing that to disk.
+ struct SerializationResult {
+ // The number of password entries written. 0 if error encountered.
+ int entries_count;
+
+ // The path to the temporary file containing the serialized passwords. Empty
+ // if error encountered.
+ std::string exported_file_path;
+
+ // The error description recorded after the last write operation. Empty if
+ // no error encountered.
+ std::string error;
+ };
+
+ PasswordUiViewAndroid(JNIEnv* env,
+ const jni_zero::JavaRef<jobject>& obj,
+ Profile* profile);
+
+ PasswordUiViewAndroid(const PasswordUiViewAndroid&) = delete;
+ PasswordUiViewAndroid& operator=(const PasswordUiViewAndroid&) = delete;
+
+ ~PasswordUiViewAndroid() override;
+
+ // Calls from Java.
+ base::android::ScopedJavaLocalRef<jobject> GetSavedPasswordEntry(
+ JNIEnv* env,
+ const base::android::JavaRef<jobject>&,
+ int index);
+ std::string GetSavedPasswordException(JNIEnv* env,
+ const base::android::JavaRef<jobject>&,
+ int index);
+ void InsertPasswordEntryForTesting(JNIEnv* env,
+ const std::u16string& origin,
+ const std::u16string& username,
+ const std::u16string& password);
+ void UpdatePasswordLists(JNIEnv* env, const base::android::JavaRef<jobject>&);
+ void HandleRemoveSavedPasswordEntry(JNIEnv* env,
+ const base::android::JavaRef<jobject>&,
+ int index);
+ void HandleRemoveSavedPasswordException(
+ JNIEnv* env,
+ const base::android::JavaRef<jobject>&,
+ int index);
+ void HandleSerializePasswords(
+ JNIEnv* env,
+ const base::android::JavaRef<jobject>&,
+ const std::string& java_target_directory,
+ const base::android::JavaRef<jobject>& success_callback,
+ const base::android::JavaRef<jobject>& error_callback);
+ void HandleShowPasswordEntryEditingView(
+ JNIEnv* env,
+ const base::android::JavaRef<jobject>& context,
+ int index,
+ const base::android::JavaRef<jobject>& obj);
+ void HandleShowBlockedCredentialView(
+ JNIEnv* env,
+ const base::android::JavaRef<jobject>& context,
+ int index,
+ const base::android::JavaRef<jobject>& obj);
+ jboolean IsWaitingForPasswordStore(JNIEnv* env,
+ const base::android::JavaRef<jobject>&);
+
+ void FileSelected(const ui::SelectedFileInfo& file,
+ int index) override;
+ void FileSelectionCanceled() override;
+
+ void ImportFinished(
+ const password_manager::ImportResults& results);
+
+ jboolean StartImporting(JNIEnv* env,
+ const base::android::JavaRef<jobject>& obj,
+ const base::android::JavaRef<jobject>& java_window);
+
+ // Destroy the native implementation.
+ void Destroy(JNIEnv*, const base::android::JavaRef<jobject>&);
+
+ void OnEditUIDismissed();
+
+ void set_export_target_for_testing(
+ SerializationResult* export_target_for_testing) {
+ export_target_for_testing_ = export_target_for_testing;
+ }
+
+ void set_credential_provider_for_testing(
+ password_manager::CredentialProviderInterface* provider) {
+ credential_provider_for_testing_ = provider;
+ }
+
+ private:
+ // Possible states in the life of PasswordUiViewAndroid.
+ // ALIVE:
+ // * Destroy was not called and no background tasks are pending.
+ // * All data members can be used on the main task runner.
+ // ALIVE_SERIALIZATION_PENDING:
+ // * Destroy was not called, password serialization task on another task
+ // runner is running.
+ // * All data members can be used on the main task runner, except for
+ // |saved_passwords_presenter_| which can only be used inside
+ // ObtainAndSerializePasswords, which is being run on a backend task
+ // runner.
+ // DELETION_PENDING:
+ // * Destroy() was called, a background task is pending and |this| should
+ // be deleted once the tasks complete.
+ // * This state should not be reached anywhere but in the completion call
+ // of the pending task.
+ enum class State { ALIVE, ALIVE_SERIALIZATION_PENDING, DELETION_PENDING };
+
+ // password_manager::SavedPasswordsPresenter::Observer implementation.
+ void OnSavedPasswordsChanged(
+ const password_manager::PasswordStoreChangeList& changes) override;
+
+ void UpdatePasswordLists();
+
+ // Sends |serialization_result| to Java via |success_callback| or
+ // |error_callback|, depending on whether the result is a success or an error.
+ void PostSerializedPasswords(
+ const base::android::JavaRef<jobject>& success_callback,
+ const base::android::JavaRef<jobject>& error_callback,
+ SerializationResult serialization_result);
+
+ // The |state_| must only be accessed on the main task runner.
+ State state_ = State::ALIVE;
+
+ // If not null, PostSerializedPasswords will write the serialized passwords to
+ // |*export_target_for_testing_| instead of passing them to Java. This must
+ // remain null in production code.
+ raw_ptr<SerializationResult> export_target_for_testing_ = nullptr;
+
+ raw_ptr<Profile> profile_;
+
+ // Pointer to the password store, powering |saved_passwords_presenter_|.
+ scoped_refptr<password_manager::PasswordStoreInterface> profile_store_;
+
+ // Manages the list of saved passwords, including updates.
+ password_manager::SavedPasswordsPresenter saved_passwords_presenter_;
+
+ // Cached passwords and blocked sites.
+ std::vector<password_manager::CredentialUIEntry> passwords_;
+ std::vector<password_manager::CredentialUIEntry> blocked_sites_;
+
+ scoped_refptr<ui::SelectFileDialog> select_file_dialog_;
+ std::unique_ptr<password_manager::PasswordImporter> importer_;
+
+ // If not null, passwords for exporting will be obtained from
+ // |*credential_provider_for_testing_|, otherwise from
+ // |saved_passwords_presenter_|. This must remain null in production code.
+ raw_ptr<password_manager::CredentialProviderInterface>
+ credential_provider_for_testing_ = nullptr;
+
+ // Java side of UI controller.
+ JavaObjectWeakGlobalRef weak_java_ui_controller_;
+
+ // Used to open the view/edit/delete UI.
+ std::unique_ptr<CredentialEditBridge> credential_edit_bridge_;
+};
+
+#endif // CHROME_BROWSER_PASSWORD_MANAGER_ANDROID_PASSWORD_UI_VIEW_ANDROID_H_
diff --git a/chrome/browser/password_manager/android/pwm_disabled/BUILD.gn b/chrome/browser/password_manager/android/pwm_disabled/BUILD.gn
--- a/chrome/browser/password_manager/android/pwm_disabled/BUILD.gn
+++ b/chrome/browser/password_manager/android/pwm_disabled/BUILD.gn
@@ -19,8 +19,6 @@ android_library("java") {
"java/src/org/chromium/chrome/browser/pwm_disabled/PasswordCsvDownloadDialogViewBinder.java",
"java/src/org/chromium/chrome/browser/pwm_disabled/PasswordCsvDownloadFlowController.java",
"java/src/org/chromium/chrome/browser/pwm_disabled/PasswordCsvDownloadFlowControllerFactory.java",
- "java/src/org/chromium/chrome/browser/pwm_disabled/PasswordManagerUnavailableDialogCoordinator.java",
- "java/src/org/chromium/chrome/browser/pwm_disabled/PasswordManagerUnavailableDialogMediator.java",
"java/src/org/chromium/chrome/browser/pwm_disabled/PwmDeprecationDialogsMetricsRecorder.java",
"java/src/org/chromium/chrome/browser/pwm_disabled/SingleThreadBarrierClosure.java",
"java/src/org/chromium/chrome/browser/pwm_disabled/TimedCallbackDelayer.java",
diff --git a/chrome/browser/password_manager/factories/password_manager_settings_service_factory.cc b/chrome/browser/password_manager/factories/password_manager_settings_service_factory.cc
--- a/chrome/browser/password_manager/factories/password_manager_settings_service_factory.cc
+++ b/chrome/browser/password_manager/factories/password_manager_settings_service_factory.cc
@@ -90,9 +90,7 @@ PasswordManagerSettingsServiceFactory::CreateService(Profile* profile) const {
return std::make_unique<PasswordManagerSettingsServiceAndroidImpl>(
profile->GetPrefs(), SyncServiceFactory::GetForProfile(profile));
}
- return nullptr;
-#else
+#endif
return std::make_unique<password_manager::PasswordManagerSettingsServiceImpl>(
profile->GetPrefs());
-#endif
}
diff --git a/chrome/browser/password_manager/factories/password_store_backend_factory.cc b/chrome/browser/password_manager/factories/password_store_backend_factory.cc
--- a/chrome/browser/password_manager/factories/password_store_backend_factory.cc
+++ b/chrome/browser/password_manager/factories/password_store_backend_factory.cc
@@ -17,6 +17,8 @@
#include "chrome/browser/password_manager/android/password_store_android_account_backend.h"
#include "chrome/browser/password_manager/android/password_store_android_local_backend.h"
#include "chrome/browser/password_manager/android/password_store_empty_backend.h"
+#include "components/password_manager/core/browser/password_store/login_database.h"
+#include "components/password_manager/core/browser/password_store/password_store_built_in_backend.h"
#else // BUILDFLAG(IS_ANDROID)
#include "components/password_manager/core/browser/password_store/login_database.h"
#include "components/password_manager/core/browser/password_store/password_store_built_in_backend.h"
@@ -53,24 +55,20 @@ CreatePasswordStoreBackend(password_manager::IsAccountStore is_account_store,
#if BUILDFLAG(IS_ANDROID)
using password_manager_android_util::PasswordManagerUtilBridge;
- if (!password_manager_android_util::IsPasswordManagerAvailable(
- std::make_unique<PasswordManagerUtilBridge>())) {
- return std::make_unique<password_manager::PasswordStoreEmptyBackend>();
- }
if (is_account_store) {
return std::make_unique<
- password_manager::PasswordStoreAndroidAccountBackend>();
+ password_manager::PasswordStoreEmptyBackend>();
}
- return std::make_unique<password_manager::PasswordStoreAndroidLocalBackend>();
-#else // BUILDFLAG(IS_ANDROID)
+#endif // BUILDFLAG(IS_ANDROID)
std::unique_ptr<password_manager::LoginDatabase> login_db(
password_manager::CreateLoginDatabase(is_account_store,
login_db_directory, prefs));
+#if !BUILDFLAG(IS_ANDROID)
SetIsUserDataDirPolicySet(login_db.get());
+#endif
auto behavior = is_account_store
? syncer::WipeModelUponSyncDisabledBehavior::kAlways
: syncer::WipeModelUponSyncDisabledBehavior::kNever;
return std::make_unique<password_manager::PasswordStoreBuiltInBackend>(
std::move(login_db), behavior, prefs, os_crypt_async);
-#endif // BUILDFLAG(IS_ANDROID)
}
diff --git a/chrome/browser/prefs/browser_prefs.cc b/chrome/browser/prefs/browser_prefs.cc
--- a/chrome/browser/prefs/browser_prefs.cc
+++ b/chrome/browser/prefs/browser_prefs.cc
@@ -2527,13 +2527,6 @@ void MigrateObsoleteProfilePrefs(PrefService* profile_prefs,
profile_prefs->ClearPref(kObsoletePasswordsUseUPMLocalAndSeparateStores);
profile_prefs->ClearPref(kObsoleteEmptyProfileStoreLoginDatabase);
profile_prefs->ClearPref(kObsoleteUpmAutoExportCsvNeedsDeletion);
- base::DeleteFile(profile_path.Append(FILE_PATH_LITERAL("Login Data")));
- base::DeleteFile(
- profile_path.Append(FILE_PATH_LITERAL("Login Data For Account")));
- base::DeleteFile(
- profile_path.Append(FILE_PATH_LITERAL("Login Data-journal")));
- base::DeleteFile(
- profile_path.Append(FILE_PATH_LITERAL("Login Data For Account-journal")));
#endif // BUILDFLAG(IS_ANDROID)
// Added 09/2025.
diff --git a/chrome/browser/ui/android/strings/android_chrome_strings.grd b/chrome/browser/ui/android/strings/android_chrome_strings.grd
--- a/chrome/browser/ui/android/strings/android_chrome_strings.grd
+++ b/chrome/browser/ui/android/strings/android_chrome_strings.grd
@@ -835,7 +835,7 @@ For more settings that use data to improve your Chrome experience, go to <ph nam
Passwords
</message>
<message name="IDS_PASSWORD_MANAGER_SETTINGS_TITLE" desc="Title for the settings menu item leading to the Password Manager UI surface. This allows users to e.g. manage their saved passwords, toggle saving and auto-sign-in on/off, etc. [CHAR_LIMIT=32]">
- Google Password Manager
+ Password Manager
</message>
<message name="IDS_PASSWORD_SAVING_ON_BY_ADMINISTRATOR" desc="Subtitle for the settings menu item leading to the Password Manager UI surface, shown when the device administrator set saving passwords to enabled.">
Password saving is turned on by your administrator
@@ -935,6 +935,127 @@ For more settings that use data to improve your Chrome experience, go to <ph nam
Password suggestion is closed.
</message>
+ <!-- Password Manager Android -->
+ <message name="IDS_PASSWORD_ENTRY_VIEWER_TITLE" desc='Title of the overview screen for a saved password.'>
+ Saved password
+ </message>
+ <message name="IDS_PASSWORD_VIA_FEDERATION" desc="Text for federated credential's value.">
+ with <ph name="PROVIDER">%1$s<ex>example.com</ex></ph>
+ </message>
+ <message name="IDS_PASSWORD_ENTRY_VIEWER_EDIT_STORED_PASSWORD_ACTION_TITLE" desc='Title for button on action bar that edits the stored password.'>
+ Edit password
+ </message>
+ <message name="IDS_PASSWORD_EDIT_HINT" desc="Small hint on the editing screens that advises users to store the same password as they use on the currently edited site.">
+ Make sure the password you are saving matches your password for <ph name="SITE">%1$s<ex>example.com</ex></ph>
+ </message>
+ <message name="IDS_PASSWORD_ENTRY_EDIT_DUPLICATE_USERNAME_ERROR" desc='Error text displayed when the user changes the username in the editor to another username that they already saved for the same site.'>
+ You already saved this username for this site
+ </message>
+ <message name="IDS_PASSWORD_ENTRY_VIEWER_SHOW_STORED_PASSWORD" desc='Content description for the button that allows users to view the stored password upon reauthentication.'>
+ Show password
+ </message>
+ <message name="IDS_PASSWORD_ENTRY_VIEWER_HIDE_STORED_PASSWORD" desc="A tooltip on a button that hides the saved password that is being shown.">
+ Hide password
+ </message>
+ <message name="IDS_SECTION_SAVED_PASSWORDS_EXCEPTIONS" desc="Header for the list of websites for which user selected to never save passwords. [CHAR_LIMIT=32]">
+ Never saved
+ </message>
+ <message name="IDS_PASSWORD_SETTINGS_EXPORT_ACTION_TITLE" desc="The title of a menu item to trigger exporting passwords from the password settings.">
+ Export passwords…
+ </message>
+ <message name="IDS_PASSWORD_SETTINGS_EXPORT_ACTION_DESCRIPTION" desc="The description of a menu item to trigger exporting passwords from the password settings.">
+ Export passwords stored with Chrome
+ </message>
+ <message name="IDS_PASSWORD_ENTRY_VIEWER_USERNAME_TITLE" desc='Title preceding username of the account where the stored password comes from.'>
+ Username
+ </message>
+ <message name="IDS_PASSWORD_ENTRY_VIEWER_COPY_STORED_USERNAME" desc='Content description for the button that copies the stored username.'>
+ Copy username
+ </message>
+ <message name="IDS_PASSWORD_ENTRY_VIEWER_PASSWORD" desc='Title preceding password on password entry viewer.'>
+ Password
+ </message>
+ <message name="IDS_PASSWORD_ENTRY_VIEWER_USERNAME_COPIED_INTO_CLIPBOARD" desc='Text that announces to the user that the username of a saved account has been copied into clipboard.'>
+ Username copied
+ </message>
+ <message name="IDS_PASSWORD_ENTRY_VIEWER_COPY_STORED_PASSWORD" desc='Content description for the button that copies the stored password.'>
+ Copy password
+ </message>
+ <message name="IDS_PASSWORD_ENTRY_VIEWER_PASSWORD_COPIED_INTO_CLIPBOARD" desc='Text that announces to the user that the password of a saved account has been copied into clipboard.'>
+ Password copied
+ </message>
+ <message name="IDS_PASSWORD_ENTRY_VIEWER_SITE_TITLE" desc='Title preceding the identification of the site where the stored password comes from.'>
+ Site
+ </message>
+ <message name="IDS_PASSWORD_ENTRY_VIEWER_DELETE_STORED_PASSWORD_ACTION_TITLE" desc='Title for button on action bar that deletes the stored password.'>
+ Delete password
+ </message>
+ <message name="IDS_PASSWORD_ENTRY_EDIT_DELETE_CREDENTIAL_DIALOG_CONFIRM" desc="The caption the button confirming that the user intends to delete the stored password.">
+ Delete password
+ </message>
+ <message name="IDS_PASSWORD_ENTRY_VIEWER_DELETE_STORED_PASSWORD" desc='Content description for the button that allows users to delete the stored password.'>
+ Delete stored password
+ </message>
+ <message name="IDS_PASSWORD_ENTRY_EDIT_DELETION_DIALOG_BODY" desc="Text explaining the user that deleting the credential that they are viewing will not remove their account on the reaspective website.">
+ Deleting this password will not delete your account on <ph name="SITE">%1$s<ex>example.com</ex></ph>
+ </message>
+ <message name="IDS_PASSWORD_CHECK_DELETE_CREDENTIAL_DIALOG_BODY" desc="The body of a prompt to confirm that the user intends to delete the stored password.">
+ Deleting this password will not delete your account on <ph name="SITE">%1$s<ex>example.com</ex></ph>. Change your password or delete your account on <ph name="SITE">%1$s<ex>example.com</ex></ph> to keep it safe from others.
+ </message>
+ <message name="IDS_PASSWORD_ENTRY_EDIT_DELETE_CREDENTIAL_DIALOG_TITLE" desc="The title of a prompt to confirm that the user intends to delete the stored password.">
+ Delete password?
+ </message>
+ <message name="IDS_PASSWORD_ENTRY_VIEW_SET_SCREEN_LOCK" desc='Text prompting user to set up screen lock on their device in order to view passwords'>
+ To view passwords, first set a screen lock on your device
+ </message>
+ <message name="IDS_PASSWORD_ENTRY_COPY_SET_SCREEN_LOCK" desc='Text prompting user to set up screen lock on their device in order to copy passwords'>
+ To copy passwords, first set a screen lock on your device
+ </message>
+ <message name="IDS_SAVED_PASSWORDS_NONE_TEXT" desc="Text when there are no saved passwords/exceptions.">
+ Saved passwords will appear here.
+ </message>
+ <message name="IDS_PASSWORD_NO_RESULT" desc="Text when a password search returned no results.">
+ Cant find that password. Check your spelling and try again.
+ </message>
+ <message name="IDS_PASSWORD_SETTINGS_SAVE_PASSWORDS" desc="Title for the checkbox toggling whether passwords are saved or not. [CHAR_LIMIT=32]">
+ Save passwords
+ </message>
+ <message name="IDS_PASSWORDS_AUTO_SIGNIN_TITLE" desc="Title for checkbox to enable automatically signing the user in to websites">
+ Auto Sign-in
+ </message>
+ <message name="IDS_PASSWORDS_AUTO_SIGNIN_DESCRIPTION" desc="Text under 'Auto sign-in' checkbox">
+ Automatically sign in to websites using stored credentials. When the feature is off, youll be asked for verification every time before signing in to a website.
+ </message>
+ <message name="IDS_MANAGE_PASSWORDS_TEXT" desc="Text for link to manage passwords on Account Central.">
+ View and manage saved passwords in your <ph name="BEGIN_LINK">&lt;link&gt;</ph>Google Account<ph name="END_LINK">&lt;/link&gt;</ph>
+ </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>
+ <message name="IDS_PASSWORD_SETTINGS_EXPORT_ERROR_DETAILS" desc="A short prefix to introduce a technical error message passed to the user from Android after exporting passwords through a temporary file fails. Both the prefix and the error message are shown in the body of an alert dialog.">
+ Details: <ph name="ERROR_DESCRIPTION">%1$s<ex>IOException: No space left on device</ex></ph>
+ </message>
+ <message name="IDS_PASSWORD_SETTINGS_EXPORT_LEARN_GOOGLE_DRIVE" desc="The label of a button in an error dialog after exporting passwords from the settings failed. The button takes the user to a help article about using Google Drive.">
+ Learn how to use Google Drive
+ </message>
+ <message name="IDS_PASSWORD_SETTINGS_EXPORT_NO_APP" desc="The explanation of the error when exporting passwords fails because the user has no Android app installed which could consume the exported data. The description is shown in the body of an alert dialog.">
+ Your device doesnt have an app to store the passwords file.
+ </message>
+
+ <!-- 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.">
+ Unlock to copy your password
+ </message>
+ <message name="IDS_LOCKSCREEN_DESCRIPTION_VIEW" desc="When a user attempts to view a password for a particular website in Chrome's settings, Chrome launches a lock screen to verify the user's identity and displays the following explanation.">
+ Unlock to view your password
+ </message>
+ <message name="IDS_LOCKSCREEN_DESCRIPTION_EDIT" desc="When a user attempts to edit a password for a particular website in Chrome's settings, Chrome launches a lock screen to verify the user's identity and displays the following explanation.">
+ Unlock to edit your password
+ </message>
+ <message name="IDS_LOCKSCREEN_DESCRIPTION_EXPORT" desc="When a user attempts to export saved passwords in Chrome's settings, Chrome launches a lock screen to verify the user's identity and displays the following explanation.">
+ Unlock to export your passwords
+ </message>
+
<!-- Homepage preferences -->
<message name="IDS_OPTIONS_HOMEPAGE_EDIT_HINT" desc="Hint for the text edit on Homepage Preference setting, guiding user to enter their customized homepage setting">
Enter custom web address
diff --git a/chrome/browser/ui/android/strings/cromite_android_chrome_strings_grd/Import-Password-Android.grdp b/chrome/browser/ui/android/strings/cromite_android_chrome_strings_grd/Import-Password-Android.grdp
new file mode 100644
--- /dev/null
+++ b/chrome/browser/ui/android/strings/cromite_android_chrome_strings_grd/Import-Password-Android.grdp
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<grit-part>
+ <message name="IDS_PASSWORD_SETTINGS_IMPORT_ACTION_TITLE" desc="The title of a menu item to trigger importing passwords from the password settings.">
+ Import passwords…
+ </message>
+ <message name="IDS_PASSWORD_SETTINGS_IMPORT_ACTION_DESCRIPTION" desc="The description of a menu item to trigger importing passwords from the password settings.">
+ Import passwords stored with Chrome
+ </message>
+</grit-part>
diff --git a/components/autofill/core/common/autofill_features.cc b/components/autofill/core/common/autofill_features.cc
--- a/components/autofill/core/common/autofill_features.cc
+++ b/components/autofill/core/common/autofill_features.cc
@@ -409,6 +409,7 @@ BASE_FEATURE(kAutofillCreditCardUserPerceptionSurvey,
// If enabled, other apps can open the Autofill Options in Chrome.
BASE_FEATURE(kAutofillDeepLinkAutofillOptions,
base::FEATURE_ENABLED_BY_DEFAULT);
+SET_CROMITE_FEATURE_DISABLED(kAutofillDeepLinkAutofillOptions);
#endif // BUILDFLAG(IS_ANDROID)
// If enabled, `FormParsingTracker` will wait up to 1 second
diff --git a/components/browser_ui/settings/android/java/src/org/chromium/components/browser_ui/settings/SettingsNavigation.java b/components/browser_ui/settings/android/java/src/org/chromium/components/browser_ui/settings/SettingsNavigation.java
--- a/components/browser_ui/settings/android/java/src/org/chromium/components/browser_ui/settings/SettingsNavigation.java
+++ b/components/browser_ui/settings/android/java/src/org/chromium/components/browser_ui/settings/SettingsNavigation.java
@@ -28,6 +28,7 @@ public interface SettingsNavigation {
SettingsFragment.SAFETY_CHECK,
SettingsFragment.SITE,
SettingsFragment.ACCESSIBILITY,
+ SettingsFragment.PASSWORDS,
SettingsFragment.GOOGLE_SERVICES,
SettingsFragment.MANAGE_SYNC,
SettingsFragment.FINANCIAL_ACCOUNTS,
@@ -55,6 +56,8 @@ public interface SettingsNavigation {
int FINANCIAL_ACCOUNTS = 8;
/// Non-card payment methods.
int NON_CARD_PAYMENT_METHODS = 9;
+ /// Password settings.
+ int PASSWORDS = 10;
}
/**
diff --git a/components/password_manager/core/browser/features/password_manager_features_util.cc b/components/password_manager/core/browser/features/password_manager_features_util.cc
--- a/components/password_manager/core/browser/features/password_manager_features_util.cc
+++ b/components/password_manager/core/browser/features/password_manager_features_util.cc
@@ -18,6 +18,7 @@ namespace password_manager::features_util {
namespace {
bool IsUserEligibleForAccountStorage(const syncer::SyncService* sync_service) {
+ if ((true)) return false;
if (!sync_service) {
return false;
}
diff --git a/components/password_manager/core/browser/import/password_importer.cc b/components/password_manager/core/browser/import/password_importer.cc
--- a/components/password_manager/core/browser/import/password_importer.cc
+++ b/components/password_manager/core/browser/import/password_importer.cc
@@ -82,6 +82,17 @@ base::expected<void, ImportResults::Status> ValidateDataSize(int64_t size) {
// optional string. The string will be present if the status is SUCCESS.
base::expected<std::string, ImportResults::Status> ReadFileToString(
const base::FilePath& path) {
+#if BUILDFLAG(IS_ANDROID)
+ if (path.IsContentUri()) {
+ base::File file(path, base::File::FLAG_OPEN | base::File::FLAG_READ);
+ auto fileLength = file.GetLength();
+ std::vector<char> buffer(fileLength);
+ file.ReadAtCurrentPos(base::as_writable_byte_span(buffer));
+
+ std::string file_contents(buffer.begin(), buffer.end());
+ return std::move(file_contents);
+ }
+#endif
if (std::optional<int64_t> file_size = base::GetFileSize(path)) {
RETURN_IF_ERROR(ValidateDataSize(file_size.value()));
}
diff --git a/components/password_manager/core/browser/password_manager.cc b/components/password_manager/core/browser/password_manager.cc
--- a/components/password_manager/core/browser/password_manager.cc
+++ b/components/password_manager/core/browser/password_manager.cc
@@ -623,9 +623,7 @@ void PasswordManager::RegisterProfilePrefs(
registry->RegisterIntegerPref(prefs::kTotalPasswordsAvailableForProfile, 0);
registry->RegisterIntegerPref(prefs::kPasswordRemovalReasonForAccount, 0);
registry->RegisterIntegerPref(prefs::kPasswordRemovalReasonForProfile, 0);
-#if !BUILDFLAG(IS_ANDROID)
registry->RegisterBooleanPref(prefs::kClearingUndecryptablePasswords, false);
-#endif
#if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_MAC) || BUILDFLAG(IS_LINUX) || \
BUILDFLAG(IS_IOS)
diff --git a/components/password_manager/core/browser/password_manager_constants.cc b/components/password_manager/core/browser/password_manager_constants.cc
--- a/components/password_manager/core/browser/password_manager_constants.cc
+++ b/components/password_manager/core/browser/password_manager_constants.cc
@@ -8,7 +8,6 @@
namespace password_manager {
-#if !BUILDFLAG(IS_ANDROID)
const base::FilePath::CharType kLoginDataForProfileFileName[] =
FILE_PATH_LITERAL("Login Data");
const base::FilePath::CharType kLoginDataForAccountFileName[] =
@@ -17,7 +16,6 @@ const base::FilePath::CharType kLoginDataJournalForProfileFileName[] =
FILE_PATH_LITERAL("Login Data-journal");
const base::FilePath::CharType kLoginDataJournalForAccountFileName[] =
FILE_PATH_LITERAL("Login Data For Account-journal");
-#endif // !BUILDFLAG(IS_ANDROID)
const char kPasswordManagerAccountDashboardURL[] =
"https://passwords.google.com";
diff --git a/components/password_manager/core/browser/password_manager_constants.h b/components/password_manager/core/browser/password_manager_constants.h
--- a/components/password_manager/core/browser/password_manager_constants.h
+++ b/components/password_manager/core/browser/password_manager_constants.h
@@ -11,12 +11,10 @@
namespace password_manager {
-#if !BUILDFLAG(IS_ANDROID)
extern const base::FilePath::CharType kLoginDataForProfileFileName[];
extern const base::FilePath::CharType kLoginDataForAccountFileName[];
extern const base::FilePath::CharType kLoginDataJournalForProfileFileName[];
extern const base::FilePath::CharType kLoginDataJournalForAccountFileName[];
-#endif // !BUILDFLAG(IS_ANDROID)
// URL to the password manager account dashboard.
extern const char kPasswordManagerAccountDashboardURL[];
diff --git a/components/password_manager/core/browser/password_store/BUILD.gn b/components/password_manager/core/browser/password_store/BUILD.gn
--- a/components/password_manager/core/browser/password_store/BUILD.gn
+++ b/components/password_manager/core/browser/password_store/BUILD.gn
@@ -75,6 +75,19 @@ source_set("password_store_impl") {
"android_backend_error.h",
"password_data_type_controller_delegate_android.cc",
"password_data_type_controller_delegate_android.h",
+ "insecure_credentials_table.cc",
+ "insecure_credentials_table.h",
+ "login_database.cc",
+ "login_database.h",
+ "login_database_async_helper.cc",
+ "login_database_async_helper.h",
+ "password_notes_table.cc",
+ "password_notes_table.h",
+ "password_store_built_in_backend.cc",
+ "password_store_built_in_backend.h",
+ "statistics_table.cc",
+ "statistics_table.h",
+ "login_database_posix.cc",
]
deps += [
"//components/sync/base",
diff --git a/components/password_manager/core/browser/password_store/login_database_posix.cc b/components/password_manager/core/browser/password_store/login_database_posix.cc
--- a/components/password_manager/core/browser/password_store/login_database_posix.cc
+++ b/components/password_manager/core/browser/password_store/login_database_posix.cc
@@ -42,7 +42,7 @@ EncryptionResult LoginDatabase::DecryptedString(
const std::string& cipher_text,
std::u16string* plain_text) const {
#if !BUILDFLAG(IS_FUCHSIA)
-#if BUILDFLAG(IS_CHROMEOS)
+#if BUILDFLAG(IS_ANDROID) || BUILDFLAG(IS_CHROMEOS)
// On ChromeOS, we have a mix of obfuscated and plain-text
// passwords. Obfuscated passwords always start with "v10", therefore anything
// else is plain-text.
@@ -63,7 +63,7 @@ EncryptionResult LoginDatabase::DecryptedString(
bool decryption_success =
encryptor_ && encryptor_->DecryptString16(cipher_text, plain_text);
-#if BUILDFLAG(IS_CHROMEOS)
+#if BUILDFLAG(IS_ANDROID) || BUILDFLAG(IS_CHROMEOS)
// If decryption failed, we assume it was because the value was actually a
// plain-text password which started with "v10".
// TODO(crbug.com/41457193): Remove this when there isn't a mix of plain-text
diff --git a/components/password_manager/core/browser/password_store/password_store_built_in_backend.cc b/components/password_manager/core/browser/password_store/password_store_built_in_backend.cc
--- a/components/password_manager/core/browser/password_store/password_store_built_in_backend.cc
+++ b/components/password_manager/core/browser/password_store/password_store_built_in_backend.cc
@@ -247,6 +247,7 @@ void PasswordStoreBuiltInBackend::InitBackend(
remote_form_changes_received_callback_ = remote_form_changes_received;
sync_enabled_or_disabled_cb_ = sync_enabled_or_disabled_cb;
+#if !BUILDFLAG(IS_ANDROID)
// To ensure that groups of the kClearUndecryptablePasswords will stay
// balanced, after the cleanup is done an additional flag check is needed.
// Users won't reach the flag the normal way since the LoginDB is working
@@ -255,6 +256,7 @@ void PasswordStoreBuiltInBackend::InitBackend(
if (pref_service_->GetBoolean(prefs::kClearingUndecryptablePasswords)) {
base::FeatureList::IsEnabled(features::kClearUndecryptablePasswords);
}
+#endif
background_task_runner_->PostTask(
FROM_HERE, base::BindOnce(&LoginDatabaseAsyncHelper::CreateSyncBackend,
@@ -561,11 +563,15 @@ void PasswordStoreBuiltInBackend::OnEncryptorReceived(
weak_ptr_factory_.GetWeakPtr()))
.Then(std::move(remote_form_changes_received));
+#if BUILDFLAG(IS_ANDROID)
+ auto on_undecryptable_passwords_removed = base::DoNothing();
+#else
auto on_undecryptable_passwords_removed =
base::BindPostTaskToCurrentDefault(base::BindRepeating(
&PasswordStoreBuiltInBackend::
SetClearingUndecryptablePasswordsIsEnabledPref,
weak_ptr_factory_.GetWeakPtr()));
+#endif
background_task_runner_->PostTaskAndReplyWithResult(
FROM_HERE,
diff --git a/components/password_manager/core/browser/password_store_factory_util.cc b/components/password_manager/core/browser/password_store_factory_util.cc
--- a/components/password_manager/core/browser/password_store_factory_util.cc
+++ b/components/password_manager/core/browser/password_store_factory_util.cc
@@ -21,15 +21,12 @@
#include "components/password_manager/core/common/password_manager_pref_names.h"
#include "components/prefs/pref_service.h"
-#if !BUILDFLAG(IS_ANDROID)
#include "components/password_manager/core/browser/password_store/login_database.h"
-#endif // !BUILDFLAG(IS_ANDROID)
namespace password_manager {
namespace {
-#if !BUILDFLAG(IS_ANDROID)
LoginDatabase::DeletingUndecryptablePasswordsEnabled GetPolicyFromPrefs(
PrefService* prefs) {
#if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_MAC) || BUILDFLAG(IS_LINUX) || \
@@ -40,11 +37,9 @@ LoginDatabase::DeletingUndecryptablePasswordsEnabled GetPolicyFromPrefs(
return LoginDatabase::DeletingUndecryptablePasswordsEnabled(true);
#endif
}
-#endif // !BUILDFLAG(IS_ANDROID)
} // namespace
-#if !BUILDFLAG(IS_ANDROID)
std::unique_ptr<LoginDatabase> CreateLoginDatabase(
password_manager::IsAccountStore is_account_store,
const base::FilePath& db_directory,
@@ -55,7 +50,6 @@ std::unique_ptr<LoginDatabase> CreateLoginDatabase(
return std::make_unique<LoginDatabase>(login_db_file_path, is_account_store,
GetPolicyFromPrefs(prefs));
}
-#endif // !BUILDFLAG(IS_ANDROID)
// TODO(http://crbug.com/890318): Add unitests to check cleaners are correctly
// created.
diff --git a/components/password_manager/core/browser/password_store_factory_util.h b/components/password_manager/core/browser/password_store_factory_util.h
--- a/components/password_manager/core/browser/password_store_factory_util.h
+++ b/components/password_manager/core/browser/password_store_factory_util.h
@@ -13,9 +13,7 @@
#include "build/build_config.h"
#include "components/password_manager/core/browser/password_store/password_store_interface.h"
-#if !BUILDFLAG(IS_ANDROID)
#include "components/password_manager/core/browser/password_store/login_database.h"
-#endif // !BUILDFLAG(IS_ANDROID)
namespace network::mojom {
class NetworkContext;
@@ -27,7 +25,6 @@ namespace password_manager {
class CredentialsCleanerRunner;
-#if !BUILDFLAG(IS_ANDROID)
// Creates a LoginDatabase. Looks in |db_directory| for the database file.
// Does not call LoginDatabase::Init() -- to avoid UI jank, that needs to be
// called by PasswordStore::Init() on the background thread.
@@ -35,7 +32,6 @@ std::unique_ptr<LoginDatabase> CreateLoginDatabase(
password_manager::IsAccountStore is_account_store,
const base::FilePath& db_directory,
PrefService* prefs);
-#endif // !BUILDFLAG(IS_ANDROID)
// This function handles the following clean-ups of credentials:
// (1) Removing blocklisted duplicates: if two blocklisted credentials have the
diff --git a/components/password_manager/core/common/password_manager_pref_names.h b/components/password_manager/core/common/password_manager_pref_names.h
--- a/components/password_manager/core/common/password_manager_pref_names.h
+++ b/components/password_manager/core/common/password_manager_pref_names.h
@@ -259,12 +259,10 @@ inline constexpr char kRelaunchChromeBubbleDismissedCounter[] =
"password_manager.relaunch_chrome_bubble_dismissed_counter";
#endif
-#if !BUILDFLAG(IS_ANDROID)
// Boolean pref indicating if the user is in one of the groups of the
// kClearUndecryptablePasswords experiment.
inline constexpr char kClearingUndecryptablePasswords[] =
"password_manager.clearing_undecryptable_passwords";
-#endif
// Boolean pref indicating if passwords were migrated to OSCryptAsync. Two for
// each store.
diff --git a/components/sync/service/sync_prefs.cc b/components/sync/service/sync_prefs.cc
--- a/components/sync/service/sync_prefs.cc
+++ b/components/sync/service/sync_prefs.cc
@@ -738,7 +738,7 @@ bool SyncPrefs::IsTypeSupportedInTransportMode(UserSelectableType type) {
kSeparateLocalAndAccountSearchEngines);
#endif
case UserSelectableType::kPasswords:
- return true;
+ return false;
case UserSelectableType::kAutofill:
return true;
case UserSelectableType::kPayments:
diff --git a/cromite_flags/components/password_manager/core/browser/features/password_features_cc/Restore-chrome-password-store.inc b/cromite_flags/components/password_manager/core/browser/features/password_features_cc/Restore-chrome-password-store.inc
new file mode 100644
--- /dev/null
+++ b/cromite_flags/components/password_manager/core/browser/features/password_features_cc/Restore-chrome-password-store.inc
@@ -0,0 +1 @@
+SET_CROMITE_FEATURE_ENABLED(kSkipUndecryptablePasswords);
--