From: uazo 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. - + android:excludeFromRecents="true"> 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 @@ + + + + + + + + + + + + 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 @@ + + + + + + + + + + + + + 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. 56dp 330dp + + 12dp + 8dp + 16dp + 8dp + 8dp 156dp 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"/> - 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 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 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("", "", 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 + +#include + +#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::MaybeCreate( + const password_manager::CredentialUIEntry credential, + IsInsecureCredential is_insecure_credential, + std::vector existing_usernames, + password_manager::SavedPasswordsPresenter* saved_passwords_presenter, + base::OnceClosure dismissal_callback, + const base::android::JavaRef& context) { + base::android::ScopedJavaGlobalRef 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 existing_usernames, + password_manager::SavedPasswordsPresenter* saved_passwords_presenter, + base::OnceClosure dismissal_callback, + const base::android::JavaRef& context, + base::android::ScopedJavaGlobalRef 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(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 + +#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; + // 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 MaybeCreate( + const password_manager::CredentialUIEntry credential, + IsInsecureCredential is_insecure_credential, + std::vector existing_usernames, + password_manager::SavedPasswordsPresenter* saved_passwords_presenter, + base::OnceClosure dismissal_callback, + const base::android::JavaRef& 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 existing_usernames, + password_manager::SavedPasswordsPresenter* saved_passwords_presenter, + base::OnceClosure dismissal_callback, + const base::android::JavaRef& context, + base::android::ScopedJavaGlobalRef 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 existing_usernames_; + + // The backend to route the edit event to. Should be owned by the the owner of + // the bridge. + raw_ptr + 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 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 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 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 UI_ACTION_HANDLER = + new PropertyModel.WritableObjectPropertyKey<>("ui action handler"); + static final PropertyModel.ReadableObjectPropertyKey URL_OR_APP = + new PropertyModel.ReadableObjectPropertyKey<>("url or app"); + static final PropertyModel.WritableObjectPropertyKey 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 PASSWORD = + new PropertyModel.WritableObjectPropertyKey<>("password"); + static final PropertyModel.WritableBooleanPropertyKey EMPTY_PASSWORD_ERROR = + new PropertyModel.WritableBooleanPropertyKey("empty password error"); + static final PropertyModel.ReadableObjectPropertyKey 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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ 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 @@ + + + + + + + + \ 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 @@ + + + + + + + + + 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 @@ + + + + + + 4dp + 2dp + 12dp + 10dp + 24dp + 16dp + 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 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 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. + * + *
+ * 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.
+ * 
+ */ +@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. + * + *

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 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 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 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. + * + *

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 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 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 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 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. + * + *

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 +#include +#include + +#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 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(credentials.size()), export_file.value(), + std::string()}; +} + +} // namespace + +PasswordUiViewAndroid::PasswordUiViewAndroid( + JNIEnv* env, + const jni_zero::JavaRef& 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&) { + 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&) { + DCHECK_EQ(State::ALIVE, state_); + UpdatePasswordLists(); +} + +ScopedJavaLocalRef PasswordUiViewAndroid::GetSavedPasswordEntry( + JNIEnv* env, + const JavaRef&, + int index) { + DCHECK_EQ(State::ALIVE, state_); + if (static_cast(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&, + int index) { + DCHECK_EQ(State::ALIVE, state_); + if (static_cast(index) >= blocked_sites_.size()) { + return ""; + } + return password_manager::GetShownOrigin(blocked_sites_[index]); +} + +void PasswordUiViewAndroid::HandleRemoveSavedPasswordEntry( + JNIEnv* env, + const JavaRef&, + int index) { + DCHECK_EQ(State::ALIVE, state_); + if (static_cast(index) >= passwords_.size()) { + return; + } + if (saved_passwords_presenter_.RemoveCredential(passwords_[index])) { + base::RecordAction( + base::UserMetricsAction("PasswordManager_RemoveSavedPassword")); + } +} + +void PasswordUiViewAndroid::HandleRemoveSavedPasswordException( + JNIEnv* env, + const JavaRef&, + int index) { + DCHECK_EQ(State::ALIVE, state_); + if (static_cast(index) >= passwords_.size()) { + return; + } + if (saved_passwords_presenter_.RemoveCredential(passwords_[index])) { + base::RecordAction( + base::UserMetricsAction("PasswordManager_RemovePasswordException")); + } +} + +void PasswordUiViewAndroid::HandleSerializePasswords( + JNIEnv* env, + const JavaRef&, + const std::string& java_target_directory, + const JavaRef& success_callback, + const JavaRef& 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 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( + env, success_callback), + base::android::ScopedJavaGlobalRef( + env, error_callback))); +} + +void PasswordUiViewAndroid::HandleShowPasswordEntryEditingView( + JNIEnv* env, + const base::android::JavaRef& context, + int index, + const JavaRef& obj) { + if (static_cast(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& context, + int index, + const JavaRef& obj) { + if (static_cast(index) >= blocked_sites_.size() || + credential_edit_bridge_) { + return; + } + credential_edit_bridge_ = CredentialEditBridge::MaybeCreate( + blocked_sites_[index], IsInsecureCredential(false), + std::vector(), &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&) { + 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& obj, + const JavaRef& java_window) { + ui::WindowAndroid* window = + ui::WindowAndroid::FromJavaWindowAndroid(java_window); + CHECK(window); + + select_file_dialog_ = ui::SelectFileDialog::Create( + this, std::make_unique(nullptr)); + + ui::SelectFileDialog::FileTypeInfo file_type_info; + + const std::vector 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(&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& obj, + Profile* profile) { + PasswordUiViewAndroid* controller = + new PasswordUiViewAndroid(env, obj, profile); + return reinterpret_cast(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 ui_controller = weak_java_ui_controller_.get(env); + if (!ui_controller.is_null()) { + Java_PasswordUiView_passwordListAvailable( + env, ui_controller, static_cast(passwords_.size())); + Java_PasswordUiView_passwordExceptionListAvailable( + env, ui_controller, static_cast(blocked_sites_.size())); + } +} + +void PasswordUiViewAndroid::PostSerializedPasswords( + const base::android::JavaRef& success_callback, + const base::android::JavaRef& 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 + +#include +#include +#include + +#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& obj, + Profile* profile); + + PasswordUiViewAndroid(const PasswordUiViewAndroid&) = delete; + PasswordUiViewAndroid& operator=(const PasswordUiViewAndroid&) = delete; + + ~PasswordUiViewAndroid() override; + + // Calls from Java. + base::android::ScopedJavaLocalRef GetSavedPasswordEntry( + JNIEnv* env, + const base::android::JavaRef&, + int index); + std::string GetSavedPasswordException(JNIEnv* env, + const base::android::JavaRef&, + 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&); + void HandleRemoveSavedPasswordEntry(JNIEnv* env, + const base::android::JavaRef&, + int index); + void HandleRemoveSavedPasswordException( + JNIEnv* env, + const base::android::JavaRef&, + int index); + void HandleSerializePasswords( + JNIEnv* env, + const base::android::JavaRef&, + const std::string& java_target_directory, + const base::android::JavaRef& success_callback, + const base::android::JavaRef& error_callback); + void HandleShowPasswordEntryEditingView( + JNIEnv* env, + const base::android::JavaRef& context, + int index, + const base::android::JavaRef& obj); + void HandleShowBlockedCredentialView( + JNIEnv* env, + const base::android::JavaRef& context, + int index, + const base::android::JavaRef& obj); + jboolean IsWaitingForPasswordStore(JNIEnv* env, + const base::android::JavaRef&); + + 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& obj, + const base::android::JavaRef& java_window); + + // Destroy the native implementation. + void Destroy(JNIEnv*, const base::android::JavaRef&); + + 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& success_callback, + const base::android::JavaRef& 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 export_target_for_testing_ = nullptr; + + raw_ptr profile_; + + // Pointer to the password store, powering |saved_passwords_presenter_|. + scoped_refptr profile_store_; + + // Manages the list of saved passwords, including updates. + password_manager::SavedPasswordsPresenter saved_passwords_presenter_; + + // Cached passwords and blocked sites. + std::vector passwords_; + std::vector blocked_sites_; + + scoped_refptr select_file_dialog_; + std::unique_ptr 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 + 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 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( profile->GetPrefs(), SyncServiceFactory::GetForProfile(profile)); } - return nullptr; -#else +#endif return std::make_unique( 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())) { - return std::make_unique(); - } if (is_account_store) { return std::make_unique< - password_manager::PasswordStoreAndroidAccountBackend>(); + password_manager::PasswordStoreEmptyBackend>(); } - return std::make_unique(); -#else // BUILDFLAG(IS_ANDROID) +#endif // BUILDFLAG(IS_ANDROID) std::unique_ptr 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( 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 - Google Password Manager + Password Manager 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 + + + Saved password + + + with %1$sexample.com + + + Edit password + + + Make sure the password you are saving matches your password for %1$sexample.com + + + You already saved this username for this site + + + Show password + + + Hide password + + + Never saved + + + Export passwords… + + + Export passwords stored with Chrome + + + Username + + + Copy username + + + Password + + + Username copied + + + Copy password + + + Password copied + + + Site + + + Delete password + + + Delete password + + + Delete stored password + + + Deleting this password will not delete your account on %1$sexample.com + + + Deleting this password will not delete your account on %1$sexample.com. Change your password or delete your account on %1$sexample.com to keep it safe from others. + + + Delete password? + + + To view passwords, first set a screen lock on your device + + + To copy passwords, first set a screen lock on your device + + + Saved passwords will appear here. + + + Can’t find that password. Check your spelling and try again. + + + Save passwords + + + Auto Sign-in + + + Automatically sign in to websites using stored credentials. When the feature is off, you’ll be asked for verification every time before signing in to a website. + + + View and manage saved passwords in your <link>Google Account</link> + + + Preparing passwords… + + + Details: %1$sIOException: No space left on device + + + Learn how to use Google Drive + + + Your device doesn’t have an app to store the passwords file. + + + + + Unlock to copy your password + + + Unlock to view your password + + + Unlock to edit your password + + + Unlock to export your passwords + + 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 @@ + + + + Import passwords… + + + Import passwords stored with Chrome + + 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 ValidateDataSize(int64_t size) { // optional string. The string will be present if the status is SUCCESS. base::expected 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 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 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 CreateLoginDatabase( password_manager::IsAccountStore is_account_store, const base::FilePath& db_directory, @@ -55,7 +50,6 @@ std::unique_ptr CreateLoginDatabase( return std::make_unique(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 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); --