[go: nahoru, domu]

blob: f20668e3dc2fccbca2ae736b9d388838670e2112 [file] [log] [blame]
// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "ios/chrome/credential_provider_extension/ui/credential_list_view_controller.h"
#import "base/apple/foundation_util.h"
#import "base/numerics/safe_conversions.h"
#import "ios/chrome/common/app_group/app_group_constants.h"
#import "ios/chrome/common/app_group/app_group_metrics.h"
#import "ios/chrome/common/credential_provider/credential.h"
#import "ios/chrome/common/ui/colors/semantic_color_names.h"
#import "ios/chrome/common/ui/elements/highlight_button.h"
#import "ios/chrome/common/ui/favicon/favicon_attributes.h"
#import "ios/chrome/common/ui/favicon/favicon_view.h"
#import "ios/chrome/common/ui/table_view/favicon_table_view_cell.h"
#import "ios/chrome/common/ui/util/pointer_interaction_util.h"
#import "ios/chrome/credential_provider_extension/metrics_util.h"
#import "ios/chrome/credential_provider_extension/ui/credential_list_global_header_view.h"
#import "ios/chrome/credential_provider_extension/ui/credential_list_header_view.h"
namespace {
// Reuse Identifiers for table views.
NSString* kHeaderIdentifier = @"clvcHeader";
NSString* kCredentialCellIdentifier = @"clvcCredentialCell";
NSString* kNewPasswordCellIdentifier = @"clvcNewPasswordCell";
const CGFloat kNewCredentialHeaderHeight = 35;
// Add extra space to offset the top of the table view from the search bar.
const CGFloat kTableViewTopSpace = 8;
UIColor* BackgroundColor() {
return [UIColor colorNamed:kGroupedPrimaryBackgroundColor];
}
}
// This cell just adds a simple hover pointer interaction to the TableViewCell.
@interface CredentialListCell : FaviconTableViewCell
@end
@implementation CredentialListCell
- (instancetype)initWithStyle:(UITableViewCellStyle)style
reuseIdentifier:(NSString*)reuseIdentifier {
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
if (self) {
[self addInteraction:[[ViewPointerInteraction alloc] init]];
}
return self;
}
@end
@interface CredentialListViewController () <UITableViewDataSource,
UISearchResultsUpdating>
// Search controller that contains search bar.
@property(nonatomic, strong) UISearchController* searchController;
// Current list of suggested passwords.
@property(nonatomic, copy) NSArray<id<Credential>>* suggestedPasswords;
// Current list of all passwords.
@property(nonatomic, copy) NSArray<id<Credential>>* allPasswords;
// Indicates if the option to create a new password should be presented.
@property(nonatomic, assign) BOOL showNewPasswordOption;
// FaviconAttributes object with the default world icon as fallback.
@property(nonatomic, strong) FaviconAttributes* defaultWorldIconAttributes;
@end
@implementation CredentialListViewController
@synthesize delegate;
- (instancetype)init {
UITableViewStyle style = UITableViewStyleInsetGrouped;
self = [super initWithStyle:style];
return self;
}
- (void)viewDidLoad {
[super viewDidLoad];
self.title = NSLocalizedString(
@"IDS_IOS_CREDENTIAL_PROVIDER_CREDENTIAL_LIST_BRANDED_TITLE",
@"Google Password Manager");
self.view.backgroundColor = BackgroundColor();
self.navigationItem.leftBarButtonItem = [self navigationCancelButton];
self.searchController =
[[UISearchController alloc] initWithSearchResultsController:nil];
self.searchController.searchResultsUpdater = self;
self.searchController.obscuresBackgroundDuringPresentation = NO;
self.searchController.searchBar.barTintColor = BackgroundColor();
// Add en empty space at the bottom of the list, the size of the search bar,
// to allow scrolling up enough to see last result, otherwise it remains
// hidden under the accessories.
self.tableView.tableFooterView =
[[UIView alloc] initWithFrame:self.searchController.searchBar.frame];
self.tableView.contentInset = UIEdgeInsetsMake(kTableViewTopSpace, 0, 0, 0);
self.navigationItem.searchController = self.searchController;
self.navigationItem.hidesSearchBarWhenScrolling = NO;
UINavigationBarAppearance* appearance =
[[UINavigationBarAppearance alloc] init];
[appearance configureWithDefaultBackground];
appearance.backgroundColor = BackgroundColor();
if (@available(iOS 15, *)) {
self.navigationItem.scrollEdgeAppearance = appearance;
} else {
// On iOS 14, scrollEdgeAppearance only affects navigation bars with large
// titles, so it can't be used. Instead, the navigation bar will always be
// the same style.
self.navigationItem.standardAppearance = appearance;
}
self.navigationController.navigationBar.tintColor =
[UIColor colorNamed:kBlueColor];
// Presentation of searchController will walk up the view controller hierarchy
// until it finds the root view controller or one that defines a presentation
// context. Make this class the presentation context so that the search
// controller does not present on top of the navigation controller.
self.definesPresentationContext = YES;
[self.tableView registerClass:[UITableViewHeaderFooterView class]
forHeaderFooterViewReuseIdentifier:kHeaderIdentifier];
[self.tableView registerClass:[CredentialListHeaderView class]
forHeaderFooterViewReuseIdentifier:CredentialListHeaderView.reuseID];
[self.tableView registerClass:[CredentialListGlobalHeaderView class]
forHeaderFooterViewReuseIdentifier:CredentialListGlobalHeaderView
.reuseID];
}
#pragma mark - CredentialListConsumer
- (void)presentSuggestedPasswords:(NSArray<id<Credential>>*)suggested
allPasswords:(NSArray<id<Credential>>*)all
showSearchBar:(BOOL)showSearchBar
showNewPasswordOption:(BOOL)showNewPasswordOption {
self.suggestedPasswords = suggested;
self.allPasswords = all;
self.showNewPasswordOption = showNewPasswordOption;
[self.tableView reloadData];
[self.tableView layoutIfNeeded];
// Remove or add the search controller depending on whether there are
// passwords to search.
if (showSearchBar) {
self.navigationItem.searchController = self.searchController;
} else {
if (self.navigationItem.searchController.isActive) {
self.navigationItem.searchController.active = NO;
}
self.navigationItem.searchController = nil;
}
}
- (void)setTopPrompt:(NSString*)prompt {
self.navigationController.navigationBar.topItem.prompt = prompt;
}
#pragma mark - UITableViewDataSource
- (NSInteger)numberOfSectionsInTableView:(UITableView*)tableView {
return [self numberOfSections];
}
- (NSInteger)tableView:(UITableView*)tableView
numberOfRowsInSection:(NSInteger)section {
if ([self isEmptyTable]) {
return 0;
} else if ([self isSuggestedPasswordSection:section]) {
return [self numberOfRowsInSuggestedPasswordSection];
} else {
return self.allPasswords.count;
}
}
- (UITableViewCell*)tableView:(UITableView*)tableView
cellForRowAtIndexPath:(NSIndexPath*)indexPath {
if ([self isIndexPathNewPasswordRow:indexPath]) {
UITableViewCell* cell = [tableView
dequeueReusableCellWithIdentifier:kNewPasswordCellIdentifier];
if (!cell) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle
reuseIdentifier:kNewPasswordCellIdentifier];
}
cell.backgroundColor = [UIColor colorNamed:kBackgroundColor];
cell.textLabel.text =
NSLocalizedString(@"IDS_IOS_CREDENTIAL_PROVIDER_CREATE_PASSWORD_ROW",
@"Add New Password");
cell.textLabel.textColor = [UIColor colorNamed:kBlueColor];
return cell;
}
id<Credential> credential = [self credentialForIndexPath:indexPath];
UITableViewCell* cell =
[tableView dequeueReusableCellWithIdentifier:kCredentialCellIdentifier];
if (!cell) {
cell =
[[CredentialListCell alloc] initWithStyle:UITableViewCellStyleDefault
reuseIdentifier:kCredentialCellIdentifier];
cell.accessoryView = [self infoIconButton];
}
CredentialListCell* credentialCell =
base::apple::ObjCCastStrict<CredentialListCell>(cell);
credentialCell.textLabel.text = credential.serviceName;
credentialCell.detailTextLabel.text = credential.user;
credentialCell.uniqueIdentifier = credential.serviceIdentifier;
credentialCell.selectionStyle = UITableViewCellSelectionStyleDefault;
credentialCell.backgroundColor = [UIColor colorNamed:kBackgroundColor];
credentialCell.accessibilityTraits |= UIAccessibilityTraitButton;
// Load favicon.
if (credential.favicon) {
// Load the favicon from disk.
[self loadFaviconAtIndexPath:indexPath forCell:cell];
}
// Use the default world icon as fallback.
if (!self.defaultWorldIconAttributes) {
self.defaultWorldIconAttributes = [FaviconAttributes
attributesWithImage:
[[UIImage imageNamed:@"default_world_favicon"]
imageWithTintColor:[UIColor colorNamed:kTextQuaternaryColor]
renderingMode:UIImageRenderingModeAlwaysOriginal]];
}
[credentialCell.faviconView
configureWithAttributes:self.defaultWorldIconAttributes];
return credentialCell;
}
// Asynchronously loads favicon for given index path. The loads are cancelled
// upon cell reuse automatically.
- (void)loadFaviconAtIndexPath:(NSIndexPath*)indexPath
forCell:(UITableViewCell*)cell {
id<Credential> credential = [self credentialForIndexPath:indexPath];
DCHECK(credential);
DCHECK(cell);
CredentialListCell* credentialCell =
base::apple::ObjCCastStrict<CredentialListCell>(cell);
NSString* serviceIdentifier = credential.serviceIdentifier;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
NSURL* filePath = [app_group::SharedFaviconAttributesFolder()
URLByAppendingPathComponent:credential.favicon
isDirectory:NO];
NSError* error = nil;
NSData* data = [NSData dataWithContentsOfURL:filePath
options:0
error:&error];
if (data && !error) {
NSKeyedUnarchiver* unarchiver =
[[NSKeyedUnarchiver alloc] initForReadingFromData:data error:nil];
unarchiver.requiresSecureCoding = NO;
FaviconAttributes* attributes =
[unarchiver decodeObjectForKey:NSKeyedArchiveRootObjectKey];
// Only set favicon if the cell hasn't been reused.
if ([credentialCell.uniqueIdentifier isEqualToString:serviceIdentifier]) {
// Update the UI on the main thread.
dispatch_async(dispatch_get_main_queue(), ^{
if (attributes) {
[credentialCell.faviconView configureWithAttributes:attributes];
}
});
}
}
});
}
#pragma mark - UITableViewDelegate
- (UIView*)tableView:(UITableView*)tableView
viewForHeaderInSection:(NSInteger)section {
if ([self isGlobalHeaderSection:section]) {
return [self.tableView dequeueReusableHeaderFooterViewWithIdentifier:
CredentialListGlobalHeaderView.reuseID];
}
CredentialListHeaderView* view = [self.tableView
dequeueReusableHeaderFooterViewWithIdentifier:CredentialListHeaderView
.reuseID];
view.headerTextLabel.text = [self titleForHeaderInSection:section];
view.contentView.backgroundColor = BackgroundColor();
return view;
}
- (CGFloat)tableView:(UITableView*)tableView
heightForHeaderInSection:(NSInteger)section {
if ([self isGlobalHeaderSection:section]) {
return UITableViewAutomaticDimension;
}
if ([self isSuggestedPasswordSection:section]) {
return 0;
}
return kNewCredentialHeaderHeight;
}
- (void)tableView:(UITableView*)tableView
didSelectRowAtIndexPath:(NSIndexPath*)indexPath {
if ([self isIndexPathNewPasswordRow:indexPath]) {
[self.delegate newPasswordWasSelected];
[tableView deselectRowAtIndexPath:indexPath animated:YES];
return;
}
UpdateUMACountForKey(app_group::kCredentialExtensionPasswordUseCount);
id<Credential> credential = [self credentialForIndexPath:indexPath];
if (!credential) {
return;
}
[self.delegate userSelectedCredential:credential];
}
#pragma mark - UISearchResultsUpdating
- (void)updateSearchResultsForSearchController:
(UISearchController*)searchController {
if (searchController.searchBar.text.length) {
UpdateUMACountForKey(app_group::kCredentialExtensionSearchCount);
}
[self.delegate updateResultsWithFilter:searchController.searchBar.text];
}
#pragma mark - Private
// Creates a cancel button for the navigation item.
- (UIBarButtonItem*)navigationCancelButton {
UIBarButtonItem* cancelButton = [[UIBarButtonItem alloc]
initWithBarButtonSystemItem:UIBarButtonSystemItemCancel
target:self.delegate
action:@selector(navigationCancelButtonWasPressed:)];
cancelButton.tintColor = [UIColor colorNamed:kBlueColor];
return cancelButton;
}
// Creates a button to be displayed as accessory of the password row item.
- (UIView*)infoIconButton {
UIImage* image = [UIImage imageNamed:@"info_icon"];
image = [image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
HighlightButton* button = [HighlightButton buttonWithType:UIButtonTypeCustom];
button.frame = CGRectMake(0.0, 0.0, image.size.width, image.size.height);
[button setBackgroundImage:image forState:UIControlStateNormal];
[button setTintColor:[UIColor colorNamed:kBlueColor]];
[button addTarget:self
action:@selector(infoIconButtonTapped:event:)
forControlEvents:UIControlEventTouchUpInside];
button.accessibilityLabel = NSLocalizedString(
@"IDS_IOS_CREDENTIAL_PROVIDER_SHOW_DETAILS_ACCESSIBILITY_LABEL",
@"Show Details.");
button.pointerInteractionEnabled = YES;
button.pointerStyleProvider = ^UIPointerStyle*(
UIButton* theButton, __unused UIPointerEffect* proposedEffect,
__unused UIPointerShape* proposedShape) {
UITargetedPreview* preview =
[[UITargetedPreview alloc] initWithView:theButton];
UIPointerHighlightEffect* effect =
[UIPointerHighlightEffect effectWithPreview:preview];
UIPointerShape* shape =
[UIPointerShape shapeWithRoundedRect:theButton.frame
cornerRadius:theButton.frame.size.width / 2];
return [UIPointerStyle styleWithEffect:effect shape:shape];
};
return button;
}
// Called when info icon is tapped.
- (void)infoIconButtonTapped:(id)sender event:(id)event {
CGPoint hitPoint = [base::apple::ObjCCastStrict<UIButton>(sender)
convertPoint:CGPointZero
toView:self.tableView];
NSIndexPath* indexPath = [self.tableView indexPathForRowAtPoint:hitPoint];
id<Credential> credential = [self credentialForIndexPath:indexPath];
if (!credential) {
return;
}
[self.delegate showDetailsForCredential:credential];
}
// Returns number of sections to display based on `suggestedPasswords` and
// `allPasswords`. If no sections with data, returns 1 for the 'no data' banner.
- (int)numberOfSections {
if ([self numberOfRowsInSuggestedPasswordSection] == 0 ||
[self.allPasswords count] == 0) {
return 1;
}
return 2;
}
// Returns YES if there is no data to display.
- (BOOL)isEmptyTable {
return [self numberOfRowsInSuggestedPasswordSection] == 0 &&
[self.allPasswords count] == 0;
}
// Returns YES if given section is for suggested passwords.
- (BOOL)isSuggestedPasswordSection:(int)section {
int sections = [self numberOfSections];
if ((sections == 2 && section == 0) ||
(sections == 1 && [self numberOfRowsInSuggestedPasswordSection])) {
return YES;
} else {
return NO;
}
}
// Returns YES if given section is for global header.
- (BOOL)isGlobalHeaderSection:(int)section {
return section == 0 && ![self isEmptyTable];
}
// Returns the credential at the passed index.
- (id<Credential>)credentialForIndexPath:(NSIndexPath*)indexPath {
if ([self isSuggestedPasswordSection:indexPath.section]) {
if (indexPath.row >=
base::checked_cast<NSInteger>(self.suggestedPasswords.count)) {
return nil;
}
return self.suggestedPasswords[indexPath.row];
} else {
if (indexPath.row >=
base::checked_cast<NSInteger>(self.allPasswords.count)) {
return nil;
}
return self.allPasswords[indexPath.row];
}
}
// Returns true if the passed index corresponds to the Create New Password Cell.
- (BOOL)isIndexPathNewPasswordRow:(NSIndexPath*)indexPath {
if ([self isSuggestedPasswordSection:indexPath.section]) {
return indexPath.row == NSInteger(self.suggestedPasswords.count);
}
return NO;
}
// Returns the number of rows in suggested passwords section.
- (NSUInteger)numberOfRowsInSuggestedPasswordSection {
return [self.suggestedPasswords count] + (self.showNewPasswordOption ? 1 : 0);
}
// Returns the title of the given section
- (NSString*)titleForHeaderInSection:(NSInteger)section {
if ([self isEmptyTable]) {
return NSLocalizedString(@"IDS_IOS_CREDENTIAL_PROVIDER_NO_SEARCH_RESULTS",
@"No search results found");
} else if ([self isSuggestedPasswordSection:section]) {
return nil;
} else {
return NSLocalizedString(@"IDS_IOS_CREDENTIAL_PROVIDER_ALL_PASSWORDS",
@"All Passwords");
}
}
@end