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