[go: nahoru, domu]

blob: 88088fd4e9eae6073a98fc153306e482818b46f5 [file] [log] [blame]
// Copyright 2015 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "ios/chrome/browser/ui/settings/password/password_manager_view_controller.h"
#import <UIKit/UIKit.h>
#import <utility>
#import <vector>
#import "base/apple/foundation_util.h"
#import "base/ios/ios_util.h"
#import "base/metrics/histogram_functions.h"
#import "base/metrics/user_metrics.h"
#import "base/ranges/algorithm.h"
#import "base/strings/sys_string_conversions.h"
#import "components/google/core/common/google_util.h"
#import "components/keyed_service/core/service_access_type.h"
#import "components/password_manager/core/browser/password_list_sorter.h"
#import "components/password_manager/core/browser/password_manager_constants.h"
#import "components/password_manager/core/browser/password_manager_metrics_util.h"
#import "components/password_manager/core/browser/password_ui_utils.h"
#import "components/password_manager/core/browser/ui/affiliated_group.h"
#import "components/password_manager/core/browser/ui/credential_ui_entry.h"
#import "components/password_manager/core/browser/ui/password_check_referrer.h"
#import "components/password_manager/core/common/password_manager_features.h"
#import "components/password_manager/core/common/password_manager_pref_names.h"
#import "components/prefs/pref_service.h"
#import "components/strings/grit/components_strings.h"
#import "components/sync/base/features.h"
#import "components/sync/service/sync_service.h"
#import "components/sync/service/sync_service_utils.h"
#import "components/sync/service/sync_user_settings.h"
#import "ios/chrome/browser/net/crurl.h"
#import "ios/chrome/browser/passwords/password_checkup_metrics.h"
#import "ios/chrome/browser/shared/model/application_context/application_context.h"
#import "ios/chrome/browser/shared/model/url/chrome_url_constants.h"
#import "ios/chrome/browser/shared/public/features/system_flags.h"
#import "ios/chrome/browser/shared/ui/elements/home_waiting_view.h"
#import "ios/chrome/browser/shared/ui/table_view/cells/table_view_detail_icon_item.h"
#import "ios/chrome/browser/shared/ui/table_view/cells/table_view_detail_text_item.h"
#import "ios/chrome/browser/shared/ui/table_view/cells/table_view_image_item.h"
#import "ios/chrome/browser/shared/ui/table_view/cells/table_view_info_button_cell.h"
#import "ios/chrome/browser/shared/ui/table_view/cells/table_view_info_button_item.h"
#import "ios/chrome/browser/shared/ui/table_view/cells/table_view_link_header_footer_item.h"
#import "ios/chrome/browser/shared/ui/table_view/cells/table_view_switch_cell.h"
#import "ios/chrome/browser/shared/ui/table_view/cells/table_view_switch_item.h"
#import "ios/chrome/browser/shared/ui/table_view/cells/table_view_text_header_footer_item.h"
#import "ios/chrome/browser/shared/ui/table_view/cells/table_view_text_item.h"
#import "ios/chrome/browser/shared/ui/table_view/cells/table_view_url_item.h"
#import "ios/chrome/browser/shared/ui/table_view/table_view_favicon_data_source.h"
#import "ios/chrome/browser/shared/ui/table_view/table_view_illustrated_empty_view.h"
#import "ios/chrome/browser/shared/ui/table_view/table_view_navigation_controller_constants.h"
#import "ios/chrome/browser/shared/ui/table_view/table_view_utils.h"
#import "ios/chrome/browser/shared/ui/util/uikit_ui_util.h"
#import "ios/chrome/browser/signin/chrome_account_manager_service_observer_bridge.h"
#import "ios/chrome/browser/ui/settings/cells/settings_check_cell.h"
#import "ios/chrome/browser/ui/settings/cells/settings_check_item.h"
#import "ios/chrome/browser/ui/settings/elements/enterprise_info_popover_view_controller.h"
#import "ios/chrome/browser/ui/settings/password/branded_navigation_item_title_view.h"
#import "ios/chrome/browser/ui/settings/password/create_password_manager_title_view.h"
#import "ios/chrome/browser/ui/settings/password/password_manager_ui_features.h"
#import "ios/chrome/browser/ui/settings/password/password_manager_view_controller+private.h"
#import "ios/chrome/browser/ui/settings/password/password_manager_view_controller_delegate.h"
#import "ios/chrome/browser/ui/settings/password/password_manager_view_controller_items.h"
#import "ios/chrome/browser/ui/settings/password/password_manager_view_controller_presentation_delegate.h"
#import "ios/chrome/browser/ui/settings/password/passwords_consumer.h"
#import "ios/chrome/browser/ui/settings/password/passwords_settings_commands.h"
#import "ios/chrome/browser/ui/settings/password/passwords_table_view_constants.h"
#import "ios/chrome/browser/ui/settings/settings_root_table_view_controller+toolbar_add.h"
#import "ios/chrome/browser/ui/settings/settings_root_table_view_controller+toolbar_settings.h"
#import "ios/chrome/browser/ui/settings/settings_root_table_view_controller.h"
#import "ios/chrome/browser/ui/settings/utils/settings_utils.h"
#import "ios/chrome/common/string_util.h"
#import "ios/chrome/common/ui/colors/semantic_color_names.h"
#import "ios/chrome/common/ui/elements/popover_label_view_controller.h"
#import "ios/chrome/common/ui/reauthentication/reauthentication_module.h"
#import "ios/chrome/common/ui/util/constraints_ui_util.h"
#import "ios/chrome/grit/ios_chromium_strings.h"
#import "ios/chrome/grit/ios_strings.h"
#import "net/base/mac/url_conversions.h"
#import "third_party/abseil-cpp/absl/types/optional.h"
#import "ui/base/device_form_factor.h"
#import "ui/base/l10n/l10n_util.h"
#import "ui/base/l10n/l10n_util_mac.h"
#import "url/gurl.h"
using base::UmaHistogramEnumeration;
using password_manager::features::IsPasswordCheckupEnabled;
using password_manager::metrics_util::PasswordCheckInteraction;
namespace {
// Height of empty footer below the manage account header.
// This ammount added to the internal padding of the manage account header (8pt)
// and the height of the empty header of the next section (10pt) achieves the
// desired vertical spacing (20pt) between the manager account header's text and
// the first item of the next section.
constexpr CGFloat kManageAccountHeaderSectionFooterHeight = 2;
typedef NS_ENUM(NSInteger, ItemType) {
// Section: SectionIdentifierManageAccountHeader
ItemTypeLinkHeader = kItemTypeEnumZero,
// Section: SectionIdentifierPasswordCheck
ItemTypePasswordCheckStatus,
ItemTypeCheckForProblemsButton,
ItemTypeLastCheckTimestampFooter,
// Section: SectionIdentifierSavedPasswords
ItemTypeHeader,
ItemTypeSavedPassword, // This is a repeated item type.
// Section: SectionIdentifierBlocked
ItemTypeBlocked, // This is a repeated item type.
// Section: SectionIdentifierAddPasswordButton
ItemTypeAddPasswordButton,
};
bool IsPasswordNotesWithBackupEnabled() {
return base::FeatureList::IsEnabled(syncer::kPasswordNotesWithBackup);
}
// Helper method to determine whether the Password Check cell is tappable or
// not.
bool IsPasswordCheckTappable(PasswordCheckUIState passwordCheckState) {
switch (passwordCheckState) {
case PasswordCheckStateUnmutedCompromisedPasswords:
return true;
case PasswordCheckStateReusedPasswords:
case PasswordCheckStateWeakPasswords:
case PasswordCheckStateDismissedWarnings:
case PasswordCheckStateSafe:
return IsPasswordCheckupEnabled();
case PasswordCheckStateDefault:
case PasswordCheckStateRunning:
case PasswordCheckStateDisabled:
case PasswordCheckStateError:
case PasswordCheckStateSignedOut:
return false;
}
}
// TODO(crbug.com/1426463): Remove when CredentialUIEntry operator== is fixed.
template <typename T>
bool AreNotesEqual(const T& lhs, const T& rhs) {
return base::ranges::equal(lhs, rhs, {},
&password_manager::CredentialUIEntry::note,
&password_manager::CredentialUIEntry::note);
}
bool AreNotesEqual(const std::vector<password_manager::AffiliatedGroup>& lhs,
const std::vector<password_manager::AffiliatedGroup>& rhs) {
return base::ranges::equal(
lhs, rhs,
AreNotesEqual<base::span<const password_manager::CredentialUIEntry>>,
&password_manager::AffiliatedGroup::GetCredentials,
&password_manager::AffiliatedGroup::GetCredentials);
}
template <typename T>
bool AreStoresEqual(const T& lhs, const T& rhs) {
return base::ranges::equal(lhs, rhs, {},
&password_manager::CredentialUIEntry::stored_in,
&password_manager::CredentialUIEntry::stored_in);
}
bool AreStoresEqual(const std::vector<password_manager::AffiliatedGroup>& lhs,
const std::vector<password_manager::AffiliatedGroup>& rhs) {
return base::ranges::equal(
lhs, rhs,
AreStoresEqual<base::span<const password_manager::CredentialUIEntry>>,
&password_manager::AffiliatedGroup::GetCredentials,
&password_manager::AffiliatedGroup::GetCredentials);
}
template <typename T>
bool AreIssuesEqual(const T& lhs, const T& rhs) {
return base::ranges::equal(
lhs, rhs, {}, &password_manager::CredentialUIEntry::password_issues,
&password_manager::CredentialUIEntry::password_issues);
}
bool AreIssuesEqual(const std::vector<password_manager::AffiliatedGroup>& lhs,
const std::vector<password_manager::AffiliatedGroup>& rhs) {
return base::ranges::equal(
lhs, rhs,
AreIssuesEqual<base::span<const password_manager::CredentialUIEntry>>,
&password_manager::AffiliatedGroup::GetCredentials,
&password_manager::AffiliatedGroup::GetCredentials);
}
} // namespace
@interface PasswordManagerViewController () <
ChromeAccountManagerServiceObserver,
PopoverLabelViewControllerDelegate,
TableViewIllustratedEmptyViewDelegate> {
// Boolean indicating that passwords are being saved in an account if YES,
// and locally if NO.
BOOL _savingPasswordsToAccount;
// The list of the user's blocked sites.
std::vector<password_manager::CredentialUIEntry> _blockedSites;
// The list of the user's saved grouped passwords.
std::vector<password_manager::AffiliatedGroup> _affiliatedGroups;
// AcountManagerService Observer.
std::unique_ptr<ChromeAccountManagerServiceObserverBridge>
_accountManagerServiceObserver;
// Boolean indicating if password forms have been received for the first time.
// Used to show a loading indicator while waiting for the store response.
BOOL _didReceivePasswords;
// Whether the table view is in search mode. That is, it only has the search
// bar potentially saved passwords and blocked sites.
BOOL _tableIsInSearchMode;
// Whether the favicon metric was already logged.
BOOL _faviconMetricLogged;
// Whether the search controller should be set as active when the view is
// presented.
BOOL _shouldOpenInSearchMode;
}
// Current passwords search term.
@property(nonatomic, copy) NSString* searchTerm;
// The scrim view that covers the table view when search bar is focused with
// empty search term. Tapping on the scrim view will dismiss the search bar.
@property(nonatomic, strong) UIControl* scrimView;
// The loading spinner background which appears when loading passwords.
@property(nonatomic, strong) HomeWaitingView* spinnerView;
// Current state of the Password Check.
@property(nonatomic, assign) PasswordCheckUIState passwordCheckState;
// Number of insecure passwords.
@property(assign) NSInteger insecurePasswordsCount;
// Stores the most recently created or updated Affiliated Group.
@property(nonatomic, assign) absl::optional<password_manager::AffiliatedGroup>
mostRecentlyUpdatedAffiliatedGroup;
// Stores the most recently created or updated password form.
@property(nonatomic, assign) absl::optional<password_manager::CredentialUIEntry>
mostRecentlyUpdatedPassword;
// Stores the item which has form attribute's username and site equivalent to
// that of `mostRecentlyUpdatedPassword`.
@property(nonatomic, weak) TableViewItem* mostRecentlyUpdatedItem;
// YES, if the user triggered a password check by tapping on the "Check Now"
// button.
@property(nonatomic, assign) BOOL checkWasTriggeredManually;
// Return YES if the search bar should be enabled.
@property(nonatomic, assign) BOOL shouldEnableSearchBar;
// The search controller used in this view. This may be added/removed from the
// navigation controller, but the instance will persist here.
@property(nonatomic, strong) UISearchController* searchController;
// Settings button for the toolbar.
@property(nonatomic, strong) UIBarButtonItem* settingsButtonInToolbar;
// Add button for the toolbar.
@property(nonatomic, strong) UIBarButtonItem* addButtonInToolbar;
// Indicates whether the check button should be shown or not. Used when
// kIOSPasswordCheckup feature is enabled.
@property(nonatomic, assign) BOOL shouldShowCheckButton;
// The PrefService passed to this instance.
@property(nonatomic, assign) PrefService* prefService;
// The header for save passwords switch section.
@property(nonatomic, readonly)
TableViewLinkHeaderFooterItem* manageAccountLinkItem;
// The item related to the password check status.
@property(nonatomic, readonly) SettingsCheckItem* passwordProblemsItem;
// The button to start password check.
@property(nonatomic, readonly) TableViewTextItem* checkForProblemsItem;
// The button to add a password.
@property(nonatomic, readonly) TableViewTextItem* addPasswordItem;
@end
@implementation PasswordManagerViewController
@synthesize manageAccountLinkItem = _manageAccountLinkItem;
@synthesize passwordProblemsItem = _passwordProblemsItem;
@synthesize checkForProblemsItem = _checkForProblemsItem;
@synthesize addPasswordItem = _addPasswordItem;
#pragma mark - Initialization
- (instancetype)initWithChromeAccountManagerService:
(ChromeAccountManagerService*)accountManagerService
prefService:(PrefService*)prefService
shouldOpenInSearchMode:
(BOOL)shouldOpenInSearchMode {
self = [super initWithStyle:ChromeTableViewStyle()];
if (self) {
_prefService = prefService;
_accountManagerServiceObserver =
std::make_unique<ChromeAccountManagerServiceObserverBridge>(
self, accountManagerService);
self.shouldDisableDoneButtonOnEdit = YES;
self.searchTerm = @"";
// Default behavior: search bar is enabled.
self.shouldEnableSearchBar = YES;
_shouldOpenInSearchMode = shouldOpenInSearchMode;
[self updateUIForEditState];
}
return self;
}
- (void)dealloc {
DCHECK(!_accountManagerServiceObserver.get());
}
- (void)setReauthenticationModule:
(ReauthenticationModule*)reauthenticationModule {
_reauthenticationModule = reauthenticationModule;
}
// TODO(crbug.com/1358978): Receive AffiliatedGroup object instead of a
// CredentialUIEntry. Store into mostRecentlyUpdatedAffiliatedGroup.
- (void)setMostRecentlyUpdatedPasswordDetails:
(const password_manager::CredentialUIEntry&)credential {
self.mostRecentlyUpdatedPassword = credential;
}
#pragma mark - UIViewController
- (void)viewDidLoad {
[super viewDidLoad];
[self setUpTitle];
self.tableView.allowsMultipleSelectionDuringEditing = YES;
self.tableView.accessibilityIdentifier = kPasswordsTableViewId;
// With no header on first appearance, UITableView adds a 35 points space at
// the beginning of the table view. This space remains after this table view
// reloads with headers. Setting a small tableHeaderView avoids this.
self.tableView.tableHeaderView =
[[UIView alloc] initWithFrame:CGRectMake(0, 0, 0, CGFLOAT_MIN)];
// SearchController Configuration.
// Init the searchController with nil so the results are displayed on the same
// TableView.
UISearchController* searchController =
[[UISearchController alloc] initWithSearchResultsController:nil];
self.searchController = searchController;
searchController.obscuresBackgroundDuringPresentation = NO;
searchController.delegate = self;
UISearchBar* searchBar = searchController.searchBar;
searchBar.delegate = self;
searchBar.backgroundColor = UIColor.clearColor;
searchBar.accessibilityIdentifier = kPasswordsSearchBarId;
// TODO(crbug.com/1268684): Explicitly set the background color for the search
// bar to match with the color of navigation bar in iOS 13/14 to work around
// an iOS issue.
// UIKit needs to know which controller will be presenting the
// searchController. If we don't add this trying to dismiss while
// SearchController is active will fail.
self.definesPresentationContext = YES;
// Place the search bar in the navigation bar.
self.navigationItem.searchController = searchController;
self.navigationItem.hidesSearchBarWhenScrolling = NO;
self.scrimView = [[UIControl alloc] init];
self.scrimView.alpha = 0.0f;
self.scrimView.backgroundColor = [UIColor colorNamed:kScrimBackgroundColor];
self.scrimView.translatesAutoresizingMaskIntoConstraints = NO;
self.scrimView.accessibilityIdentifier = kPasswordsScrimViewId;
[self.scrimView addTarget:self
action:@selector(dismissSearchController:)
forControlEvents:UIControlEventTouchUpInside];
[self loadModel];
if (!_didReceivePasswords) {
[self showLoadingSpinnerBackground];
}
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
self.navigationController.toolbarHidden = NO;
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
if (_shouldOpenInSearchMode) {
// Queue search bar focus so the keyboard animation doesn't collide with
// other animations.
__weak __typeof(self.searchController.searchBar) weakSearchBar =
self.searchController.searchBar;
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE, base::BindOnce(^{
[weakSearchBar becomeFirstResponder];
}));
}
}
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
// viewWillDisappear is also called if you drag the sheet down then release
// without actually closing.
if (!_faviconMetricLogged) {
[self logMetricsForFavicons];
_faviconMetricLogged = YES;
}
}
- (void)viewDidDisappear:(BOOL)animated {
[super viewDidDisappear:animated];
// Dismiss the search bar if presented; otherwise UIKit may retain it and
// cause a memory leak. If this dismissal happens before viewWillDisappear
// (e.g., settingsWillBeDismissed) an internal UIKit crash occurs. See also:
// crbug.com/947417, crbug.com/1350625. Dismissing in viewDidDisappear to make
// sure that it happens when the view is well and truly gone.
if (self.navigationItem.searchController.active == YES) {
self.navigationItem.searchController.active = NO;
}
}
- (void)didMoveToParentViewController:(UIViewController*)parent {
[super didMoveToParentViewController:parent];
if (!parent) {
[self.presentationDelegate PasswordManagerViewControllerDismissed];
}
}
- (void)setEditing:(BOOL)editing animated:(BOOL)animated {
[super setEditing:editing animated:animated];
[self setAddPasswordButtonEnabled:!editing];
[self setSearchBarEnabled:self.shouldEnableSearchBar];
[self updatePasswordCheckButtonWithState:self.passwordCheckState];
[self updatePasswordCheckStatusLabelWithState:self.passwordCheckState];
[self updatePasswordCheckSectionWithState:self.passwordCheckState];
[self updateUIForEditState];
}
- (BOOL)hasPasswords {
return !_affiliatedGroups.empty();
}
#pragma mark - SettingsRootTableViewController
- (void)loadModel {
[super loadModel];
if (!_didReceivePasswords) {
return;
}
[self showOrHideEmptyView];
// If we don't have data or settings to show, add an empty state, then
// stop so that we don't add anything that overlaps the illustrated
// background.
if ([self shouldShowEmptyStateView]) {
return;
}
TableViewModel* model = self.tableViewModel;
// Don't show sections hidden when search controller is displayed.
if (!_tableIsInSearchMode) {
// Manage account header.
[model addSectionWithIdentifier:SectionIdentifierManageAccountHeader];
[model setHeader:self.manageAccountLinkItem
forSectionWithIdentifier:SectionIdentifierManageAccountHeader];
// Password check.
[model addSectionWithIdentifier:SectionIdentifierPasswordCheck];
[self updatePasswordCheckStatusLabelWithState:_passwordCheckState];
[model addItem:self.passwordProblemsItem
toSectionWithIdentifier:SectionIdentifierPasswordCheck];
[self updatePasswordCheckButtonWithState:_passwordCheckState];
// Only add check button if kIOSPasswordCheckup is disabled, or if it is
// enabled and the current PasswordCheckUIState requires the button to be
// shown.
if (!IsPasswordCheckupEnabled() || self.shouldShowCheckButton) {
[model addItem:self.checkForProblemsItem
toSectionWithIdentifier:SectionIdentifierPasswordCheck];
}
// When the Password Checkup feature is enabled, this timestamp only appears
// in the detail text of the Password Checkup status cell. It is therefore
// managed in `updatePasswordCheckStatusLabelWithState`.
if (!IsPasswordCheckupEnabled()) {
[self updateLastCheckTimestampWithState:_passwordCheckState
fromState:_passwordCheckState
update:NO];
}
// Add Password button.
if ([self allowsAddPassword]) {
[model addSectionWithIdentifier:SectionIdentifierAddPasswordButton];
[model addItem:self.addPasswordItem
toSectionWithIdentifier:SectionIdentifierAddPasswordButton];
}
}
// Saved passwords.
if ([self hasPasswords]) {
[model addSectionWithIdentifier:SectionIdentifierSavedPasswords];
TableViewTextHeaderFooterItem* headerItem =
[[TableViewTextHeaderFooterItem alloc] initWithType:ItemTypeHeader];
headerItem.text =
l10n_util::GetNSString(IDS_IOS_SETTINGS_PASSWORDS_SAVED_HEADING);
[model setHeader:headerItem
forSectionWithIdentifier:SectionIdentifierSavedPasswords];
}
// Blocked passwords.
if (!_blockedSites.empty()) {
[model addSectionWithIdentifier:SectionIdentifierBlocked];
TableViewTextHeaderFooterItem* headerItem =
[[TableViewTextHeaderFooterItem alloc] initWithType:ItemTypeHeader];
headerItem.text =
l10n_util::GetNSString(IDS_IOS_SETTINGS_PASSWORDS_EXCEPTIONS_HEADING);
[model setHeader:headerItem
forSectionWithIdentifier:SectionIdentifierBlocked];
}
[self filterItems:self.searchTerm];
}
// Returns YES if the array of index path contains a saved password. This is to
// determine if we need to show the user the alert dialog.
- (BOOL)indexPathsContainsSavedPassword:(NSArray<NSIndexPath*>*)indexPaths {
for (NSIndexPath* indexPath : indexPaths) {
if ([self.tableViewModel itemTypeForIndexPath:indexPath] ==
ItemTypeSavedPassword) {
return YES;
}
}
return NO;
}
- (void)deleteItems:(NSArray<NSIndexPath*>*)indexPaths {
// Only show the user the alert dialog if the index path array contain at
// least one saved password.
if ([self indexPathsContainsSavedPassword:indexPaths]) {
// Show password delete dialog before deleting the passwords.
NSMutableArray<NSString*>* origins = [[NSMutableArray alloc] init];
for (NSIndexPath* indexPath : indexPaths) {
NSInteger itemType = [self.tableViewModel itemTypeForIndexPath:indexPath];
if (itemType == ItemTypeSavedPassword) {
password_manager::AffiliatedGroup affiliatedGroup =
base::apple::ObjCCastStrict<AffiliatedGroupTableViewItem>(
[self.tableViewModel itemAtIndexPath:indexPath])
.affiliatedGroup;
[origins addObject:base::SysUTF8ToNSString(
affiliatedGroup.GetDisplayName())];
}
}
[self.handler
showPasswordDeleteDialogWithOrigins:origins
completion:^{
[self deleteItemAtIndexPaths:indexPaths];
}];
} else {
// Do not call super as this also deletes the section if it is empty.
[self deleteItemAtIndexPaths:indexPaths];
}
}
- (BOOL)editButtonEnabled {
return [self hasPasswords] || !_blockedSites.empty();
}
- (BOOL)shouldHideToolbar {
return NO;
}
- (BOOL)shouldShowEditDoneButton {
// The "Done" button in the navigation bar closes the sheet.
return NO;
}
- (void)updateUIForEditState {
[super updateUIForEditState];
[self updatedToolbarForEditState];
}
- (void)editButtonPressed {
// Disable search bar if the user is bulk editing (edit mode). (Reverse logic
// because parent method -editButtonPressed is calling setEditing to change
// the state).
self.shouldEnableSearchBar = self.tableView.editing;
[super editButtonPressed];
}
- (UIBarButtonItem*)customLeftToolbarButton {
return self.tableView.isEditing ? nil : self.settingsButtonInToolbar;
}
- (UIBarButtonItem*)customRightToolbarButton {
if (!self.tableView.isEditing) {
// Display Add button on the right side of the toolbar when the empty state
// is displayed. The Settings button will be on the left. When the tableView
// is not empty, the Add button is displayed in a row.
if ([self shouldShowEmptyStateView]) {
return self.addButtonInToolbar;
}
}
return nil;
}
#pragma mark - SettingsControllerProtocol
- (void)reportDismissalUserAction {
base::RecordAction(base::UserMetricsAction("MobilePasswordsSettingsClose"));
_accountManagerServiceObserver.reset();
}
- (void)reportBackUserAction {
base::RecordAction(base::UserMetricsAction("MobilePasswordsSettingsBack"));
_accountManagerServiceObserver.reset();
}
- (void)settingsWillBeDismissed {
CHECK(self.prefService);
_accountManagerServiceObserver.reset();
self.prefService = nullptr;
}
#pragma mark - Items
- (TableViewLinkHeaderFooterItem*)manageAccountLinkItem {
if (_manageAccountLinkItem) {
return _manageAccountLinkItem;
}
_manageAccountLinkItem =
[[TableViewLinkHeaderFooterItem alloc] initWithType:ItemTypeLinkHeader];
if (_savingPasswordsToAccount) {
_manageAccountLinkItem.text =
l10n_util::GetNSString(IDS_IOS_SAVE_PASSWORDS_MANAGE_ACCOUNT_HEADER);
_manageAccountLinkItem.urls = @[ [[CrURL alloc]
initWithGURL:
google_util::AppendGoogleLocaleParam(
GURL(password_manager::kPasswordManagerHelpCenteriOSURL),
GetApplicationContext()->GetApplicationLocale())] ];
} else {
_manageAccountLinkItem.text =
l10n_util::GetNSString(IDS_IOS_PASSWORD_MANAGER_HEADER_NOT_SYNCING);
_manageAccountLinkItem.urls = @[];
}
return _manageAccountLinkItem;
}
- (SettingsCheckItem*)passwordProblemsItem {
if (_passwordProblemsItem) {
return _passwordProblemsItem;
}
_passwordProblemsItem =
[[SettingsCheckItem alloc] initWithType:ItemTypePasswordCheckStatus];
_passwordProblemsItem.enabled = NO;
_passwordProblemsItem.text =
IsPasswordCheckupEnabled()
? l10n_util::GetNSString(IDS_IOS_PASSWORD_CHECKUP)
: l10n_util::GetNSString(IDS_IOS_CHECK_PASSWORDS);
_passwordProblemsItem.detailText =
IsPasswordCheckupEnabled()
? l10n_util::GetNSString(IDS_IOS_PASSWORD_CHECKUP_DESCRIPTION)
: l10n_util::GetNSString(IDS_IOS_CHECK_PASSWORDS_DESCRIPTION);
_passwordProblemsItem.accessibilityTraits = UIAccessibilityTraitHeader;
return _passwordProblemsItem;
}
- (TableViewTextItem*)checkForProblemsItem {
if (_checkForProblemsItem) {
return _checkForProblemsItem;
}
_checkForProblemsItem =
[[TableViewTextItem alloc] initWithType:ItemTypeCheckForProblemsButton];
_checkForProblemsItem.text =
l10n_util::GetNSString(IDS_IOS_CHECK_PASSWORDS_NOW_BUTTON);
_checkForProblemsItem.textColor = [UIColor colorNamed:kTextSecondaryColor];
_checkForProblemsItem.accessibilityTraits = UIAccessibilityTraitButton;
return _checkForProblemsItem;
}
- (TableViewLinkHeaderFooterItem*)lastCompletedCheckTime {
TableViewLinkHeaderFooterItem* footerItem =
[[TableViewLinkHeaderFooterItem alloc]
initWithType:ItemTypeLastCheckTimestampFooter];
footerItem.text = [self.delegate formattedElapsedTimeSinceLastCheck];
return footerItem;
}
- (TableViewTextItem*)addPasswordItem {
if (_addPasswordItem) {
return _addPasswordItem;
}
_addPasswordItem =
[[TableViewTextItem alloc] initWithType:ItemTypeAddPasswordButton];
_addPasswordItem.text = l10n_util::GetNSString(IDS_IOS_ADD_PASSWORD);
_addPasswordItem.accessibilityIdentifier = kAddPasswordButtonId;
_addPasswordItem.accessibilityTraits = UIAccessibilityTraitButton;
_addPasswordItem.textColor = [UIColor colorNamed:kBlueColor];
return _addPasswordItem;
}
- (AffiliatedGroupTableViewItem*)savedFormItemForAffiliatedGroup:
(const password_manager::AffiliatedGroup&)affiliatedGroup {
AffiliatedGroupTableViewItem* passwordItem =
[[AffiliatedGroupTableViewItem alloc] initWithType:ItemTypeSavedPassword];
passwordItem.affiliatedGroup = affiliatedGroup;
passwordItem.showLocalOnlyIcon =
[self.delegate shouldShowLocalOnlyIconForGroup:affiliatedGroup];
passwordItem.accessibilityTraits |= UIAccessibilityTraitButton;
passwordItem.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
if (self.mostRecentlyUpdatedAffiliatedGroup) {
if (self.mostRecentlyUpdatedAffiliatedGroup->GetDisplayName() ==
affiliatedGroup.GetDisplayName()) {
self.mostRecentlyUpdatedItem = passwordItem;
self.mostRecentlyUpdatedAffiliatedGroup = absl::nullopt;
}
}
return passwordItem;
}
- (BlockedSiteTableViewItem*)blockedSiteItem:
(const password_manager::CredentialUIEntry&)credential {
BlockedSiteTableViewItem* passwordItem =
[[BlockedSiteTableViewItem alloc] initWithType:ItemTypeBlocked];
passwordItem.credential = credential;
passwordItem.accessibilityTraits |= UIAccessibilityTraitButton;
passwordItem.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
return passwordItem;
}
#pragma mark - PopoverLabelViewControllerDelegate
- (void)didTapLinkURL:(NSURL*)URL {
[self view:nil didTapLinkURL:[[CrURL alloc] initWithNSURL:URL]];
}
#pragma mark - Actions
// Called when user tapped on the information button of the password check
// item. Shows popover with detailed description of an error.
- (void)didTapPasswordCheckInfoButton:(UIButton*)buttonView {
NSAttributedString* info = [self.delegate passwordCheckErrorInfo];
// If no info returned by mediator handle this tap as tap on a cell.
if (!info) {
IsPasswordCheckupEnabled() ? [self showPasswordCheckupPage]
: [self showPasswordIssuesPage];
return;
}
PopoverLabelViewController* errorInfoPopover =
[[PopoverLabelViewController alloc] initWithPrimaryAttributedString:info
secondaryAttributedString:nil];
errorInfoPopover.delegate = self;
errorInfoPopover.popoverPresentationController.sourceView = buttonView;
errorInfoPopover.popoverPresentationController.sourceRect = buttonView.bounds;
errorInfoPopover.popoverPresentationController.permittedArrowDirections =
UIPopoverArrowDirectionAny;
[self presentViewController:errorInfoPopover animated:YES completion:nil];
}
#pragma mark - PasswordsConsumer
- (void)setPasswordCheckUIState:(PasswordCheckUIState)state
insecurePasswordsCount:(NSInteger)insecureCount {
self.insecurePasswordsCount = insecureCount;
// Update password check status and check button with new state.
[self updatePasswordCheckButtonWithState:state];
[self updatePasswordCheckStatusLabelWithState:state];
// During searching Password Check section is hidden so cells should not be
// reconfigured.
if (_tableIsInSearchMode) {
_passwordCheckState = state;
return;
}
[self updatePasswordCheckSectionWithState:state];
// When the Password Checkup feature is enabled, this timestamp only appears
// in the detail text of the Password Checkup status cell. It is therefore
// managed in `updatePasswordCheckStatusLabelWithState`.
if (!IsPasswordCheckupEnabled()) {
// Before updating cached state value update timestamp as for proper
// animation it requires both new and old values.
[self updateLastCheckTimestampWithState:state
fromState:_passwordCheckState
update:YES];
}
_passwordCheckState = state;
}
- (void)setSavingPasswordsToAccount:(BOOL)savingPasswordsToAccount {
if (_savingPasswordsToAccount == savingPasswordsToAccount) {
return;
}
_savingPasswordsToAccount = savingPasswordsToAccount;
[self reloadData];
}
- (void)setAffiliatedGroups:
(const std::vector<password_manager::AffiliatedGroup>&)
affiliatedGroups
blockedSites:
(const std::vector<password_manager::CredentialUIEntry>&)
blockedSites {
if (!_didReceivePasswords) {
_blockedSites = blockedSites;
_affiliatedGroups = affiliatedGroups;
[self hideLoadingSpinnerBackground];
} else {
// The AffiliatedGroup equality operator ignores the password stores, but
// this UI cares, see password_manager::ShouldShowLocalOnlyIcon().
// The AffiliatedGroup equality operator ignores password notes, but the UI
// should be updated so that any changes to just notes are visible.
if (_affiliatedGroups == affiliatedGroups &&
_blockedSites == blockedSites &&
AreStoresEqual(_affiliatedGroups, affiliatedGroups) &&
AreIssuesEqual(_affiliatedGroups, affiliatedGroups) &&
AreNotesEqual(_affiliatedGroups, affiliatedGroups)) {
return;
}
_blockedSites = blockedSites;
_affiliatedGroups = affiliatedGroups;
[self updatePasswordManagerUI];
}
}
- (void)updatePasswordManagerUI {
if ([self shouldShowEmptyStateView]) {
[self setEditing:NO animated:YES];
[self reloadData];
return;
}
TableViewModel* model = self.tableViewModel;
NSMutableIndexSet* sectionIdentifiersToUpdate = [NSMutableIndexSet indexSet];
// Hold in reverse order of section indexes (bottom up of section
// displayed). If we don't we'll cause a crash.
std::vector<PasswordSectionIdentifier> sections = {
SectionIdentifierBlocked, SectionIdentifierSavedPasswords};
for (const auto& section : sections) {
bool hasSection = [model hasSectionForSectionIdentifier:section];
bool needsSection = section == SectionIdentifierBlocked
? !_blockedSites.empty()
: [self hasPasswords];
// If section exists but it shouldn't - gracefully remove it with
// animation.
if (!needsSection && hasSection) {
[self clearSectionWithIdentifier:section
withRowAnimation:UITableViewRowAnimationAutomatic];
}
// If section exists and it should - reload it.
else if (needsSection && hasSection) {
[sectionIdentifiersToUpdate addIndex:section];
}
// If section doesn't exist but it should - add it.
else if (needsSection && !hasSection) {
// This is very rare condition, in this case just reload all data.
[self updateUIForEditState];
[self reloadData];
return;
}
}
// After deleting any sections, calculate the indices of sections to be
// updated. Doing this before deleting sections will lead to incorrect indices
// and possible crashes.
NSMutableIndexSet* sectionsToUpdate = [NSMutableIndexSet indexSet];
[sectionIdentifiersToUpdate
enumerateIndexesUsingBlock:^(NSUInteger sectionIdentifier, BOOL* stop) {
[sectionsToUpdate
addIndex:[model sectionForSectionIdentifier:sectionIdentifier]];
}];
// Reload items in sections.
if (sectionsToUpdate.count > 0) {
[self filterItems:self.searchTerm];
[self.tableView reloadSections:sectionsToUpdate
withRowAnimation:UITableViewRowAnimationAutomatic];
[self scrollToLastUpdatedItem];
} else if (_affiliatedGroups.empty() && _blockedSites.empty()) {
[self setEditing:NO animated:YES];
}
}
#pragma mark - UISearchControllerDelegate
- (void)willPresentSearchController:(UISearchController*)searchController {
// This is needed to remove the transparency of the navigation bar at scroll
// edge in iOS 15+ to prevent the following UITableViewRowAnimationTop
// animations from being visible through the navigation bar.
self.navigationController.navigationBar.backgroundColor =
[UIColor colorNamed:kGroupedPrimaryBackgroundColor];
[self showScrim];
// Remove save passwords switch section, password check section and
// on device encryption.
_tableIsInSearchMode = YES;
[self
performBatchTableViewUpdates:^{
// Sections must be removed from bottom to top, otherwise it crashes
[self clearSectionWithIdentifier:SectionIdentifierAddPasswordButton
withRowAnimation:UITableViewRowAnimationTop];
[self clearSectionWithIdentifier:SectionIdentifierPasswordCheck
withRowAnimation:UITableViewRowAnimationTop];
[self clearSectionWithIdentifier:SectionIdentifierManageAccountHeader
withRowAnimation:UITableViewRowAnimationTop];
// Hide the toolbar when the search controller is presented.
self.navigationController.toolbarHidden = YES;
}
completion:nil];
}
- (void)willDismissSearchController:(UISearchController*)searchController {
// This is needed to restore the transparency of the navigation bar at scroll
// edge in iOS 15+.
self.navigationController.navigationBar.backgroundColor = nil;
// No need to restore UI if the Password Manager is being dismissed or if a
// previous call to `willDismissSearchController` already restored the UI.
if (self.navigationController.isBeingDismissed || !_tableIsInSearchMode) {
return;
}
[self hideScrim];
[self searchForTerm:@""];
// Recover save passwords switch section.
TableViewModel* model = self.tableViewModel;
[self.tableView
performBatchUpdates:^{
int sectionIndex = 0;
NSMutableArray<NSIndexPath*>* rowsIndexPaths =
[[NSMutableArray alloc] init];
// Add manage account header.
[model insertSectionWithIdentifier:SectionIdentifierManageAccountHeader
atIndex:sectionIndex];
[model setHeader:self.manageAccountLinkItem
forSectionWithIdentifier:SectionIdentifierManageAccountHeader];
[self.tableView
insertSections:[NSIndexSet indexSetWithIndex:sectionIndex]
withRowAnimation:UITableViewRowAnimationTop];
sectionIndex++;
// Add "Password check" section.
[model insertSectionWithIdentifier:SectionIdentifierPasswordCheck
atIndex:sectionIndex];
NSInteger checkSection =
[model sectionForSectionIdentifier:SectionIdentifierPasswordCheck];
[self.tableView
insertSections:[NSIndexSet indexSetWithIndex:sectionIndex]
withRowAnimation:UITableViewRowAnimationTop];
[model addItem:self.passwordProblemsItem
toSectionWithIdentifier:SectionIdentifierPasswordCheck];
[rowsIndexPaths addObject:[NSIndexPath indexPathForRow:0
inSection:checkSection]];
// Only add check button if kIOSPasswordCheckup is disabled, or if it is
// enabled and the current PasswordCheckUIState requires the button to
// be shown.
if (!IsPasswordCheckupEnabled() || self.shouldShowCheckButton) {
[model addItem:self.checkForProblemsItem
toSectionWithIdentifier:SectionIdentifierPasswordCheck];
[rowsIndexPaths addObject:[NSIndexPath indexPathForRow:1
inSection:checkSection]];
}
sectionIndex++;
// Add "Add Password" button.
if ([self allowsAddPassword]) {
[model insertSectionWithIdentifier:SectionIdentifierAddPasswordButton
atIndex:sectionIndex];
[self.tableView
insertSections:[NSIndexSet indexSetWithIndex:sectionIndex]
withRowAnimation:UITableViewRowAnimationTop];
[model addItem:self.addPasswordItem
toSectionWithIdentifier:SectionIdentifierAddPasswordButton];
[rowsIndexPaths
addObject:
[NSIndexPath
indexPathForRow:0
inSection:
[model
sectionForSectionIdentifier:
SectionIdentifierAddPasswordButton]]];
sectionIndex++;
}
[self.tableView insertRowsAtIndexPaths:rowsIndexPaths
withRowAnimation:UITableViewRowAnimationTop];
// We want to restart the toolbar (display it) when the search bar is
// dismissed only if the current view is the Password Manager.
if ([self.navigationController.topViewController
isKindOfClass:[PasswordManagerViewController class]]) {
self.navigationController.toolbarHidden = NO;
}
_tableIsInSearchMode = NO;
}
completion:nil];
}
#pragma mark - UISearchBarDelegate
- (void)searchBar:(UISearchBar*)searchBar textDidChange:(NSString*)searchText {
if (searchText.length == 0 && self.navigationItem.searchController.active) {
[self showScrim];
} else {
[self hideScrim];
}
[self searchForTerm:searchText];
}
#pragma mark - Private methods
// Shows loading spinner background view.
- (void)showLoadingSpinnerBackground {
if (!self.spinnerView) {
self.spinnerView =
[[HomeWaitingView alloc] initWithFrame:self.tableView.bounds
backgroundColor:UIColor.clearColor];
[self.spinnerView startWaiting];
}
self.navigationItem.searchController.searchBar.userInteractionEnabled = NO;
self.tableView.backgroundView = self.spinnerView;
}
// Hide the loading spinner if it is showing.
- (void)hideLoadingSpinnerBackground {
DCHECK(self.spinnerView);
__weak __typeof(self) weakSelf = self;
[self.spinnerView stopWaitingWithCompletion:^{
[UIView animateWithDuration:0.2
animations:^{
self.spinnerView.alpha = 0.0;
}
completion:^(BOOL finished) {
[weakSelf didHideSpinner];
}];
}];
}
// Called after the loading spinner hiding animation finished. Updates
// `tableViewModel` and then the view hierarchy.
- (void)didHideSpinner {
// Remove spinner view after animation finished.
self.navigationItem.searchController.searchBar.userInteractionEnabled = YES;
self.tableView.backgroundView = nil;
self.spinnerView = nil;
// Update model and view hierarchy.
_didReceivePasswords = YES;
[self updateUIForEditState];
[self reloadData];
}
// Dismisses the search controller when there's a touch event on the scrim.
- (void)dismissSearchController:(UIControl*)sender {
if (self.navigationItem.searchController.active) {
self.navigationItem.searchController.active = NO;
}
}
// Shows scrim overlay and hide toolbar.
- (void)showScrim {
if (self.scrimView.alpha < 1.0f) {
self.scrimView.alpha = 0.0f;
[self.tableView addSubview:self.scrimView];
// We attach our constraints to the superview because the tableView is
// a scrollView and it seems that we get an empty frame when attaching to
// it.
AddSameConstraints(self.scrimView, self.view.superview);
self.tableView.accessibilityElementsHidden = YES;
self.tableView.scrollEnabled = NO;
[UIView animateWithDuration:kTableViewNavigationScrimFadeDuration
animations:^{
self.scrimView.alpha = 1.0f;
[self.view layoutIfNeeded];
}];
}
}
// Hides scrim and restore toolbar.
- (void)hideScrim {
if (self.scrimView.alpha > 0.0f) {
[UIView animateWithDuration:kTableViewNavigationScrimFadeDuration
animations:^{
self.scrimView.alpha = 0.0f;
}
completion:^(BOOL finished) {
[self.scrimView removeFromSuperview];
self.tableView.accessibilityElementsHidden = NO;
self.tableView.scrollEnabled = YES;
}];
}
}
- (void)searchForTerm:(NSString*)searchTerm {
self.searchTerm = searchTerm;
[self filterItems:searchTerm];
TableViewModel* model = self.tableViewModel;
NSMutableIndexSet* indexSet = [NSMutableIndexSet indexSet];
if ([model hasSectionForSectionIdentifier:SectionIdentifierSavedPasswords]) {
NSInteger passwordSection =
[model sectionForSectionIdentifier:SectionIdentifierSavedPasswords];
[indexSet addIndex:passwordSection];
}
if ([model hasSectionForSectionIdentifier:SectionIdentifierBlocked]) {
NSInteger blockedSection =
[model sectionForSectionIdentifier:SectionIdentifierBlocked];
[indexSet addIndex:blockedSection];
}
if (indexSet.count > 0) {
BOOL animationsWereEnabled = [UIView areAnimationsEnabled];
[UIView setAnimationsEnabled:NO];
[self.tableView reloadSections:indexSet
withRowAnimation:UITableViewRowAnimationAutomatic];
[UIView setAnimationsEnabled:animationsWereEnabled];
}
}
- (void)updatePasswordsSectionWithSearchTerm:(NSString*)searchTerm {
for (const auto& affiliatedGroup : _affiliatedGroups) {
AffiliatedGroupTableViewItem* item =
[self savedFormItemForAffiliatedGroup:affiliatedGroup];
bool hidden =
searchTerm.length > 0 &&
![item.title localizedCaseInsensitiveContainsString:searchTerm];
if (hidden) {
continue;
}
[self.tableViewModel addItem:item
toSectionWithIdentifier:SectionIdentifierSavedPasswords];
}
}
// Builds the filtered list of passwords/blocked based on given
// `searchTerm`.
- (void)filterItems:(NSString*)searchTerm {
TableViewModel* model = self.tableViewModel;
if ([self hasPasswords]) {
[model deleteAllItemsFromSectionWithIdentifier:
SectionIdentifierSavedPasswords];
[self updatePasswordsSectionWithSearchTerm:searchTerm];
}
if (!_blockedSites.empty()) {
[model deleteAllItemsFromSectionWithIdentifier:SectionIdentifierBlocked];
for (const auto& credential : _blockedSites) {
BlockedSiteTableViewItem* item = [self blockedSiteItem:credential];
bool hidden =
searchTerm.length > 0 &&
![item.title localizedCaseInsensitiveContainsString:searchTerm];
if (hidden)
continue;
[model addItem:item toSectionWithIdentifier:SectionIdentifierBlocked];
}
}
}
// Update timestamp of the last check. Both old and new password check state
// should be provided in order to animate footer in a proper way.
- (void)updateLastCheckTimestampWithState:(PasswordCheckUIState)state
fromState:(PasswordCheckUIState)oldState
update:(BOOL)update {
if (!_didReceivePasswords || ![self hasPasswords]) {
return;
}
NSInteger checkSection = [self.tableViewModel
sectionForSectionIdentifier:SectionIdentifierPasswordCheck];
switch (state) {
case PasswordCheckStateUnmutedCompromisedPasswords:
[self.tableViewModel setFooter:[self lastCompletedCheckTime]
forSectionWithIdentifier:SectionIdentifierPasswordCheck];
// Transition from disabled to unsafe state is possible only on page load.
// In this case we want to avoid animation.
if (oldState == PasswordCheckStateDisabled) {
[UIView performWithoutAnimation:^{
[self.tableView
reloadSections:[NSIndexSet indexSetWithIndex:checkSection]
withRowAnimation:UITableViewRowAnimationNone];
}];
return;
}
break;
case PasswordCheckStateSafe:
case PasswordCheckStateDefault:
case PasswordCheckStateError:
case PasswordCheckStateSignedOut:
case PasswordCheckStateRunning:
case PasswordCheckStateDisabled:
if (oldState != PasswordCheckStateUnmutedCompromisedPasswords) {
return;
}
[self.tableViewModel setFooter:nil
forSectionWithIdentifier:SectionIdentifierPasswordCheck];
break;
// These states only occur when the kIOSPasswordCheckup feature is enabled
// and the last check timestamp footer item is only shown when
// kIOSPasswordCheckup feature is disabled. These should never be reached.
case PasswordCheckStateReusedPasswords:
case PasswordCheckStateWeakPasswords:
case PasswordCheckStateDismissedWarnings:
NOTREACHED_NORETURN();
}
if (update) {
[self.tableView
performBatchUpdates:^{
if (!self.tableView)
return;
// Deleting and inserting section results in pleasant animation of
// footer being added/removed.
[self.tableView
deleteSections:[NSIndexSet indexSetWithIndex:checkSection]
withRowAnimation:UITableViewRowAnimationNone];
[self.tableView
insertSections:[NSIndexSet indexSetWithIndex:checkSection]
withRowAnimation:UITableViewRowAnimationNone];
}
completion:nil];
}
}
// Updates password check button according to provided state.
- (void)updatePasswordCheckButtonWithState:(PasswordCheckUIState)state {
self.checkForProblemsItem.text =
l10n_util::GetNSString(IDS_IOS_CHECK_PASSWORDS_NOW_BUTTON);
if (self.editing) {
self.checkForProblemsItem.textColor =
[UIColor colorNamed:kTextSecondaryColor];
self.checkForProblemsItem.accessibilityTraits |=
UIAccessibilityTraitNotEnabled;
return;
}
if (IsPasswordCheckupEnabled()) {
switch (state) {
case PasswordCheckStateSafe:
case PasswordCheckStateUnmutedCompromisedPasswords:
case PasswordCheckStateReusedPasswords:
case PasswordCheckStateWeakPasswords:
case PasswordCheckStateDismissedWarnings:
case PasswordCheckStateRunning:
self.shouldShowCheckButton = NO;
break;
case PasswordCheckStateDefault:
case PasswordCheckStateError:
self.shouldShowCheckButton = YES;
[self setCheckForProblemsItemEnabled:YES];
break;
case PasswordCheckStateSignedOut:
self.shouldShowCheckButton = YES;
[self setCheckForProblemsItemEnabled:NO];
break;
// Fall through.
case PasswordCheckStateDisabled:
self.shouldShowCheckButton = YES;
[self setCheckForProblemsItemEnabled:NO];
break;
}
} else {
switch (state) {
case PasswordCheckStateSafe:
case PasswordCheckStateUnmutedCompromisedPasswords:
case PasswordCheckStateReusedPasswords:
case PasswordCheckStateWeakPasswords:
case PasswordCheckStateDismissedWarnings:
case PasswordCheckStateDefault:
case PasswordCheckStateError:
[self setCheckForProblemsItemEnabled:YES];
break;
case PasswordCheckStateSignedOut:
[self setCheckForProblemsItemEnabled:NO];
break;
case PasswordCheckStateRunning:
// Fall through.
case PasswordCheckStateDisabled:
[self setCheckForProblemsItemEnabled:NO];
break;
}
}
}
// Updates password check status label according to provided state.
- (void)updatePasswordCheckStatusLabelWithState:(PasswordCheckUIState)state {
self.passwordProblemsItem.trailingImage = nil;
self.passwordProblemsItem.trailingImageTintColor = nil;
self.passwordProblemsItem.enabled = !self.editing;
self.passwordProblemsItem.indicatorHidden = YES;
self.passwordProblemsItem.infoButtonHidden = YES;
self.passwordProblemsItem.accessoryType =
IsPasswordCheckTappable(state)
? UITableViewCellAccessoryDisclosureIndicator
: UITableViewCellAccessoryNone;
self.passwordProblemsItem.text =
IsPasswordCheckupEnabled()
? l10n_util::GetNSString(IDS_IOS_PASSWORD_CHECKUP)
: l10n_util::GetNSString(IDS_IOS_CHECK_PASSWORDS);
self.passwordProblemsItem.detailText =
IsPasswordCheckupEnabled()
? l10n_util::GetNSString(IDS_IOS_PASSWORD_CHECKUP_DESCRIPTION)
: l10n_util::GetNSString(IDS_IOS_CHECK_PASSWORDS_DESCRIPTION);
switch (state) {
case PasswordCheckStateRunning: {
if (IsPasswordCheckupEnabled()) {
self.passwordProblemsItem.text =
l10n_util::GetNSString(IDS_IOS_PASSWORD_CHECKUP_ONGOING);
self.passwordProblemsItem.detailText =
base::SysUTF16ToNSString(l10n_util::GetPluralStringFUTF16(
IDS_IOS_PASSWORD_CHECKUP_SITES_AND_APPS_COUNT,
_affiliatedGroups.size()));
}
self.passwordProblemsItem.indicatorHidden = NO;
break;
}
case PasswordCheckStateDisabled: {
self.passwordProblemsItem.enabled = NO;
break;
}
case PasswordCheckStateUnmutedCompromisedPasswords: {
self.passwordProblemsItem.detailText =
base::SysUTF16ToNSString(l10n_util::GetPluralStringFUTF16(
IsPasswordCheckupEnabled()
? IDS_IOS_PASSWORD_CHECKUP_COMPROMISED_COUNT
: IDS_IOS_CHECK_PASSWORDS_COMPROMISED_COUNT,
self.insecurePasswordsCount));
self.passwordProblemsItem.warningState = WarningState::kSevereWarning;
// The red tint color for the compromised password warning here depends on
// the Password Grouping feature (which will be enabled before Password
// Checkup). Overriding the tint color set by setting the item's warning
// state to make sure it is the correct one for the Password Grouping
// feature. TODO(crbug.com/1406871): Remove line when kIOSPasswordCheckup
// is enabled by default.
self.passwordProblemsItem.trailingImageTintColor =
[UIColor colorNamed:kRed500Color];
break;
}
case PasswordCheckStateReusedPasswords: {
self.passwordProblemsItem.detailText = l10n_util::GetNSStringF(
IDS_IOS_PASSWORD_CHECKUP_REUSED_COUNT,
base::NumberToString16(self.insecurePasswordsCount));
self.passwordProblemsItem.warningState = WarningState::kWarning;
break;
}
case PasswordCheckStateWeakPasswords: {
self.passwordProblemsItem.detailText = base::SysUTF16ToNSString(
l10n_util::GetPluralStringFUTF16(IDS_IOS_PASSWORD_CHECKUP_WEAK_COUNT,
self.insecurePasswordsCount));
self.passwordProblemsItem.warningState = WarningState::kWarning;
break;
}
case PasswordCheckStateDismissedWarnings: {
self.passwordProblemsItem.detailText =
base::SysUTF16ToNSString(l10n_util::GetPluralStringFUTF16(
IDS_IOS_PASSWORD_CHECKUP_DISMISSED_COUNT,
self.insecurePasswordsCount));
self.passwordProblemsItem.warningState = WarningState::kWarning;
break;
}
case PasswordCheckStateSafe: {
self.passwordProblemsItem.detailText =
IsPasswordCheckupEnabled()
? [self.delegate formattedElapsedTimeSinceLastCheck]
: base::SysUTF16ToNSString(l10n_util::GetPluralStringFUTF16(
IDS_IOS_PASSWORD_CHECKUP_COMPROMISED_COUNT, 0));
self.passwordProblemsItem.warningState = WarningState::kSafe;
break;
}
case PasswordCheckStateDefault:
break;
case PasswordCheckStateError:
case PasswordCheckStateSignedOut: {
self.passwordProblemsItem.detailText =
IsPasswordCheckupEnabled()
? l10n_util::GetNSString(IDS_IOS_PASSWORD_CHECKUP_ERROR)
: l10n_util::GetNSString(IDS_IOS_PASSWORD_CHECK_ERROR);
self.passwordProblemsItem.infoButtonHidden = NO;
break;
}
}
// Notify accessibility to focus on the Password Check Status cell if needed.
if ([self shouldFocusAccessibilityOnPasswordCheckStatusForState:state]) {
[self focusAccessibilityOnPasswordCheckStatus];
self.checkWasTriggeredManually = NO;
}
}
// Enables or disables the `checkForProblemsItem` and sets it up accordingly.
- (void)setCheckForProblemsItemEnabled:(BOOL)enabled {
self.checkForProblemsItem.enabled = enabled;
if (enabled) {
self.checkForProblemsItem.textColor = [UIColor colorNamed:kBlueColor];
self.checkForProblemsItem.accessibilityTraits &=
~UIAccessibilityTraitNotEnabled;
} else {
self.checkForProblemsItem.textColor =
[UIColor colorNamed:kTextSecondaryColor];
self.checkForProblemsItem.accessibilityTraits |=
UIAccessibilityTraitNotEnabled;
}
}
- (void)setAddPasswordButtonEnabled:(BOOL)enabled {
if (enabled) {
self.addPasswordItem.textColor = [UIColor colorNamed:kBlueColor];
self.addPasswordItem.accessibilityTraits &= ~UIAccessibilityTraitNotEnabled;
} else {
self.addPasswordItem.textColor = [UIColor colorNamed:kTextSecondaryColor];
self.addPasswordItem.accessibilityTraits |= UIAccessibilityTraitNotEnabled;
}
[self reconfigureCellsForItems:@[ self.addPasswordItem ]];
}
// Removes the given section if it exists.
- (void)clearSectionWithIdentifier:(NSInteger)sectionIdentifier
withRowAnimation:(UITableViewRowAnimation)animation {
TableViewModel* model = self.tableViewModel;
if ([model hasSectionForSectionIdentifier:sectionIdentifier]) {
NSInteger section = [model sectionForSectionIdentifier:sectionIdentifier];
[model removeSectionWithIdentifier:sectionIdentifier];
[[self tableView] deleteSections:[NSIndexSet indexSetWithIndex:section]
withRowAnimation:animation];
}
}
- (void)deleteItemAtIndexPaths:(NSArray<NSIndexPath*>*)indexPaths {
std::vector<password_manager::CredentialUIEntry> credentialsToDelete;
for (NSIndexPath* indexPath in indexPaths) {
// Only form items are editable.
NSInteger itemType = [self.tableViewModel itemTypeForIndexPath:indexPath];
TableViewItem* item = [self.tableViewModel itemAtIndexPath:indexPath];
// Remove affiliated group.
if (itemType == ItemTypeSavedPassword) {
password_manager::AffiliatedGroup affiliatedGroup =
base::apple::ObjCCastStrict<AffiliatedGroupTableViewItem>(item)
.affiliatedGroup;
// Remove from local cache.
auto iterator = base::ranges::find(_affiliatedGroups, affiliatedGroup);
if (iterator != _affiliatedGroups.end())
_affiliatedGroups.erase(iterator);
// Add to the credentials to delete vector to remove from store.
credentialsToDelete.insert(credentialsToDelete.end(),
affiliatedGroup.GetCredentials().begin(),
affiliatedGroup.GetCredentials().end());
} else if (itemType == ItemTypeBlocked) {
password_manager::CredentialUIEntry credential =
base::apple::ObjCCastStrict<BlockedSiteTableViewItem>(item)
.credential;
auto removeCredential =
[](std::vector<password_manager::CredentialUIEntry>& credentials,
const password_manager::CredentialUIEntry& credential) {
auto iterator = base::ranges::find(credentials, credential);
if (iterator != credentials.end())
credentials.erase(iterator);
};
removeCredential(_blockedSites, credential);
credentialsToDelete.push_back(std::move(credential));
}
}
// Remove empty sections.
__weak PasswordManagerViewController* weakSelf = self;
[self.tableView
performBatchUpdates:^{
PasswordManagerViewController* strongSelf = weakSelf;
if (!strongSelf)
return;
[strongSelf removeFromModelItemAtIndexPaths:indexPaths];
[strongSelf.tableView
deleteRowsAtIndexPaths:indexPaths
withRowAnimation:UITableViewRowAnimationAutomatic];
// Delete in reverse order of section indexes (bottom up of section
// displayed), so that indexes in model matches those in the view. if
// we don't we'll cause a crash.
if (strongSelf->_blockedSites.empty()) {
[strongSelf
clearSectionWithIdentifier:SectionIdentifierBlocked
withRowAnimation:UITableViewRowAnimationAutomatic];
}
if (![strongSelf hasPasswords]) {
[strongSelf
clearSectionWithIdentifier:SectionIdentifierSavedPasswords
withRowAnimation:UITableViewRowAnimationAutomatic];
}
}
completion:^(BOOL finished) {
PasswordManagerViewController* strongSelf = weakSelf;
if (!strongSelf)
return;
// If both lists are empty, exit editing mode.
if (![strongSelf hasPasswords] && strongSelf->_blockedSites.empty()) {
[strongSelf setEditing:NO animated:YES];
// An illustrated empty state is required, so reload the whole model.
[strongSelf reloadData];
}
[strongSelf updateUIForEditState];
}];
[self.delegate deleteCredentials:credentialsToDelete];
}
// Notifies the handler to show the Password Checkup homepage if the state of
// the Password Check cell allows it.
- (void)showPasswordCheckupPage {
if (!IsPasswordCheckTappable(self.passwordCheckState)) {
return;
}
[self.handler showPasswordCheckup];
}
// Notifies the handler to show the password issues page if the state of the
// Password Check cell allows it.
// TODO(crbug.com/1406871): Remove when kIOSPasswordCheckup is enabled by
// default.
- (void)showPasswordIssuesPage {
if (!IsPasswordCheckTappable(self.passwordCheckState)) {
return;
}
[self.handler showPasswordIssues];
password_manager::LogPasswordCheckReferrer(
password_manager::PasswordCheckReferrer::kPasswordSettings);
}
// Scrolls the password lists such that most recently updated
// SavedFormContentItem is in the top of the screen.
- (void)scrollToLastUpdatedItem {
if (self.mostRecentlyUpdatedItem) {
NSIndexPath* indexPath =
[self.tableViewModel indexPathForItem:self.mostRecentlyUpdatedItem];
[self.tableView scrollToRowAtIndexPath:indexPath
atScrollPosition:UITableViewScrollPositionTop
animated:NO];
self.mostRecentlyUpdatedItem = nil;
}
}
// Returns YES if accessibility should focus on the Password Check Status cell.
- (BOOL)shouldFocusAccessibilityOnPasswordCheckStatusForState:
(PasswordCheckUIState)state {
if (!UIAccessibilityIsVoiceOverRunning()) {
return false;
}
BOOL passwordCheckStateIsValid = state != PasswordCheckStateDefault &&
state != PasswordCheckStateRunning &&
state != PasswordCheckStateDisabled;
// When kIOSPasswordCheckup is disabled, accessibility should focus on the
// Password Check Status cell when:
// 1. The password check was triggered manually.
// AND
// 2. The password check state changed to insecure (compromised, reused, weak
// or dismissed warnings), safe or error (i.e., any state other than default,
// running and disabled).
if (!IsPasswordCheckupEnabled()) {
return self.checkWasTriggeredManually && passwordCheckStateIsValid;
}
// When kIOSPasswordCheckup is enabled, accessibility should focus on the
// Password Check Status cell when:
// 1. The password check was triggered manually (because the "Check Now"
// button dissapears afterwards, so the focus should move to the status cell).
// OR
// 2. The focus was already on the Password Check Status cell. AND
// 3. The password check state changed to insecure (compromised, reused, weak
// or dismissed warnings), safe or error (i.e., any state other than default,
// running and disabled).
return self.checkWasTriggeredManually ||
([self isPasswordCheckStatusFocusedByVoiceOver] &&
passwordCheckStateIsValid);
}
// Returns YES if the Password Check Staus cell is currently focused by
// accessibility.
- (BOOL)isPasswordCheckStatusFocusedByVoiceOver {
if (![self.tableViewModel
hasItemForItemType:ItemTypePasswordCheckStatus
sectionIdentifier:SectionIdentifierPasswordCheck]) {
return false;
}
// Get the Password Check Status cell.
NSIndexPath* indexPath =
[self.tableViewModel indexPathForItemType:ItemTypePasswordCheckStatus
sectionIdentifier:SectionIdentifierPasswordCheck];
UITableViewCell* passwordCheckStatusCell =
[self.tableView cellForRowAtIndexPath:indexPath];
// Get the view element that is currently focused.
UIAccessibilityElement* focusedElement = UIAccessibilityFocusedElement(
UIAccessibilityNotificationVoiceOverIdentifier);
return [passwordCheckStatusCell.accessibilityLabel
isEqualToString:focusedElement.accessibilityLabel];
}
// Notifies accessibility to focus on the Password Check Status cell when its
// layout changed.
- (void)focusAccessibilityOnPasswordCheckStatus {
if ([self.tableViewModel hasItemForItemType:ItemTypePasswordCheckStatus
sectionIdentifier:SectionIdentifierPasswordCheck]) {
NSIndexPath* indexPath = [self.tableViewModel
indexPathForItemType:ItemTypePasswordCheckStatus
sectionIdentifier:SectionIdentifierPasswordCheck];
UITableViewCell* cell = [self.tableView cellForRowAtIndexPath:indexPath];
UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification,
cell);
}
}
- (void)setPasswordProblemsItemAccessibilityLabelForSafeState {
NSIndexPath* indexPath =
[self.tableViewModel indexPathForItemType:ItemTypePasswordCheckStatus
sectionIdentifier:SectionIdentifierPasswordCheck];
UITableViewCell* passwordProblemsCell =
[self.tableView cellForRowAtIndexPath:indexPath];
passwordProblemsCell.accessibilityLabel = [NSString
stringWithFormat:
@"%@. %@", passwordProblemsCell.accessibilityLabel,
l10n_util::GetNSString(
IDS_IOS_PASSWORD_CHECKUP_SAFE_STATE_ACCESSIBILITY_LABEL)];
}
// Logs metrics related to favicons for the Password Manager.
- (void)logMetricsForFavicons {
DCHECK(!_faviconMetricLogged);
int n_monograms = 0;
int n_images = 0;
std::vector sections_and_types = {
std::pair{SectionIdentifierSavedPasswords, ItemTypeSavedPassword},
std::pair{SectionIdentifierBlocked, ItemTypeBlocked}};
for (auto [section, type] : sections_and_types) {
if (![self.tableViewModel hasSectionForSectionIdentifier:section]) {
continue;
}
NSArray<NSIndexPath*>* indexPaths =
[self.tableViewModel indexPathsForItemType:type
sectionIdentifier:section];
for (NSIndexPath* indexPath : indexPaths) {
PasswordFormContentCell* cell =
[self.tableView cellForRowAtIndexPath:indexPath];
if (!cell) {
// Cell not queued for displaying yet.
continue;
}
switch (cell.faviconTypeForMetrics) {
case FaviconTypeNotLoaded:
continue;
case FaviconTypeMonogram:
n_monograms++;
break;
case FaviconTypeImage:
n_images++;
break;
}
}
}
base::UmaHistogramCounts10000(
"IOS.PasswordManager.PasswordsWithFavicons.Count",
n_images + n_monograms);
if (n_images + n_monograms > 0) {
base::UmaHistogramCounts10000("IOS.PasswordManager.Favicons.Count",
n_images);
base::UmaHistogramPercentage("IOS.PasswordManager.Favicons.Percentage",
100.0f * n_images / (n_images + n_monograms));
}
}
- (bool)allowsAddPassword {
// If the settings are managed by enterprise policy and the password manager
// is not enabled, there won't be any add functionality.
const char* prefName = password_manager::prefs::kCredentialsEnableService;
return !self.prefService->IsManagedPreference(prefName) ||
self.prefService->GetBoolean(prefName);
}
// Configures the title of this ViewController.
- (void)setUpTitle {
self.title = l10n_util::GetNSString(IDS_IOS_PASSWORD_MANAGER);
self.navigationItem.titleView =
password_manager::CreatePasswordManagerTitleView(/*title=*/self.title);
}
// Shows the empty state view when there is no content to display in the
// tableView, otherwise hides the empty state view if one is being displayed.
- (void)showOrHideEmptyView {
if (![self hasPasswords] && _blockedSites.empty()) {
NSString* title =
l10n_util::GetNSString(IDS_IOS_SETTINGS_PASSWORD_EMPTY_TITLE);
NSDictionary* textAttributes =
[TableViewIllustratedEmptyView defaultTextAttributesForSubtitle];
NSURL* linkURL = net::NSURLWithGURL(google_util::AppendGoogleLocaleParam(
GURL(password_manager::kPasswordManagerHelpCenteriOSURL),
GetApplicationContext()->GetApplicationLocale()));
NSDictionary* linkAttributes = @{
NSForegroundColorAttributeName : [UIColor colorNamed:kBlueColor],
NSLinkAttributeName : linkURL,
};
NSAttributedString* subtitle = AttributedStringFromStringWithLink(
l10n_util::GetNSString(IDS_IOS_SAVE_PASSWORDS_MANAGE_ACCOUNT_HEADER),
textAttributes, linkAttributes);
[self addEmptyTableViewWithImage:[UIImage imageNamed:@"passwords_empty"]
title:title
attributedSubtitle:subtitle
delegate:self];
self.navigationItem.searchController = nil;
self.tableView.alwaysBounceVertical = NO;
} else {
[self removeEmptyTableView];
self.navigationItem.searchController = self.searchController;
self.tableView.alwaysBounceVertical = YES;
}
}
// Private accessor to `_didReceivePasswords` only exposed to unit tests.
- (BOOL)didReceivePasswords {
return _didReceivePasswords;
}
- (void)settingsButtonCallback {
[self.presentationDelegate showPasswordSettingsSubmenu];
}
- (void)addButtonCallback {
[self.handler showAddPasswordSheet];
}
- (UIBarButtonItem*)settingsButtonInToolbar {
if (!_settingsButtonInToolbar) {
_settingsButtonInToolbar =
[self settingsButtonWithAction:@selector(settingsButtonCallback)];
}
return _settingsButtonInToolbar;
}
- (UIBarButtonItem*)addButtonInToolbar {
if (!_addButtonInToolbar) {
_addButtonInToolbar =
[self addButtonWithAction:@selector(addButtonCallback)];
}
return _addButtonInToolbar;
}
// Helper method determining if the empty state view should be displayed.
- (BOOL)shouldShowEmptyStateView {
return ![self hasPasswords] && _blockedSites.empty();
}
- (void)deleteItemAtIndexPathsForTesting:(NSArray<NSIndexPath*>*)indexPaths {
[self deleteItemAtIndexPaths:indexPaths];
}
- (void)updatePasswordCheckSectionWithState:(PasswordCheckUIState)state {
if (![self.tableViewModel
hasSectionForSectionIdentifier:SectionIdentifierPasswordCheck]) {
return;
}
[self.tableView
performBatchUpdates:^{
if (self.passwordProblemsItem) {
[self reconfigureCellsForItems:@[ self.passwordProblemsItem ]];
// When in safe state, a custom accessibility label needs to be set
// for the Password Checkup cell.
if (state == PasswordCheckStateSafe) {
[self setPasswordProblemsItemAccessibilityLabelForSafeState];
}
}
if (self.checkForProblemsItem) {
// If kIOSPasswordCheckup feature is disabled, only reconfigure the
// check button cell.
if (!IsPasswordCheckupEnabled()) {
[self reconfigureCellsForItems:@[ self.checkForProblemsItem ]];
} else {
BOOL checkForProblemsItemIsInModel = [self.tableViewModel
hasItemForItemType:ItemTypeCheckForProblemsButton
sectionIdentifier:SectionIdentifierPasswordCheck];
// Check if the check button should be removed from the table view.
if (!self.shouldShowCheckButton && checkForProblemsItemIsInModel) {
[self.tableView
deleteRowsAtIndexPaths:@[ [self checkButtonIndexPath] ]
withRowAnimation:UITableViewRowAnimationAutomatic];
[self.tableViewModel
removeItemWithType:ItemTypeCheckForProblemsButton
fromSectionWithIdentifier:SectionIdentifierPasswordCheck];
} else if (self.shouldShowCheckButton) {
[self reconfigureCellsForItems:@[ self.checkForProblemsItem ]];
// Check if the check button should be added to the table view.
if (!checkForProblemsItemIsInModel) {
[self.tableViewModel addItem:self.checkForProblemsItem
toSectionWithIdentifier:SectionIdentifierPasswordCheck];
[self.tableView
insertRowsAtIndexPaths:@[ [self checkButtonIndexPath] ]
withRowAnimation:UITableViewRowAnimationAutomatic];
}
}
}
}
[self.tableView layoutIfNeeded];
}
completion:nil];
}
- (NSIndexPath*)checkButtonIndexPath {
return
[self.tableViewModel indexPathForItemType:ItemTypeCheckForProblemsButton
sectionIdentifier:SectionIdentifierPasswordCheck];
}
- (void)showDetailedViewPageForItem:(TableViewItem*)item {
[self.handler
showDetailedViewForAffiliatedGroup:base::apple::ObjCCastStrict<
AffiliatedGroupTableViewItem>(item)
.affiliatedGroup];
}
#pragma mark - UITableViewDelegate
- (void)tableView:(UITableView*)tableView
didSelectRowAtIndexPath:(NSIndexPath*)indexPath {
[super tableView:tableView didSelectRowAtIndexPath:indexPath];
// Actions should only take effect when not in editing mode.
if (self.editing) {
self.deleteButton.enabled = YES;
return;
}
TableViewModel* model = self.tableViewModel;
ItemType itemType =
static_cast<ItemType>([model itemTypeForIndexPath:indexPath]);
switch (itemType) {
case ItemTypePasswordCheckStatus:
IsPasswordCheckupEnabled() ? [self showPasswordCheckupPage]
: [self showPasswordIssuesPage];
break;
case ItemTypeSavedPassword: {
DCHECK_EQ(SectionIdentifierSavedPasswords,
[model sectionIdentifierForSectionIndex:indexPath.section]);
TableViewItem* item = [model itemAtIndexPath:indexPath];
if (!IsPasswordNotesWithBackupEnabled() ||
password_manager::features::IsAuthOnEntryV2Enabled()) {
[self showDetailedViewPageForItem:item];
} else if ([self.reauthenticationModule canAttemptReauth]) {
void (^showPasswordDetailsHandler)(ReauthenticationResult) =
^(ReauthenticationResult result) {
if (result == ReauthenticationResult::kFailure) {
return;
}
[self showDetailedViewPageForItem:item];
};
[self.reauthenticationModule
attemptReauthWithLocalizedReason:
l10n_util::GetNSString(
IDS_IOS_SETTINGS_PASSWORD_REAUTH_REASON_SHOW)
canReusePreviousAuth:YES
handler:showPasswordDetailsHandler];
} else {
DCHECK(self.handler);
[self.handler showSetupPasscodeDialog];
}
break;
}
case ItemTypeBlocked: {
DCHECK_EQ(SectionIdentifierBlocked,
[model sectionIdentifierForSectionIndex:indexPath.section]);
password_manager::CredentialUIEntry credential =
base::apple::ObjCCastStrict<BlockedSiteTableViewItem>(
[model itemAtIndexPath:indexPath])
.credential;
[self.handler showDetailedViewForCredential:credential];
break;
}
case ItemTypeCheckForProblemsButton:
if (self.passwordCheckState != PasswordCheckStateRunning) {
[self.delegate startPasswordCheck];
password_manager::LogStartPasswordCheckManually();
self.checkWasTriggeredManually = YES;
}
break;
case ItemTypeAddPasswordButton: {
[self.handler showAddPasswordSheet];
break;
}
case ItemTypeLastCheckTimestampFooter:
case ItemTypeLinkHeader:
case ItemTypeHeader:
NOTREACHED();
}
[tableView deselectRowAtIndexPath:indexPath animated:YES];
}
- (void)tableView:(UITableView*)tableView
didDeselectRowAtIndexPath:(NSIndexPath*)indexPath {
[super tableView:tableView didDeselectRowAtIndexPath:indexPath];
if (!self.editing) {
return;
}
if (self.tableView.indexPathsForSelectedRows.count == 0) {
self.deleteButton.enabled = NO;
}
}
- (BOOL)tableView:(UITableView*)tableView
shouldHighlightRowAtIndexPath:(NSIndexPath*)indexPath {
NSInteger itemType = [self.tableViewModel itemTypeForIndexPath:indexPath];
switch (itemType) {
case ItemTypePasswordCheckStatus:
return IsPasswordCheckTappable(self.passwordCheckState);
case ItemTypeCheckForProblemsButton:
return self.checkForProblemsItem.isEnabled;
case ItemTypeAddPasswordButton:
return [self allowsAddPassword];
}
return YES;
}
- (UIView*)tableView:(UITableView*)tableView
viewForHeaderInSection:(NSInteger)section {
UIView* view = [super tableView:tableView viewForHeaderInSection:section];
if ([self.tableViewModel sectionIdentifierForSectionIndex:section] ==
SectionIdentifierManageAccountHeader) {
// This is the text at the top of the page with a link. Attach as a delegate
// to ensure clicks on the link are handled.
TableViewLinkHeaderFooterView* linkView =
base::apple::ObjCCastStrict<TableViewLinkHeaderFooterView>(view);
linkView.delegate = self;
}
return view;
}
- (CGFloat)tableView:(UITableView*)tableView
heightForFooterInSection:(NSInteger)section {
// Customize height of emtpy footer for manage account header section to
// achieve desired vertical spacing to next item.
if ([self.tableViewModel sectionIdentifierForSectionIndex:section] ==
SectionIdentifierManageAccountHeader) {
return kManageAccountHeaderSectionFooterHeight;
}
return [super tableView:tableView heightForFooterInSection:section];
}
#pragma mark - UITableViewDataSource
- (BOOL)tableView:(UITableView*)tableView
canEditRowAtIndexPath:(NSIndexPath*)indexPath {
// Only password cells are editable.
NSInteger itemType = [self.tableViewModel itemTypeForIndexPath:indexPath];
return itemType == ItemTypeSavedPassword || itemType == ItemTypeBlocked;
}
- (void)tableView:(UITableView*)tableView
commitEditingStyle:(UITableViewCellEditingStyle)editingStyle
forRowAtIndexPath:(NSIndexPath*)indexPath {
if (editingStyle != UITableViewCellEditingStyleDelete)
return;
[self deleteItemAtIndexPaths:@[ indexPath ]];
}
- (UITableViewCell*)tableView:(UITableView*)tableView
cellForRowAtIndexPath:(NSIndexPath*)indexPath {
UITableViewCell* cell = [super tableView:tableView
cellForRowAtIndexPath:indexPath];
switch ([self.tableViewModel itemTypeForIndexPath:indexPath]) {
case ItemTypePasswordCheckStatus: {
SettingsCheckCell* passwordCheckCell =
base::apple::ObjCCastStrict<SettingsCheckCell>(cell);
[passwordCheckCell.infoButton
addTarget:self
action:@selector(didTapPasswordCheckInfoButton:)
forControlEvents:UIControlEventTouchUpInside];
break;
}
case ItemTypeSavedPassword:
case ItemTypeBlocked: {
// Load the favicon from cache.
[base::apple::ObjCCastStrict<PasswordFormContentCell>(cell)
loadFavicon:self.imageDataSource];
break;
}
}
return cell;
}
#pragma mark Helper methods
// Enables/disables search bar.
- (void)setSearchBarEnabled:(BOOL)enabled {
if (enabled) {
self.navigationItem.searchController.searchBar.userInteractionEnabled = YES;
self.navigationItem.searchController.searchBar.alpha = 1.0;
} else {
self.navigationItem.searchController.searchBar.userInteractionEnabled = NO;
self.navigationItem.searchController.searchBar.alpha =
kTableViewNavigationAlphaForDisabledSearchBar;
}
}
#pragma mark - ChromeAccountManagerServiceObserver
- (void)identityListChanged {
[self reloadData];
}
#pragma mark - UIAdaptivePresentationControllerDelegate
- (void)presentationControllerDidDismiss:
(UIPresentationController*)presentationController {
base::RecordAction(
base::UserMetricsAction("IOSPasswordsSettingsCloseWithSwipe"));
_accountManagerServiceObserver.reset();
}
#pragma mark - TableViewIllustratedEmptyViewDelegate
- (void)tableViewIllustratedEmptyView:(TableViewIllustratedEmptyView*)view
didTapSubtitleLink:(NSURL*)URL {
[self didTapLinkURL:URL];
}
@end