Index: ios/chrome/browser/ui/settings/save_passwords_collection_view_controller.mm |
diff --git a/ios/chrome/browser/ui/settings/save_passwords_collection_view_controller.mm b/ios/chrome/browser/ui/settings/save_passwords_collection_view_controller.mm |
new file mode 100644 |
index 0000000000000000000000000000000000000000..05648dba3adcec452ce1b768bd8385583c90accf |
--- /dev/null |
+++ b/ios/chrome/browser/ui/settings/save_passwords_collection_view_controller.mm |
@@ -0,0 +1,626 @@ |
+// Copyright 2015 The Chromium Authors. All rights reserved. |
+// Use of this source code is governed by a BSD-style license that can be |
+// found in the LICENSE file. |
+ |
+#import "ios/chrome/browser/ui/settings/save_passwords_collection_view_controller.h" |
+ |
+#include <memory> |
+ |
+#include "base/ios/ios_util.h" |
+#include "base/ios/weak_nsobject.h" |
+#include "base/logging.h" |
+#include "base/mac/foundation_util.h" |
+#include "base/mac/objc_property_releaser.h" |
+#include "base/mac/scoped_nsobject.h" |
+#include "base/memory/scoped_vector.h" |
+#include "base/numerics/safe_conversions.h" |
+#include "base/strings/sys_string_conversions.h" |
+#include "base/strings/utf_string_conversions.h" |
+#include "components/autofill/core/common/password_form.h" |
+#include "components/browser_sync/profile_sync_service.h" |
+#include "components/google/core/browser/google_util.h" |
+#include "components/keyed_service/core/service_access_type.h" |
+#include "components/password_manager/core/browser/affiliation_utils.h" |
+#include "components/password_manager/core/browser/password_bubble_experiment.h" |
+#include "components/password_manager/core/browser/password_manager_constants.h" |
+#include "components/password_manager/core/browser/password_store.h" |
+#include "components/password_manager/core/browser/password_store_consumer.h" |
+#include "components/password_manager/core/common/password_manager_pref_names.h" |
+#include "components/prefs/pref_member.h" |
+#include "components/prefs/pref_service.h" |
+#include "components/strings/grit/components_strings.h" |
+#include "components/url_formatter/url_formatter.h" |
+#include "ios/chrome/browser/application_context.h" |
+#include "ios/chrome/browser/browser_state/chrome_browser_state.h" |
+#include "ios/chrome/browser/experimental_flags.h" |
+#include "ios/chrome/browser/passwords/ios_chrome_password_store_factory.h" |
+#include "ios/chrome/browser/sync/ios_chrome_profile_sync_service_factory.h" |
+#import "ios/chrome/browser/ui/collection_view/cells/MDCCollectionViewCell+Chrome.h" |
+#import "ios/chrome/browser/ui/collection_view/cells/collection_view_footer_item.h" |
+#import "ios/chrome/browser/ui/collection_view/cells/collection_view_item.h" |
+#import "ios/chrome/browser/ui/collection_view/cells/collection_view_switch_item.h" |
+#import "ios/chrome/browser/ui/collection_view/cells/collection_view_text_item.h" |
+#import "ios/chrome/browser/ui/collection_view/collection_view_model.h" |
+#import "ios/chrome/browser/ui/settings/password_details_collection_view_controller.h" |
+#import "ios/chrome/browser/ui/settings/reauthentication_module.h" |
+#import "ios/chrome/browser/ui/settings/settings_utils.h" |
+#import "ios/chrome/browser/ui/settings/utils/pref_backed_boolean.h" |
+#include "ios/chrome/grit/ios_strings.h" |
+#import "ios/third_party/material_components_ios/src/components/Palettes/src/MaterialPalettes.h" |
+#include "ui/base/l10n/l10n_util_mac.h" |
+#include "url/gurl.h" |
+ |
+namespace { |
+ |
+typedef NS_ENUM(NSInteger, SectionIdentifier) { |
+ SectionIdentifierMessage = kSectionIdentifierEnumZero, |
+ SectionIdentifierSavePasswordsSwitch, |
+ SectionIdentifierSavedPasswords, |
+ SectionIdentifierBlacklist, |
+}; |
+ |
+typedef NS_ENUM(NSInteger, ItemType) { |
+ ItemTypeManageAccount = kItemTypeEnumZero, |
+ ItemTypeHeader, |
+ ItemTypeSavePasswordsSwitch, |
+ ItemTypeSavedPassword, // This is a repeated item type. |
+ ItemTypeBlacklisted, // This is a repeated item type. |
+}; |
+ |
+// TODO(crbug.com/669538): This function should be removed after original |
+// version of GetHumanReadableOrigin will be moved to affiliation_utils. |
+std::string GetHumanReadableOriginCopy( |
+ const autofill::PasswordForm& password_form) { |
+ password_manager::FacetURI facet_uri = |
+ password_manager::FacetURI::FromPotentiallyInvalidSpec( |
+ password_form.signon_realm); |
+ if (facet_uri.IsValidAndroidFacetURI()) |
+ return facet_uri.scheme() + "://" + facet_uri.android_package_name(); |
+ return base::UTF16ToUTF8(url_formatter::FormatUrl(password_form.origin)); |
+} |
+ |
+} // namespace |
+ |
+@interface SavePasswordsCollectionViewController (UsedBySavePasswordsConsumer) |
+// Callback called when the async request launched from |
+// |getLoginsFromPasswordStore| finishes. |
+- (void)onGetPasswordStoreResults: |
+ (const std::vector<std::unique_ptr<autofill::PasswordForm>>&)result; |
+@end |
+ |
+namespace password_manager { |
+// A bridge C++ class passing notification about finished password store |
+// requests to owning Obj-C class SavePasswordsCollectionViewController. |
+class SavePasswordsConsumer : public PasswordStoreConsumer { |
+ public: |
+ explicit SavePasswordsConsumer( |
+ SavePasswordsCollectionViewController* delegate); |
+ ~SavePasswordsConsumer() override; |
+ void OnGetPasswordStoreResults( |
+ std::vector<std::unique_ptr<autofill::PasswordForm>> results) override; |
+ |
+ private: |
+ __unsafe_unretained SavePasswordsCollectionViewController* delegate_; // weak |
+ DISALLOW_COPY_AND_ASSIGN(SavePasswordsConsumer); |
+}; |
+ |
+SavePasswordsConsumer::SavePasswordsConsumer( |
+ SavePasswordsCollectionViewController* delegate) |
+ : delegate_(delegate) {} |
+ |
+SavePasswordsConsumer::~SavePasswordsConsumer() {} |
+ |
+void SavePasswordsConsumer::OnGetPasswordStoreResults( |
+ std::vector<std::unique_ptr<autofill::PasswordForm>> results) { |
+ if (!results.empty()) |
+ [delegate_ onGetPasswordStoreResults:results]; |
+} |
+} // namespace password_manager |
+ |
+// Use the type of the items to convey the Saved/Blacklisted status. |
+@interface SavedFormContentItem : CollectionViewTextItem |
+@end |
+@implementation SavedFormContentItem |
+@end |
+@interface BlacklistedFormContentItem : CollectionViewTextItem |
+@end |
+@implementation BlacklistedFormContentItem |
+@end |
+ |
+@interface SavePasswordsCollectionViewController ()< |
+ BooleanObserver, |
+ PasswordDetailsCollectionViewControllerDelegate, |
+ SuccessfulReauthTimeAccessor> { |
+ // The observable boolean that binds to the password manager setting state. |
+ // Saved passwords are only on if the password manager is enabled. |
+ base::scoped_nsobject<PrefBackedBoolean> passwordManagerEnabled_; |
+ // The item related to the switch for the password manager setting. |
+ base::scoped_nsobject<CollectionViewSwitchItem> savePasswordsItem_; |
+ // The interface for getting and manipulating a user's saved passwords. |
+ scoped_refptr<password_manager::PasswordStore> passwordStore_; |
+ // A helper object for passing data about saved passwords from a finished |
+ // password store request to the SavePasswordsCollectionViewController. |
+ std::unique_ptr<password_manager::SavePasswordsConsumer> |
+ savedPasswordsConsumer_; |
+ // A helper object for passing data about blacklisted sites from a finished |
+ // password store request to the SavePasswordsCollectionViewController. |
+ std::unique_ptr<password_manager::SavePasswordsConsumer> |
+ blacklistPasswordsConsumer_; |
+ // The list of the user's saved passwords. |
+ ScopedVector<autofill::PasswordForm> savedForms_; |
+ // The list of the user's blacklisted sites. |
+ ScopedVector<autofill::PasswordForm> blacklistedForms_; |
+ // Deletion of password being asynchronous, and the method taking a reference |
+ // to the PasswordForm, the PasswordForm must outlive the calls to |
+ // RemoveLogin. This vector will ensure this. |
+ ScopedVector<autofill::PasswordForm> deletedForms_; |
+ // The current Chrome browser state. |
+ ios::ChromeBrowserState* browserState_; |
+ // Object storing the time of the previous successful re-authentication. |
+ // This is meant to be used by the |ReauthenticationModule| for keeping |
+ // re-authentications valid for a certain time interval within the scope |
+ // of the Save Passwords Settings. |
+ base::scoped_nsobject<NSDate> successfulReauthTime_; |
+ // Module containing the reauthentication mechanism for viewing and copying |
+ // passwords. |
+ base::scoped_nsobject<ReauthenticationModule> reauthenticationModule_; |
+ |
+ base::mac::ObjCPropertyReleaser |
+ propertyReleaser_SavePasswordsCollectionViewController_; |
+} |
+// Kick off async request to get logins from password store. |
+- (void)getLoginsFromPasswordStore; |
+@end |
+ |
+@implementation SavePasswordsCollectionViewController |
+ |
+#pragma mark - Initialization |
+ |
+- (instancetype)initWithBrowserState:(ios::ChromeBrowserState*)browserState { |
+ DCHECK(browserState); |
+ self = [super initWithStyle:CollectionViewControllerStyleAppBar]; |
+ if (self) { |
+ browserState_ = browserState; |
+ reauthenticationModule_.reset([[ReauthenticationModule alloc] |
+ initWithSuccessfulReauthTimeAccessor:self]); |
+ int titleId = IDS_IOS_SAVE_PASSWORDS; |
+ syncer::SyncService* syncService = |
+ IOSChromeProfileSyncServiceFactory::GetForBrowserState(browserState_); |
+ if (password_bubble_experiment::IsSmartLockBrandingEnabled(syncService)) { |
+ titleId = IDS_IOS_SAVE_PASSWORDS_SMART_LOCK; |
+ } |
+ self.title = l10n_util::GetNSString(titleId); |
+ self.shouldHideDoneButton = YES; |
+ passwordStore_ = IOSChromePasswordStoreFactory::GetForBrowserState( |
+ browserState_, ServiceAccessType::EXPLICIT_ACCESS); |
+ DCHECK(passwordStore_); |
+ passwordManagerEnabled_.reset([[PrefBackedBoolean alloc] |
+ initWithPrefService:browserState_->GetPrefs() |
+ prefName:password_manager::prefs:: |
+ kPasswordManagerSavingEnabled]); |
+ [passwordManagerEnabled_ setObserver:self]; |
+ [self getLoginsFromPasswordStore]; |
+ [self updateEditButton]; |
+ [self loadModel]; |
+ |
+ propertyReleaser_SavePasswordsCollectionViewController_.Init( |
+ self, [SavePasswordsCollectionViewController class]); |
+ } |
+ return self; |
+} |
+ |
+- (void)dealloc { |
+ [passwordManagerEnabled_ setObserver:nil]; |
+ [super dealloc]; |
+} |
+ |
+#pragma mark - SettingsRootCollectionViewController |
+ |
+- (void)loadModel { |
+ [super loadModel]; |
+ CollectionViewModel* model = self.collectionViewModel; |
+ |
+ // Manage account message. |
+ CollectionViewItem* manageAccountLinkItem = [self manageAccountLinkItem]; |
+ if (manageAccountLinkItem) { |
+ [model addSectionWithIdentifier:SectionIdentifierMessage]; |
+ [model addItem:manageAccountLinkItem |
+ toSectionWithIdentifier:SectionIdentifierMessage]; |
+ } |
+ |
+ // Save passwords switch. |
+ [model addSectionWithIdentifier:SectionIdentifierSavePasswordsSwitch]; |
+ savePasswordsItem_.reset([[self savePasswordsItem] retain]); |
+ [model addItem:savePasswordsItem_ |
+ toSectionWithIdentifier:SectionIdentifierSavePasswordsSwitch]; |
+ |
+ // Saved passwords. |
+ if ([passwordManagerEnabled_ value]) { |
+ if (!savedForms_.empty()) { |
+ [model addSectionWithIdentifier:SectionIdentifierSavedPasswords]; |
+ CollectionViewTextItem* headerItem = [[[CollectionViewTextItem alloc] |
+ initWithType:ItemTypeHeader] autorelease]; |
+ headerItem.text = |
+ l10n_util::GetNSString(IDS_PASSWORD_MANAGER_SHOW_PASSWORDS_TAB_TITLE); |
+ [model setHeader:headerItem |
+ forSectionWithIdentifier:SectionIdentifierSavedPasswords]; |
+ for (size_t i = 0; i < savedForms_.size(); ++i) { |
+ autofill::PasswordForm* form = savedForms_[i]; |
+ [model addItem:[self savedFormItemWithForm:form] |
+ toSectionWithIdentifier:SectionIdentifierSavedPasswords]; |
+ } |
+ } |
+ if (!blacklistedForms_.empty()) { |
+ [model addSectionWithIdentifier:SectionIdentifierBlacklist]; |
+ CollectionViewTextItem* headerItem = [[[CollectionViewTextItem alloc] |
+ initWithType:ItemTypeHeader] autorelease]; |
+ headerItem.text = |
+ l10n_util::GetNSString(IDS_PASSWORD_MANAGER_EXCEPTIONS_TAB_TITLE); |
+ [model setHeader:headerItem |
+ forSectionWithIdentifier:SectionIdentifierBlacklist]; |
+ for (size_t i = 0; i < blacklistedForms_.size(); ++i) { |
+ autofill::PasswordForm* form = blacklistedForms_[i]; |
+ [model addItem:[self blacklistedFormItemWithForm:form] |
+ toSectionWithIdentifier:SectionIdentifierBlacklist]; |
+ } |
+ } |
+ } |
+} |
+ |
+#pragma mark - Items |
+ |
+- (CollectionViewItem*)manageAccountLinkItem { |
+ CollectionViewFooterItem* footerItem = [[[CollectionViewFooterItem alloc] |
+ initWithType:ItemTypeManageAccount] autorelease]; |
+ footerItem.text = |
+ l10n_util::GetNSString(IDS_IOS_SAVE_PASSWORDS_MANAGE_ACCOUNT); |
+ footerItem.linkURL = google_util::AppendGoogleLocaleParam( |
+ GURL(password_manager::kPasswordManagerAccountDashboardURL), |
+ GetApplicationContext()->GetApplicationLocale()); |
+ footerItem.linkDelegate = self; |
+ return footerItem; |
+} |
+ |
+- (CollectionViewSwitchItem*)savePasswordsItem { |
+ CollectionViewSwitchItem* savePasswordsItem = |
+ [[[CollectionViewSwitchItem alloc] |
+ initWithType:ItemTypeSavePasswordsSwitch] autorelease]; |
+ savePasswordsItem.text = l10n_util::GetNSString(IDS_IOS_SAVE_PASSWORDS); |
+ savePasswordsItem.on = [passwordManagerEnabled_ value]; |
+ return savePasswordsItem; |
+} |
+ |
+- (SavedFormContentItem*)savedFormItemWithForm:(autofill::PasswordForm*)form { |
+ SavedFormContentItem* passwordItem = [[[SavedFormContentItem alloc] |
+ initWithType:ItemTypeSavedPassword] autorelease]; |
+ passwordItem.text = |
+ base::SysUTF8ToNSString(GetHumanReadableOriginCopy(*form)); |
+ passwordItem.detailText = base::SysUTF16ToNSString(form->username_value); |
+ if (experimental_flags::IsViewCopyPasswordsEnabled()) { |
+ passwordItem.accessibilityTraits |= UIAccessibilityTraitButton; |
+ passwordItem.accessoryType = |
+ MDCCollectionViewCellAccessoryDisclosureIndicator; |
+ } |
+ return passwordItem; |
+} |
+ |
+- (BlacklistedFormContentItem*)blacklistedFormItemWithForm: |
+ (autofill::PasswordForm*)form { |
+ BlacklistedFormContentItem* passwordItem = |
+ [[[BlacklistedFormContentItem alloc] initWithType:ItemTypeBlacklisted] |
+ autorelease]; |
+ passwordItem.text = |
+ base::SysUTF8ToNSString(GetHumanReadableOriginCopy(*form)); |
+ return passwordItem; |
+} |
+ |
+#pragma mark - MDCCollectionViewEditingDelegate |
+ |
+- (BOOL)collectionViewAllowsEditing:(UICollectionView*)collectionView { |
+ return YES; |
+} |
+ |
+#pragma mark - MDCCollectionViewStylingDelegate |
+ |
+- (MDCCollectionViewCellStyle)collectionView:(UICollectionView*)collectionView |
+ cellStyleForSection:(NSInteger)section { |
+ NSInteger sectionIdentifier = |
+ [self.collectionViewModel sectionIdentifierForSection:section]; |
+ switch (sectionIdentifier) { |
+ case SectionIdentifierMessage: |
+ // Display the message in the default style with no "card" UI and no |
+ // section padding. |
+ return MDCCollectionViewCellStyleDefault; |
+ default: |
+ return self.styler.cellStyle; |
+ } |
+} |
+ |
+- (BOOL)collectionView:(UICollectionView*)collectionView |
+ shouldHideItemBackgroundAtIndexPath:(NSIndexPath*)indexPath { |
+ NSInteger sectionIdentifier = |
+ [self.collectionViewModel sectionIdentifierForSection:indexPath.section]; |
+ switch (sectionIdentifier) { |
+ case SectionIdentifierMessage: |
+ // Display the message without any background image or shadowing. |
+ return YES; |
+ default: |
+ return NO; |
+ } |
+} |
+ |
+- (CGFloat)collectionView:(UICollectionView*)collectionView |
+ cellHeightAtIndexPath:(NSIndexPath*)indexPath { |
+ CollectionViewItem* item = |
+ [self.collectionViewModel itemAtIndexPath:indexPath]; |
+ switch (item.type) { |
+ case ItemTypeManageAccount: |
+ return [MDCCollectionViewCell |
+ cr_preferredHeightForWidth:CGRectGetWidth(collectionView.bounds) |
+ forItem:item]; |
+ case ItemTypeSavedPassword: |
+ return MDCCellDefaultTwoLineHeight; |
+ default: |
+ return MDCCellDefaultOneLineHeight; |
+ } |
+} |
+ |
+- (BOOL)collectionView:(UICollectionView*)collectionView |
+ hidesInkViewAtIndexPath:(NSIndexPath*)indexPath { |
+ NSInteger type = [self.collectionViewModel itemTypeForIndexPath:indexPath]; |
+ switch (type) { |
+ case ItemTypeSavePasswordsSwitch: |
+ return YES; |
+ default: |
+ return NO; |
+ } |
+} |
+ |
+#pragma mark - UICollectionViewDataSource |
+ |
+- (UICollectionViewCell*)collectionView:(UICollectionView*)collectionView |
+ cellForItemAtIndexPath:(NSIndexPath*)indexPath { |
+ UICollectionViewCell* cell = |
+ [super collectionView:collectionView cellForItemAtIndexPath:indexPath]; |
+ |
+ if ([self.collectionViewModel itemTypeForIndexPath:indexPath] == |
+ ItemTypeSavePasswordsSwitch) { |
+ CollectionViewSwitchCell* switchCell = |
+ base::mac::ObjCCastStrict<CollectionViewSwitchCell>(cell); |
+ [switchCell.switchView addTarget:self |
+ action:@selector(savePasswordsSwitchChanged:) |
+ forControlEvents:UIControlEventValueChanged]; |
+ } |
+ return cell; |
+} |
+ |
+- (UICollectionReusableView*)collectionView:(UICollectionView*)collectionView |
+ viewForSupplementaryElementOfKind:(NSString*)kind |
+ atIndexPath:(NSIndexPath*)indexPath { |
+ UICollectionReusableView* view = [super collectionView:collectionView |
+ viewForSupplementaryElementOfKind:kind |
+ atIndexPath:indexPath]; |
+ MDCCollectionViewTextCell* textCell = |
+ base::mac::ObjCCast<MDCCollectionViewTextCell>(view); |
+ if (textCell) { |
+ textCell.textLabel.textColor = [[MDCPalette greyPalette] tint500]; |
+ } |
+ return view; |
+}; |
+ |
+#pragma mark - BooleanObserver |
+ |
+- (void)booleanDidChange:(id<ObservableBoolean>)observableBoolean { |
+ DCHECK_EQ(observableBoolean, passwordManagerEnabled_.get()); |
+ |
+ // Update the item. |
+ savePasswordsItem_.get().on = [passwordManagerEnabled_ value]; |
+ |
+ // Update the cell. |
+ [self reconfigureCellsForItems:@[ savePasswordsItem_ ] |
+ inSectionWithIdentifier:SectionIdentifierSavePasswordsSwitch]; |
+ |
+ // Update the rest of the UI. |
+ [self.editor setEditing:NO]; |
+ [self updateEditButton]; |
+ [self reloadData]; |
+} |
+ |
+#pragma mark - Actions |
+ |
+- (void)savePasswordsSwitchChanged:(UISwitch*)switchView { |
+ // Update the setting. |
+ [passwordManagerEnabled_ setValue:switchView.on]; |
+ |
+ // Update the item. |
+ savePasswordsItem_.get().on = [passwordManagerEnabled_ value]; |
+ |
+ // Update the rest of the UI. |
+ [self.editor setEditing:NO]; |
+ [self updateEditButton]; |
+ [self reloadData]; |
+} |
+ |
+#pragma mark - Private methods |
+ |
+- (void)getLoginsFromPasswordStore { |
+ savedPasswordsConsumer_.reset( |
+ new password_manager::SavePasswordsConsumer(self)); |
+ passwordStore_->GetAutofillableLogins(savedPasswordsConsumer_.get()); |
+ blacklistPasswordsConsumer_.reset( |
+ new password_manager::SavePasswordsConsumer(self)); |
+ passwordStore_->GetBlacklistLogins(blacklistPasswordsConsumer_.get()); |
+} |
+ |
+- (void)onGetPasswordStoreResults: |
+ (const std::vector<std::unique_ptr<autofill::PasswordForm>>&)result { |
+ for (auto it = result.begin(); it != result.end(); ++it) { |
+ // PasswordForm is needed when user wants to delete the site/password. |
+ autofill::PasswordForm* form = new autofill::PasswordForm(**it); |
+ if (form->blacklisted_by_user) |
+ blacklistedForms_.push_back(form); |
+ else |
+ savedForms_.push_back(form); |
+ } |
+ |
+ [self updateEditButton]; |
+ [self reloadData]; |
+} |
+ |
+- (BOOL)shouldShowEditButton { |
+ return [passwordManagerEnabled_ value]; |
+} |
+ |
+- (BOOL)editButtonEnabled { |
+ DCHECK([self shouldShowEditButton]); |
+ return !savedForms_.empty() || !blacklistedForms_.empty(); |
+} |
+ |
+#pragma mark UICollectionViewDelegate |
+ |
+- (void)collectionView:(UICollectionView*)collectionView |
+ didSelectItemAtIndexPath:(NSIndexPath*)indexPath { |
+ [super collectionView:collectionView didSelectItemAtIndexPath:indexPath]; |
+ |
+ // Actions should only take effect when not in editing mode. |
+ if (self.editing) { |
+ return; |
+ } |
+ |
+ CollectionViewModel* model = self.collectionViewModel; |
+ if ([model itemTypeForIndexPath:indexPath] == ItemTypeSavedPassword) { |
+ DCHECK_EQ([model sectionIdentifierForSection:indexPath.section], |
+ SectionIdentifierSavedPasswords); |
+ if (experimental_flags::IsViewCopyPasswordsEnabled()) { |
+ DCHECK_LT(base::checked_cast<size_t>(indexPath.item), savedForms_.size()); |
+ autofill::PasswordForm* form = savedForms_[indexPath.item]; |
+ NSString* username = base::SysUTF16ToNSString(form->username_value); |
+ NSString* password = base::SysUTF16ToNSString(form->password_value); |
+ NSString* origin = |
+ base::SysUTF8ToNSString(GetHumanReadableOriginCopy(*form)); |
+ base::scoped_nsobject<UIViewController> controller( |
+ [[PasswordDetailsCollectionViewController alloc] |
+ initWithPasswordForm:*form |
+ delegate:self |
+ reauthenticationModule:reauthenticationModule_ |
+ username:username |
+ password:password |
+ origin:origin]); |
+ [self.navigationController pushViewController:controller animated:YES]; |
+ } |
+ } |
+} |
+ |
+#pragma mark MDCCollectionViewEditingDelegate |
+ |
+- (BOOL)collectionView:(UICollectionView*)collectionView |
+ canEditItemAtIndexPath:(NSIndexPath*)indexPath { |
+ // Only password cells are editable. |
+ CollectionViewItem* item = |
+ [self.collectionViewModel itemAtIndexPath:indexPath]; |
+ return [item isKindOfClass:[SavedFormContentItem class]] || |
+ [item isKindOfClass:[BlacklistedFormContentItem class]]; |
+} |
+ |
+- (void)collectionView:(UICollectionView*)collectionView |
+ willDeleteItemsAtIndexPaths:(NSArray*)indexPaths { |
+ // Ensure indexPaths are sorted to maintain delete logic, and keep track of |
+ // number of items deleted to adjust index for accessing elements in the |
+ // forms vectors. |
+ NSArray* sortedIndexPaths = |
+ [indexPaths sortedArrayUsingSelector:@selector(compare:)]; |
+ int passwordsDeleted = 0; |
+ int blacklistedDeleted = 0; |
+ for (NSIndexPath* indexPath in sortedIndexPaths) { |
+ // Only form items are editable. |
+ CollectionViewTextItem* item = |
+ base::mac::ObjCCastStrict<CollectionViewTextItem>( |
+ [self.collectionViewModel itemAtIndexPath:indexPath]); |
+ BOOL blacklisted = [item isKindOfClass:[BlacklistedFormContentItem class]]; |
+ unsigned int formIndex = (unsigned int)indexPath.item; |
+ // Adjust index to account for deleted items. |
+ formIndex -= blacklisted ? blacklistedDeleted : passwordsDeleted; |
+ ScopedVector<autofill::PasswordForm>& forms = |
+ blacklisted ? blacklistedForms_ : savedForms_; |
+ DCHECK_LT(formIndex, forms.size()); |
+ auto formIterator = forms.begin() + formIndex; |
+ passwordStore_->RemoveLogin(**formIterator); |
+ deletedForms_.push_back(*formIterator); |
+ forms.weak_erase(formIterator); |
+ if (blacklisted) { |
+ ++blacklistedDeleted; |
+ } else { |
+ ++passwordsDeleted; |
+ } |
+ } |
+ |
+ // Must call super at the end of the child implementation. |
+ [super collectionView:collectionView willDeleteItemsAtIndexPaths:indexPaths]; |
+} |
+ |
+- (void)collectionView:(UICollectionView*)collectionView |
+ didDeleteItemsAtIndexPaths:(NSArray*)indexPaths { |
+ // Remove empty sections. |
+ // TODO(crbug.com/593786): Move this logic in CollectionViewController. |
+ NSMutableOrderedSet* sectionsToRemove = [NSMutableOrderedSet orderedSet]; |
+ // Sort and enumerate in reverse order to delete the items from the collection |
+ // view model. |
+ NSArray* sortedIndexPaths = |
+ [indexPaths sortedArrayUsingSelector:@selector(compare:)]; |
+ for (NSIndexPath* indexPath in [sortedIndexPaths reverseObjectEnumerator]) { |
+ if ([collectionView numberOfItemsInSection:indexPath.section] == 0) { |
+ [sectionsToRemove addObject:@(indexPath.section)]; |
+ } |
+ } |
+ base::WeakNSObject<SavePasswordsCollectionViewController> weakSelf(self); |
+ [self.collectionView performBatchUpdates:^{ |
+ base::scoped_nsobject<SavePasswordsCollectionViewController> strongSelf( |
+ [weakSelf retain]); |
+ if (!strongSelf) |
+ return; |
+ for (NSNumber* sectionNumber in sectionsToRemove) { |
+ NSInteger section = [sectionNumber integerValue]; |
+ NSInteger sectionIdentifier = [[strongSelf collectionViewModel] |
+ sectionIdentifierForSection:section]; |
+ [[strongSelf collectionViewModel] |
+ removeSectionWithIdentifier:sectionIdentifier]; |
+ [[strongSelf collectionView] |
+ deleteSections:[NSIndexSet indexSetWithIndex:section]]; |
+ } |
+ } |
+ completion:^(BOOL finished) { |
+ base::scoped_nsobject<SavePasswordsCollectionViewController> strongSelf( |
+ [weakSelf retain]); |
+ if (!strongSelf) |
+ return; |
+ if (![strongSelf editButtonEnabled]) { |
+ [strongSelf setEditing:NO]; |
+ } |
+ [strongSelf updateEditButton]; |
+ }]; |
+} |
+ |
+#pragma mark PasswordDetailsCollectionViewControllerDelegate |
+ |
+- (void)deletePassword:(const autofill::PasswordForm&)form { |
+ passwordStore_->RemoveLogin(form); |
+ for (auto it = savedForms_.begin(); it != savedForms_.end(); ++it) { |
+ if (**it == form) { |
+ savedForms_.weak_erase(it); |
+ return; |
+ } |
+ } |
+ [self.navigationController popViewControllerAnimated:YES]; |
+} |
+ |
+#pragma mark SuccessfulReauthTimeAccessor |
+ |
+- (void)updateSuccessfulReauthTime { |
+ successfulReauthTime_.reset([[NSDate alloc] init]); |
+} |
+ |
+- (NSDate*)lastSuccessfulReauthTime { |
+ return successfulReauthTime_.get(); |
+} |
+ |
+@end |