1// Copyright 2020 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5#import "ios/chrome/credential_provider_extension/ui/credential_list_view_controller.h" 6 7#include "base/mac/foundation_util.h" 8#include "ios/chrome/common/app_group/app_group_metrics.h" 9#import "ios/chrome/common/credential_provider/credential.h" 10#import "ios/chrome/common/ui/colors/semantic_color_names.h" 11#import "ios/chrome/common/ui/util/pointer_interaction_util.h" 12#import "ios/chrome/credential_provider_extension/metrics_util.h" 13 14#if !defined(__has_feature) || !__has_feature(objc_arc) 15#error "This file requires ARC support." 16#endif 17 18namespace { 19 20NSString* kHeaderIdentifier = @"clvcHeader"; 21NSString* kCellIdentifier = @"clvcCell"; 22 23const CGFloat kHeaderHeight = 70; 24} 25 26#if defined(__IPHONE_13_4) 27// This cell just adds a simple hover pointer interaction to the TableViewCell. 28@interface CredentialListCell : UITableViewCell 29@end 30 31@implementation CredentialListCell 32 33- (instancetype)initWithStyle:(UITableViewCellStyle)style 34 reuseIdentifier:(NSString*)reuseIdentifier { 35 self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]; 36 if (self) { 37 if (@available(iOS 13.4, *)) { 38 [self addInteraction:[[ViewPointerInteraction alloc] init]]; 39 } 40 } 41 return self; 42} 43 44@end 45#endif // defined(__IPHONE_13_4) 46 47@interface CredentialListViewController () <UITableViewDataSource, 48 UISearchResultsUpdating> 49 50// Search controller that contains search bar. 51@property(nonatomic, strong) UISearchController* searchController; 52 53// Current list of suggested passwords. 54@property(nonatomic, copy) NSArray<id<Credential>>* suggestedPasswords; 55 56// Current list of all passwords. 57@property(nonatomic, copy) NSArray<id<Credential>>* allPasswords; 58 59@end 60 61@implementation CredentialListViewController 62 63@synthesize delegate; 64 65- (void)viewDidLoad { 66 [super viewDidLoad]; 67 self.title = 68 NSLocalizedString(@"IDS_IOS_CREDENTIAL_PROVIDER_CREDENTIAL_LIST_TITLE", 69 @"AutoFill Chrome Password"); 70 self.view.backgroundColor = [UIColor colorNamed:kBackgroundColor]; 71 self.navigationItem.rightBarButtonItem = [self navigationCancelButton]; 72 73 self.searchController = 74 [[UISearchController alloc] initWithSearchResultsController:nil]; 75 self.searchController.searchResultsUpdater = self; 76 self.searchController.obscuresBackgroundDuringPresentation = NO; 77 self.searchController.searchBar.barTintColor = 78 [UIColor colorNamed:kBackgroundColor]; 79 // Add en empty space at the bottom of the list, the size of the search bar, 80 // to allow scrolling up enough to see last result, otherwise it remains 81 // hidden under the accessories. 82 self.tableView.tableFooterView = 83 [[UIView alloc] initWithFrame:self.searchController.searchBar.frame]; 84 self.navigationItem.searchController = self.searchController; 85 self.navigationItem.hidesSearchBarWhenScrolling = NO; 86 87 self.navigationController.navigationBar.barTintColor = 88 [UIColor colorNamed:kBackgroundColor]; 89 self.navigationController.navigationBar.tintColor = 90 [UIColor colorNamed:kBlueColor]; 91 self.navigationController.navigationBar.shadowImage = [[UIImage alloc] init]; 92 93 // Presentation of searchController will walk up the view controller hierarchy 94 // until it finds the root view controller or one that defines a presentation 95 // context. Make this class the presentation context so that the search 96 // controller does not present on top of the navigation controller. 97 self.definesPresentationContext = YES; 98 [self.tableView registerClass:[UITableViewHeaderFooterView class] 99 forHeaderFooterViewReuseIdentifier:kHeaderIdentifier]; 100} 101 102#pragma mark - CredentialListConsumer 103 104- (void)presentSuggestedPasswords:(NSArray<id<Credential>>*)suggested 105 allPasswords:(NSArray<id<Credential>>*)all { 106 self.suggestedPasswords = suggested; 107 self.allPasswords = all; 108 [self.tableView reloadData]; 109 [self.tableView layoutIfNeeded]; 110} 111 112#pragma mark - UITableViewDataSource 113 114- (NSInteger)numberOfSectionsInTableView:(UITableView*)tableView { 115 return [self numberOfSections]; 116} 117 118- (NSInteger)tableView:(UITableView*)tableView 119 numberOfRowsInSection:(NSInteger)section { 120 if ([self isEmptyTable]) { 121 return 0; 122 } else if ([self isSuggestedPasswordSection:section]) { 123 return self.suggestedPasswords.count; 124 } else { 125 return self.allPasswords.count; 126 } 127} 128 129- (NSString*)tableView:(UITableView*)tableView 130 titleForHeaderInSection:(NSInteger)section { 131 if ([self isEmptyTable]) { 132 return NSLocalizedString(@"IDS_IOS_CREDENTIAL_PROVIDER_NO_SEARCH_RESULTS", 133 @"No search results found"); 134 } else if ([self isSuggestedPasswordSection:section]) { 135 return NSLocalizedString(@"IDS_IOS_CREDENTIAL_PROVIDER_SUGGESTED_PASSWORDS", 136 @"Suggested Passwords"); 137 } else { 138 return NSLocalizedString(@"IDS_IOS_CREDENTIAL_PROVIDER_ALL_PASSWORDS", 139 @"All Passwords"); 140 } 141} 142 143- (UITableViewCell*)tableView:(UITableView*)tableView 144 cellForRowAtIndexPath:(NSIndexPath*)indexPath { 145 UITableViewCell* cell = 146 [tableView dequeueReusableCellWithIdentifier:kCellIdentifier]; 147 if (!cell) { 148#if defined(__IPHONE_13_4) 149 cell = 150 [[CredentialListCell alloc] initWithStyle:UITableViewCellStyleSubtitle 151 reuseIdentifier:kCellIdentifier]; 152#else 153 cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle 154 reuseIdentifier:kCellIdentifier]; 155#endif // defined(__IPHONE_13_4) 156 cell.accessoryView = [self infoIconButton]; 157 } 158 159 id<Credential> credential = [self credentialForIndexPath:indexPath]; 160 cell.textLabel.text = credential.serviceName; 161 cell.textLabel.textColor = [UIColor colorNamed:kTextPrimaryColor]; 162 cell.detailTextLabel.text = credential.user; 163 cell.detailTextLabel.textColor = [UIColor colorNamed:kTextSecondaryColor]; 164 cell.selectionStyle = UITableViewCellSelectionStyleDefault; 165 cell.backgroundColor = [UIColor colorNamed:kBackgroundColor]; 166 cell.accessibilityTraits |= UIAccessibilityTraitButton; 167 168 return cell; 169} 170 171#pragma mark - UITableViewDelegate 172 173- (UIView*)tableView:(UITableView*)tableView 174 viewForHeaderInSection:(NSInteger)section { 175 UITableViewHeaderFooterView* view = [self.tableView 176 dequeueReusableHeaderFooterViewWithIdentifier:kHeaderIdentifier]; 177 view.textLabel.font = 178 [UIFont preferredFontForTextStyle:UIFontTextStyleCaption1]; 179 view.contentView.backgroundColor = [UIColor colorNamed:kBackgroundColor]; 180 return view; 181} 182 183- (CGFloat)tableView:(UITableView*)tableView 184 heightForHeaderInSection:(NSInteger)section { 185 return kHeaderHeight; 186} 187 188- (void)tableView:(UITableView*)tableView 189 didSelectRowAtIndexPath:(NSIndexPath*)indexPath { 190 UpdateUMACountForKey(app_group::kCredentialExtensionPasswordUseCount); 191 id<Credential> credential = [self credentialForIndexPath:indexPath]; 192 [self.delegate userSelectedCredential:credential]; 193} 194 195#pragma mark - UISearchResultsUpdating 196 197- (void)updateSearchResultsForSearchController: 198 (UISearchController*)searchController { 199 if (searchController.searchBar.text.length) { 200 UpdateUMACountForKey(app_group::kCredentialExtensionSearchCount); 201 } 202 [self.delegate updateResultsWithFilter:searchController.searchBar.text]; 203} 204 205#pragma mark - Private 206 207// Creates a cancel button for the navigation item. 208- (UIBarButtonItem*)navigationCancelButton { 209 UIBarButtonItem* cancelButton = [[UIBarButtonItem alloc] 210 initWithBarButtonSystemItem:UIBarButtonSystemItemCancel 211 target:self.delegate 212 action:@selector(navigationCancelButtonWasPressed:)]; 213 cancelButton.tintColor = [UIColor colorNamed:kBlueColor]; 214 return cancelButton; 215} 216 217// Creates a button to be displayed as accessory of the password row item. 218- (UIView*)infoIconButton { 219 UIImage* image = [UIImage imageNamed:@"info_icon"]; 220 image = [image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; 221 222 UIButton* button = [UIButton buttonWithType:UIButtonTypeCustom]; 223 button.frame = CGRectMake(0.0, 0.0, image.size.width, image.size.height); 224 [button setBackgroundImage:image forState:UIControlStateNormal]; 225 [button setTintColor:[UIColor colorNamed:kBlueColor]]; 226 [button addTarget:self 227 action:@selector(infoIconButtonTapped:event:) 228 forControlEvents:UIControlEventTouchUpInside]; 229 button.accessibilityLabel = NSLocalizedString( 230 @"IDS_IOS_CREDENTIAL_PROVIDER_SHOW_DETAILS_ACCESSIBILITY_LABEL", 231 @"Show Details."); 232 233#if defined(__IPHONE_13_4) 234 if (@available(iOS 13.4, *)) { 235 button.pointerInteractionEnabled = YES; 236 button.pointerStyleProvider = ^UIPointerStyle*( 237 UIButton* button, __unused UIPointerEffect* proposedEffect, 238 __unused UIPointerShape* proposedShape) { 239 UITargetedPreview* preview = 240 [[UITargetedPreview alloc] initWithView:button]; 241 UIPointerHighlightEffect* effect = 242 [UIPointerHighlightEffect effectWithPreview:preview]; 243 UIPointerShape* shape = 244 [UIPointerShape shapeWithRoundedRect:button.frame 245 cornerRadius:button.frame.size.width / 2]; 246 return [UIPointerStyle styleWithEffect:effect shape:shape]; 247 }; 248 } 249#endif // defined(__IPHONE_13_4) 250 251 return button; 252} 253 254// Called when info icon is tapped. 255- (void)infoIconButtonTapped:(id)sender event:(id)event { 256 CGPoint hitPoint = 257 [base::mac::ObjCCastStrict<UIButton>(sender) convertPoint:CGPointZero 258 toView:self.tableView]; 259 NSIndexPath* indexPath = [self.tableView indexPathForRowAtPoint:hitPoint]; 260 id<Credential> credential = [self credentialForIndexPath:indexPath]; 261 [self.delegate showDetailsForCredential:credential]; 262} 263 264// Returns number of sections to display based on |suggestedPasswords| and 265// |allPasswords|. If no sections with data, returns 1 for the 'no data' banner. 266- (int)numberOfSections { 267 if ([self.suggestedPasswords count] == 0 || [self.allPasswords count] == 0) { 268 return 1; 269 } 270 return 2; 271} 272 273// Returns YES if there is no data to display. 274- (BOOL)isEmptyTable { 275 return [self.suggestedPasswords count] == 0 && [self.allPasswords count] == 0; 276} 277 278// Returns YES if given section is for suggested passwords. 279- (BOOL)isSuggestedPasswordSection:(int)section { 280 int sections = [self numberOfSections]; 281 if ((sections == 2 && section == 0) || 282 (sections == 1 && self.suggestedPasswords.count)) { 283 return YES; 284 } else { 285 return NO; 286 } 287} 288 289- (id<Credential>)credentialForIndexPath:(NSIndexPath*)indexPath { 290 if ([self isSuggestedPasswordSection:indexPath.section]) { 291 return self.suggestedPasswords[indexPath.row]; 292 } else { 293 return self.allPasswords[indexPath.row]; 294 } 295} 296 297@end 298