1// Copyright 2014 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/web_view/shell/shell_view_controller.h"
6
7#import <MobileCoreServices/MobileCoreServices.h>
8
9#import "ios/web_view/shell/shell_auth_service.h"
10#import "ios/web_view/shell/shell_autofill_delegate.h"
11#import "ios/web_view/shell/shell_translation_delegate.h"
12
13#if !defined(__has_feature) || !__has_feature(objc_arc)
14#error "This file requires ARC support."
15#endif
16
17// Externed accessibility identifier.
18NSString* const kWebViewShellBackButtonAccessibilityLabel = @"Back";
19NSString* const kWebViewShellForwardButtonAccessibilityLabel = @"Forward";
20NSString* const kWebViewShellAddressFieldAccessibilityLabel = @"Address field";
21NSString* const kWebViewShellJavaScriptDialogTextFieldAccessibilityIdentifier =
22    @"WebViewShellJavaScriptDialogTextFieldAccessibilityIdentifier";
23
24@interface ShellViewController () <CWVDownloadTaskDelegate,
25                                   CWVNavigationDelegate,
26                                   CWVUIDelegate,
27                                   CWVScriptCommandHandler,
28                                   CWVSyncControllerDelegate,
29                                   UIScrollViewDelegate,
30                                   UITextFieldDelegate>
31// Header containing navigation buttons and |field|.
32@property(nonatomic, strong) UIView* headerBackgroundView;
33// Header containing navigation buttons and |field|.
34@property(nonatomic, strong) UIView* headerContentView;
35// Button to navigate backwards.
36@property(nonatomic, strong) UIButton* backButton;
37// Button to navigate forwards.
38@property(nonatomic, strong) UIButton* forwardButton;
39// Button that either refresh the page or stops the page load.
40@property(nonatomic, strong) UIButton* reloadOrStopButton;
41// Button that shows the menu
42@property(nonatomic, strong) UIButton* menuButton;
43// Text field used for navigating to URLs.
44@property(nonatomic, strong) UITextField* field;
45// Container for |webView|.
46@property(nonatomic, strong) UIView* contentView;
47// Handles the autofill of the content displayed in |webView|.
48@property(nonatomic, strong) ShellAutofillDelegate* autofillDelegate;
49// Handles the translation of the content displayed in |webView|.
50@property(nonatomic, strong) ShellTranslationDelegate* translationDelegate;
51// The on-going download task if any.
52@property(nonatomic, strong, nullable) CWVDownloadTask* downloadTask;
53// The path to a local file which the download task is writing to.
54@property(nonatomic, strong, nullable) NSString* downloadFilePath;
55// A controller to show a "Share" menu for the downloaded file.
56@property(nonatomic, strong, nullable)
57    UIDocumentInteractionController* documentInteractionController;
58@property(nonatomic, strong) ShellAuthService* authService;
59// The newly opened popup windows e.g., by JavaScript function "window.open()",
60// HTML "<a target='_blank'>".
61@property(nonatomic, strong) NSMutableArray<CWVWebView*>* popupWebViews;
62
63- (void)back;
64- (void)forward;
65- (void)reloadOrStop;
66// Disconnects and release the |webView|.
67- (void)removeWebView;
68// Resets translate settings back to default.
69- (void)resetTranslateSettings;
70@end
71
72@implementation ShellViewController
73
74@synthesize autofillDelegate = _autofillDelegate;
75@synthesize backButton = _backButton;
76@synthesize contentView = _contentView;
77@synthesize field = _field;
78@synthesize forwardButton = _forwardButton;
79@synthesize reloadOrStopButton = _reloadOrStopButton;
80@synthesize menuButton = _menuButton;
81@synthesize headerBackgroundView = _headerBackgroundView;
82@synthesize headerContentView = _headerContentView;
83@synthesize webView = _webView;
84@synthesize translationDelegate = _translationDelegate;
85@synthesize downloadTask = _downloadTask;
86@synthesize downloadFilePath = _downloadFilePath;
87@synthesize documentInteractionController = _documentInteractionController;
88@synthesize authService = _authService;
89@synthesize popupWebViews = _popupWebViews;
90
91- (void)viewDidLoad {
92  [super viewDidLoad];
93
94  self.popupWebViews = [[NSMutableArray alloc] init];
95
96  // View creation.
97  self.headerBackgroundView = [[UIView alloc] init];
98  self.headerContentView = [[UIView alloc] init];
99  self.contentView = [[UIView alloc] init];
100  self.backButton = [[UIButton alloc] init];
101  self.forwardButton = [[UIButton alloc] init];
102  self.reloadOrStopButton = [[UIButton alloc] init];
103  self.menuButton = [[UIButton alloc] init];
104  self.field = [[UITextField alloc] init];
105
106  // View hierarchy.
107  [self.view addSubview:_headerBackgroundView];
108  [self.view addSubview:_contentView];
109  [_headerBackgroundView addSubview:_headerContentView];
110  [_headerContentView addSubview:_backButton];
111  [_headerContentView addSubview:_forwardButton];
112  [_headerContentView addSubview:_reloadOrStopButton];
113  [_headerContentView addSubview:_menuButton];
114  [_headerContentView addSubview:_field];
115
116  // Additional view setup.
117  _headerBackgroundView.backgroundColor = [UIColor colorWithRed:66.0 / 255.0
118                                                          green:133.0 / 255.0
119                                                           blue:244.0 / 255.0
120                                                          alpha:1.0];
121
122  [_backButton setImage:[UIImage imageNamed:@"ic_back"]
123               forState:UIControlStateNormal];
124  _backButton.tintColor = [UIColor whiteColor];
125  [_backButton addTarget:self
126                  action:@selector(back)
127        forControlEvents:UIControlEventTouchUpInside];
128  [_backButton addTarget:self
129                  action:@selector(logBackStack)
130        forControlEvents:UIControlEventTouchDragOutside];
131  [_backButton setAccessibilityLabel:kWebViewShellBackButtonAccessibilityLabel];
132
133  [_forwardButton setImage:[UIImage imageNamed:@"ic_forward"]
134                  forState:UIControlStateNormal];
135  _forwardButton.tintColor = [UIColor whiteColor];
136  [_forwardButton addTarget:self
137                     action:@selector(forward)
138           forControlEvents:UIControlEventTouchUpInside];
139  [_forwardButton addTarget:self
140                     action:@selector(logForwardStack)
141           forControlEvents:UIControlEventTouchDragOutside];
142  [_forwardButton
143      setAccessibilityLabel:kWebViewShellForwardButtonAccessibilityLabel];
144
145  _reloadOrStopButton.tintColor = [UIColor whiteColor];
146  [_reloadOrStopButton addTarget:self
147                          action:@selector(reloadOrStop)
148                forControlEvents:UIControlEventTouchUpInside];
149
150  _menuButton.tintColor = [UIColor whiteColor];
151  [_menuButton setImage:[UIImage imageNamed:@"ic_menu"]
152               forState:UIControlStateNormal];
153  [_menuButton addTarget:self
154                  action:@selector(showMainMenu)
155        forControlEvents:UIControlEventTouchUpInside];
156
157  _field.placeholder = @"Search or type URL";
158  _field.backgroundColor = [UIColor whiteColor];
159  _field.tintColor = _headerBackgroundView.backgroundColor;
160  [_field setContentHuggingPriority:UILayoutPriorityDefaultLow - 1
161                            forAxis:UILayoutConstraintAxisHorizontal];
162  _field.delegate = self;
163  _field.layer.cornerRadius = 2.0;
164  _field.keyboardType = UIKeyboardTypeURL;
165  _field.autocapitalizationType = UITextAutocapitalizationTypeNone;
166  _field.clearButtonMode = UITextFieldViewModeWhileEditing;
167  _field.autocorrectionType = UITextAutocorrectionTypeNo;
168  UIView* spacerView = [[UIView alloc] init];
169  spacerView.frame = CGRectMake(0, 0, 8, 8);
170  _field.leftViewMode = UITextFieldViewModeAlways;
171  _field.leftView = spacerView;
172
173  // Constraints.
174  _headerBackgroundView.translatesAutoresizingMaskIntoConstraints = NO;
175  [NSLayoutConstraint activateConstraints:@[
176    [_headerBackgroundView.topAnchor
177        constraintEqualToAnchor:self.view.topAnchor],
178    [_headerBackgroundView.leadingAnchor
179        constraintEqualToAnchor:self.view.leadingAnchor],
180    [_headerBackgroundView.trailingAnchor
181        constraintEqualToAnchor:self.view.trailingAnchor],
182    [_headerBackgroundView.bottomAnchor
183        constraintEqualToAnchor:_headerContentView.bottomAnchor],
184  ]];
185
186  _headerContentView.translatesAutoresizingMaskIntoConstraints = NO;
187  [NSLayoutConstraint activateConstraints:@[
188    [_headerContentView.topAnchor
189        constraintEqualToAnchor:_headerBackgroundView.safeAreaLayoutGuide
190                                    .topAnchor],
191    [_headerContentView.leadingAnchor
192        constraintEqualToAnchor:_headerBackgroundView.safeAreaLayoutGuide
193                                    .leadingAnchor],
194    [_headerContentView.trailingAnchor
195        constraintEqualToAnchor:_headerBackgroundView.safeAreaLayoutGuide
196                                    .trailingAnchor],
197    [_headerContentView.heightAnchor constraintEqualToConstant:56.0],
198  ]];
199
200  _contentView.translatesAutoresizingMaskIntoConstraints = NO;
201  [NSLayoutConstraint activateConstraints:@[
202    [_contentView.topAnchor
203        constraintEqualToAnchor:_headerBackgroundView.bottomAnchor],
204    [_contentView.leadingAnchor
205        constraintEqualToAnchor:self.view.leadingAnchor],
206    [_contentView.trailingAnchor
207        constraintEqualToAnchor:self.view.trailingAnchor],
208    [_contentView.bottomAnchor constraintEqualToAnchor:self.view.bottomAnchor],
209  ]];
210
211  _backButton.translatesAutoresizingMaskIntoConstraints = NO;
212  [NSLayoutConstraint activateConstraints:@[
213    [_backButton.leadingAnchor
214        constraintEqualToAnchor:_headerContentView.safeAreaLayoutGuide
215                                    .leadingAnchor
216                       constant:16.0],
217    [_backButton.centerYAnchor
218        constraintEqualToAnchor:_headerContentView.centerYAnchor],
219  ]];
220
221  _forwardButton.translatesAutoresizingMaskIntoConstraints = NO;
222  [NSLayoutConstraint activateConstraints:@[
223    [_forwardButton.leadingAnchor
224        constraintEqualToAnchor:_backButton.trailingAnchor
225                       constant:16.0],
226    [_forwardButton.centerYAnchor
227        constraintEqualToAnchor:_headerContentView.centerYAnchor],
228  ]];
229
230  _reloadOrStopButton.translatesAutoresizingMaskIntoConstraints = NO;
231  [NSLayoutConstraint activateConstraints:@[
232    [_reloadOrStopButton.leadingAnchor
233        constraintEqualToAnchor:_forwardButton.trailingAnchor
234                       constant:16.0],
235    [_reloadOrStopButton.centerYAnchor
236        constraintEqualToAnchor:_headerContentView.centerYAnchor],
237  ]];
238  _menuButton.translatesAutoresizingMaskIntoConstraints = NO;
239  [NSLayoutConstraint activateConstraints:@[
240    [_menuButton.leadingAnchor
241        constraintEqualToAnchor:_reloadOrStopButton.trailingAnchor
242                       constant:16.0],
243    [_menuButton.centerYAnchor
244        constraintEqualToAnchor:_headerContentView.centerYAnchor],
245  ]];
246
247  _field.translatesAutoresizingMaskIntoConstraints = NO;
248  [NSLayoutConstraint activateConstraints:@[
249    [_field.leadingAnchor constraintEqualToAnchor:_menuButton.trailingAnchor
250                                         constant:16.0],
251    [_field.centerYAnchor
252        constraintEqualToAnchor:_headerContentView.centerYAnchor],
253    [_field.trailingAnchor
254        constraintEqualToAnchor:_headerContentView.safeAreaLayoutGuide
255                                    .trailingAnchor
256                       constant:-16.0],
257    [_field.heightAnchor constraintEqualToConstant:32.0],
258  ]];
259
260  [CWVWebView setUserAgentProduct:@"Dummy/1.0"];
261  CWVWebView.chromeLongPressAndForceTouchHandlingEnabled = NO;
262
263  _authService = [[ShellAuthService alloc] init];
264  CWVSyncController.dataSource = _authService;
265
266  CWVWebViewConfiguration* configuration =
267      [CWVWebViewConfiguration defaultConfiguration];
268  configuration.syncController.delegate = self;
269  self.webView = [self createWebViewWithConfiguration:configuration];
270}
271
272- (void)applicationFinishedRestoringState {
273  [super applicationFinishedRestoringState];
274
275  // The scroll view is reset on state restoration. So the delegate must be
276  // reassigned.
277  self.webView.scrollView.delegate = self;
278}
279
280- (UIStatusBarStyle)preferredStatusBarStyle {
281  return UIStatusBarStyleLightContent;
282}
283
284- (void)observeValueForKeyPath:(NSString*)keyPath
285                      ofObject:(id)object
286                        change:(NSDictionary<NSKeyValueChangeKey, id>*)change
287                       context:(void*)context {
288  if ([keyPath isEqualToString:@"canGoBack"]) {
289    _backButton.enabled = [_webView canGoBack];
290  } else if ([keyPath isEqualToString:@"canGoForward"]) {
291    _forwardButton.enabled = [_webView canGoForward];
292  } else if ([keyPath isEqualToString:@"loading"]) {
293    NSString* imageName = _webView.loading ? @"ic_stop" : @"ic_reload";
294    [_reloadOrStopButton setImage:[UIImage imageNamed:imageName]
295                         forState:UIControlStateNormal];
296  }
297}
298
299- (void)back {
300  if ([_webView canGoBack]) {
301    [_webView goBack];
302  }
303}
304
305- (void)logBackStack {
306  if (!_webView.canGoBack) {
307    return;
308  }
309  CWVBackForwardList* list = _webView.backForwardList;
310  CWVBackForwardListItemArray* backList = list.backList;
311  for (size_t i = 0; i < backList.count; i++) {
312    CWVBackForwardListItem* item = backList[i];
313    NSLog(@"BackStack Item #%ld: <URL='%@', title='%@'>", i, item.URL,
314          item.title);
315  }
316}
317
318- (void)forward {
319  if ([_webView canGoForward]) {
320    [_webView goForward];
321  }
322}
323
324- (void)logForwardStack {
325  if (!_webView.canGoForward) {
326    return;
327  }
328  CWVBackForwardList* list = _webView.backForwardList;
329  CWVBackForwardListItemArray* forwardList = list.forwardList;
330  for (size_t i = 0; i < forwardList.count; i++) {
331    CWVBackForwardListItem* item = forwardList[i];
332    NSLog(@"ForwardStack Item #%ld: <URL='%@', title='%@'>", i, item.URL,
333          item.title);
334  }
335}
336
337- (void)reloadOrStop {
338  if (_webView.loading) {
339    [_webView stopLoading];
340  } else {
341    [_webView reload];
342  }
343}
344
345- (void)showAddressData {
346  CWVAutofillDataManager* dataManager =
347      _webView.configuration.autofillDataManager;
348  [dataManager fetchProfilesWithCompletionHandler:^(
349                   NSArray<CWVAutofillProfile*>* _Nonnull profiles) {
350    NSMutableArray<NSString*>* descriptions = [profiles
351        valueForKey:NSStringFromSelector(@selector(debugDescription))];
352    NSString* message = [descriptions componentsJoinedByString:@"\n\n"];
353    UIAlertController* alertController = [self actionSheetWithTitle:@"Addresses"
354                                                            message:message];
355    for (CWVAutofillProfile* profile in profiles) {
356      NSString* title = [NSString
357          stringWithFormat:@"Delete %@", @([profiles indexOfObject:profile])];
358      UIAlertAction* action =
359          [UIAlertAction actionWithTitle:title
360                                   style:UIAlertActionStyleDefault
361                                 handler:^(UIAlertAction* action) {
362                                   [dataManager deleteProfile:profile];
363                                 }];
364      [alertController addAction:action];
365    }
366    [alertController
367        addAction:[UIAlertAction actionWithTitle:@"Done"
368                                           style:UIAlertActionStyleCancel
369                                         handler:nil]];
370
371    [self presentViewController:alertController animated:YES completion:nil];
372  }];
373}
374
375- (void)showCreditCardData {
376  CWVAutofillDataManager* dataManager =
377      _webView.configuration.autofillDataManager;
378  [dataManager fetchCreditCardsWithCompletionHandler:^(
379                   NSArray<CWVCreditCard*>* _Nonnull creditCards) {
380    NSMutableArray<NSString*>* descriptions = [creditCards
381        valueForKey:NSStringFromSelector(@selector(debugDescription))];
382    NSString* message = [descriptions componentsJoinedByString:@"\n\n"];
383    UIAlertController* alertController =
384        [self actionSheetWithTitle:@"Credit cards" message:message];
385    __weak ShellViewController* weakSelf = self;
386    [alertController
387        addAction:[UIAlertAction
388                      actionWithTitle:@"Manage Google pay cards"
389                                style:UIAlertActionStyleDefault
390                              handler:^(UIAlertAction* action) {
391                                __weak ShellViewController* strongSelf =
392                                    weakSelf;
393                                NSString* URL;
394                                if ([CWVFlags sharedInstance]
395                                        .usesSyncAndWalletSandbox) {
396                                  URL = @"https://pay.sandbox.google.com/"
397                                        @"payments/home#paymentMethods";
398                                } else {
399                                  URL = @"https://pay.google.com/payments/"
400                                        @"home#paymentMethods";
401                                }
402                                NSURLRequest* request = [NSURLRequest
403                                    requestWithURL:[NSURL URLWithString:URL]];
404                                [strongSelf.webView loadRequest:request];
405                              }]];
406    [alertController
407        addAction:[UIAlertAction actionWithTitle:@"Done"
408                                           style:UIAlertActionStyleCancel
409                                         handler:nil]];
410    [self presentViewController:alertController animated:YES completion:nil];
411  }];
412}
413
414- (void)showPasswordData {
415  CWVAutofillDataManager* dataManager =
416      _webView.configuration.autofillDataManager;
417  [dataManager fetchPasswordsWithCompletionHandler:^(
418                   NSArray<CWVPassword*>* _Nonnull passwords) {
419    NSMutableArray<NSString*>* descriptions = [passwords
420        valueForKey:NSStringFromSelector(@selector(debugDescription))];
421    NSString* message = [descriptions componentsJoinedByString:@"\n\n"];
422
423    UIAlertController* alertController = [self actionSheetWithTitle:@"Passwords"
424                                                            message:message];
425    for (CWVPassword* password in passwords) {
426      NSString* title = [NSString
427          stringWithFormat:@"Delete %@", @([passwords indexOfObject:password])];
428      UIAlertAction* action =
429          [UIAlertAction actionWithTitle:title
430                                   style:UIAlertActionStyleDefault
431                                 handler:^(UIAlertAction* action) {
432                                   [dataManager deletePassword:password];
433                                 }];
434      [alertController addAction:action];
435    }
436    [alertController
437        addAction:[UIAlertAction actionWithTitle:@"Done"
438                                           style:UIAlertActionStyleCancel
439                                         handler:nil]];
440    [self presentViewController:alertController animated:YES completion:nil];
441  }];
442}
443
444- (void)showSyncMenu {
445  UIAlertController* alertController = [self actionSheetWithTitle:@"Sync menu"
446                                                          message:nil];
447
448  CWVSyncController* syncController = _webView.configuration.syncController;
449  CWVIdentity* currentIdentity = syncController.currentIdentity;
450  __weak ShellViewController* weakSelf = self;
451  if (currentIdentity) {
452    NSString* title = [NSString
453        stringWithFormat:@"Stop syncing for %@", currentIdentity.email];
454    [alertController
455        addAction:[UIAlertAction
456                      actionWithTitle:title
457                                style:UIAlertActionStyleDefault
458                              handler:^(UIAlertAction* action) {
459                                [syncController stopSyncAndClearIdentity];
460                              }]];
461
462    if (syncController.passphraseNeeded) {
463      [alertController
464          addAction:[UIAlertAction
465                        actionWithTitle:@"Unlock using passphrase"
466                                  style:UIAlertActionStyleDefault
467                                handler:^(UIAlertAction* action) {
468                                  [weakSelf showPassphraseUnlockAlert];
469                                }]];
470    } else {
471    }
472  } else {
473    for (CWVIdentity* identity in [_authService identities]) {
474      NSString* title =
475          [NSString stringWithFormat:@"Start sync with %@", identity.email];
476      [alertController
477          addAction:[UIAlertAction
478                        actionWithTitle:title
479                                  style:UIAlertActionStyleDefault
480                                handler:^(UIAlertAction* action) {
481                                  [syncController
482                                      startSyncWithIdentity:identity];
483                                }]];
484    }
485
486    NSString* sandboxTitle = [CWVFlags sharedInstance].usesSyncAndWalletSandbox
487                                 ? @"Use production sync/wallet"
488                                 : @"Use sandbox sync/wallet";
489    [alertController
490        addAction:[UIAlertAction actionWithTitle:sandboxTitle
491                                           style:UIAlertActionStyleDefault
492                                         handler:^(UIAlertAction* action) {
493                                           [CWVFlags sharedInstance]
494                                               .usesSyncAndWalletSandbox ^= YES;
495                                         }]];
496  }
497  [alertController
498      addAction:[UIAlertAction actionWithTitle:@"Show autofill data"
499                                         style:UIAlertActionStyleDefault
500                                       handler:^(UIAlertAction* action) {
501                                         [weakSelf showAddressData];
502                                       }]];
503  [alertController
504      addAction:[UIAlertAction actionWithTitle:@"Show credit card data"
505                                         style:UIAlertActionStyleDefault
506                                       handler:^(UIAlertAction* action) {
507                                         [weakSelf showCreditCardData];
508                                       }]];
509  [alertController
510      addAction:[UIAlertAction actionWithTitle:@"Show password data"
511                                         style:UIAlertActionStyleDefault
512                                       handler:^(UIAlertAction* action) {
513                                         [weakSelf showPasswordData];
514                                       }]];
515  [alertController
516      addAction:[UIAlertAction actionWithTitle:@"Cancel"
517                                         style:UIAlertActionStyleCancel
518                                       handler:nil]];
519
520  [self presentViewController:alertController animated:YES completion:nil];
521}
522
523- (void)showPassphraseUnlockAlert {
524  UIAlertController* alertController =
525      [UIAlertController alertControllerWithTitle:@"Unlock sync"
526                                          message:@"Enter passphrase"
527                                   preferredStyle:UIAlertControllerStyleAlert];
528
529  __weak UIAlertController* weakAlertController = alertController;
530  CWVSyncController* syncController = _webView.configuration.syncController;
531  UIAlertAction* submit = [UIAlertAction
532      actionWithTitle:@"Unlock"
533                style:UIAlertActionStyleDefault
534              handler:^(UIAlertAction* action) {
535                UITextField* textField =
536                    weakAlertController.textFields.firstObject;
537                NSString* passphrase = textField.text;
538                BOOL result = [syncController unlockWithPassphrase:passphrase];
539                NSLog(@"Sync passphrase unlock result: %d", result);
540              }];
541
542  [alertController addAction:submit];
543
544  UIAlertAction* cancel =
545      [UIAlertAction actionWithTitle:@"Cancel"
546                               style:UIAlertActionStyleCancel
547                             handler:nil];
548  [alertController addAction:cancel];
549
550  [alertController
551      addTextFieldWithConfigurationHandler:^(UITextField* textField) {
552        textField.placeholder = @"passphrase";
553        textField.keyboardType = UIKeyboardTypeDefault;
554      }];
555
556  [self presentViewController:alertController animated:YES completion:nil];
557}
558
559- (void)showMainMenu {
560  UIAlertController* alertController = [self actionSheetWithTitle:@"Main menu"
561                                                          message:nil];
562  [alertController
563      addAction:[UIAlertAction actionWithTitle:@"Cancel"
564                                         style:UIAlertActionStyleCancel
565                                       handler:nil]];
566
567  __weak ShellViewController* weakSelf = self;
568
569  // Toggles the incognito mode.
570  NSString* incognitoActionTitle = _webView.configuration.persistent
571                                       ? @"Enter incognito"
572                                       : @"Exit incognito";
573  [alertController
574      addAction:[UIAlertAction actionWithTitle:incognitoActionTitle
575                                         style:UIAlertActionStyleDefault
576                                       handler:^(UIAlertAction* action) {
577                                         [weakSelf toggleIncognito];
578                                       }]];
579
580  // Removes the web view from the view hierarchy, releases it, and recreates
581  // the web view with the same configuration. This is for testing deallocation
582  // and sharing configuration.
583  [alertController
584      addAction:[UIAlertAction
585                    actionWithTitle:@"Recreate web view"
586                              style:UIAlertActionStyleDefault
587                            handler:^(UIAlertAction* action) {
588                              CWVWebViewConfiguration* configuration =
589                                  weakSelf.webView.configuration;
590                              [weakSelf removeWebView];
591                              weakSelf.webView = [weakSelf
592                                  createWebViewWithConfiguration:configuration];
593                            }]];
594
595  // Developers can choose to use system or Chrome context menu in the shell
596  // app. This will also recreate the web view.
597  BOOL chromeContextMenuEnabled =
598      CWVWebView.chromeLongPressAndForceTouchHandlingEnabled;
599  NSString* contextMenuSwitchActionTitle = [NSString
600      stringWithFormat:@"Use %@ context menu",
601                       chromeContextMenuEnabled ? @"system" : @"Chrome"];
602  [alertController
603      addAction:[UIAlertAction
604                    actionWithTitle:contextMenuSwitchActionTitle
605                              style:UIAlertActionStyleDefault
606                            handler:^(UIAlertAction* action) {
607                              CWVWebView
608                                  .chromeLongPressAndForceTouchHandlingEnabled =
609                                  !chromeContextMenuEnabled;
610                              NSLog(@"Chrome context menu is %@ now.",
611                                    !chromeContextMenuEnabled ? @"OFF" : @"ON");
612                              CWVWebViewConfiguration* configuration =
613                                  weakSelf.webView.configuration;
614                              [weakSelf removeWebView];
615                              weakSelf.webView = [weakSelf
616                                  createWebViewWithConfiguration:configuration];
617                            }]];
618
619  // Resets all translation settings to default values.
620  [alertController
621      addAction:[UIAlertAction actionWithTitle:@"Reset translate settings"
622                                         style:UIAlertActionStyleDefault
623                                       handler:^(UIAlertAction* action) {
624                                         [weakSelf resetTranslateSettings];
625                                       }]];
626
627  [alertController
628      addAction:[UIAlertAction actionWithTitle:@"Request translation offer"
629                                         style:UIAlertActionStyleDefault
630                                       handler:^(UIAlertAction* action) {
631                                         [weakSelf requestTranslationOffer];
632                                       }]];
633
634  // Shows sync menu.
635  [alertController
636      addAction:[UIAlertAction actionWithTitle:@"Sync menu"
637                                         style:UIAlertActionStyleDefault
638                                       handler:^(UIAlertAction* action) {
639                                         [weakSelf showSyncMenu];
640                                       }]];
641
642  if (self.downloadTask) {
643    [alertController
644        addAction:[UIAlertAction actionWithTitle:@"Cancel download"
645                                           style:UIAlertActionStyleDefault
646                                         handler:^(UIAlertAction* action) {
647                                           [weakSelf.downloadTask cancel];
648                                         }]];
649  }
650
651  [self presentViewController:alertController animated:YES completion:nil];
652}
653
654- (void)resetTranslateSettings {
655  CWVWebViewConfiguration* configuration =
656      [CWVWebViewConfiguration defaultConfiguration];
657  [configuration.preferences resetTranslationSettings];
658}
659
660- (void)requestTranslationOffer {
661  BOOL offered = [_webView.translationController requestTranslationOffer];
662  NSLog(@"Manual translation was offered: %d", offered);
663}
664
665- (void)toggleIncognito {
666  BOOL wasPersistent = _webView.configuration.persistent;
667  [self removeWebView];
668  CWVWebViewConfiguration* newConfiguration =
669      wasPersistent ? [CWVWebViewConfiguration incognitoConfiguration]
670                    : [CWVWebViewConfiguration defaultConfiguration];
671  self.webView = [self createWebViewWithConfiguration:newConfiguration];
672}
673
674- (CWVWebView*)createWebViewWithConfiguration:
675    (CWVWebViewConfiguration*)configuration {
676  // Set a non empty CGRect to avoid DCHECKs that occur when a load happens
677  // after state restoration, and before the view hierarchy is laid out for the
678  // first time.
679  // https://source.chromium.org/chromium/chromium/src/+/master:ios/web/web_state/ui/crw_web_request_controller.mm;l=518;drc=df887034106ef438611326745a7cd276eedd4953
680  CGRect frame = CGRectMake(0, 0, 1, 1);
681  CWVWebView* webView = [[CWVWebView alloc] initWithFrame:frame
682                                            configuration:configuration];
683  [_contentView addSubview:webView];
684
685  // Gives a restoration identifier so that state restoration works.
686  webView.restorationIdentifier = @"webView";
687
688  // Configure delegates.
689  webView.navigationDelegate = self;
690  webView.UIDelegate = self;
691  _translationDelegate = [[ShellTranslationDelegate alloc] init];
692  webView.translationController.delegate = _translationDelegate;
693  _autofillDelegate = [[ShellAutofillDelegate alloc] init];
694  webView.autofillController.delegate = _autofillDelegate;
695  webView.scrollView.delegate = self;
696
697  // Constraints.
698  webView.translatesAutoresizingMaskIntoConstraints = NO;
699  [NSLayoutConstraint activateConstraints:@[
700    [webView.topAnchor
701        constraintEqualToAnchor:_contentView.safeAreaLayoutGuide.topAnchor],
702    [webView.leadingAnchor
703        constraintEqualToAnchor:_contentView.safeAreaLayoutGuide.leadingAnchor],
704    [webView.trailingAnchor
705        constraintEqualToAnchor:_contentView.safeAreaLayoutGuide
706                                    .trailingAnchor],
707    [webView.bottomAnchor
708        constraintEqualToAnchor:_contentView.safeAreaLayoutGuide.bottomAnchor],
709  ]];
710
711  [webView addObserver:self
712            forKeyPath:@"canGoBack"
713               options:NSKeyValueObservingOptionNew |
714                       NSKeyValueObservingOptionInitial
715               context:nil];
716  [webView addObserver:self
717            forKeyPath:@"canGoForward"
718               options:NSKeyValueObservingOptionNew |
719                       NSKeyValueObservingOptionInitial
720               context:nil];
721  [webView addObserver:self
722            forKeyPath:@"loading"
723               options:NSKeyValueObservingOptionNew |
724                       NSKeyValueObservingOptionInitial
725               context:nil];
726
727  [webView addScriptCommandHandler:self commandPrefix:@"test"];
728
729  return webView;
730}
731
732- (void)removeWebView {
733  [_webView removeFromSuperview];
734  [_webView removeObserver:self forKeyPath:@"canGoBack"];
735  [_webView removeObserver:self forKeyPath:@"canGoForward"];
736  [_webView removeObserver:self forKeyPath:@"loading"];
737  [_webView removeScriptCommandHandlerForCommandPrefix:@"test"];
738
739  _webView = nil;
740}
741
742- (void)dealloc {
743  [_webView removeObserver:self forKeyPath:@"canGoBack"];
744  [_webView removeObserver:self forKeyPath:@"canGoForward"];
745  [_webView removeObserver:self forKeyPath:@"loading"];
746  [_webView removeScriptCommandHandlerForCommandPrefix:@"test"];
747}
748
749- (BOOL)textFieldShouldReturn:(UITextField*)field {
750  NSURL* URL = [NSURL URLWithString:field.text];
751  if (URL.scheme.length == 0) {
752    NSString* enteredText = field.text;
753    enteredText =
754        [enteredText stringByAddingPercentEncodingWithAllowedCharacters:
755                         [NSCharacterSet URLQueryAllowedCharacterSet]];
756    enteredText = [NSString
757        stringWithFormat:@"https://www.google.com/search?q=%@", enteredText];
758    URL = [NSURL URLWithString:enteredText];
759  }
760  NSURLRequest* request = [NSURLRequest requestWithURL:URL];
761  [_webView loadRequest:request];
762  [field resignFirstResponder];
763  [self updateToolbar];
764  return YES;
765}
766
767- (void)updateToolbar {
768  // Do not update the URL if the text field is currently being edited.
769  if ([_field isFirstResponder]) {
770    return;
771  }
772
773  [_field setText:[[_webView visibleURL] absoluteString]];
774}
775
776- (UIAlertController*)actionSheetWithTitle:(nullable NSString*)title
777                                   message:(nullable NSString*)message {
778  UIAlertController* alertController = [UIAlertController
779      alertControllerWithTitle:title
780                       message:message
781                preferredStyle:UIAlertControllerStyleActionSheet];
782  alertController.popoverPresentationController.sourceView = _menuButton;
783  alertController.popoverPresentationController.sourceRect =
784      CGRectMake(CGRectGetWidth(_menuButton.bounds) / 2,
785                 CGRectGetHeight(_menuButton.bounds), 1, 1);
786  return alertController;
787}
788
789- (void)closePopupWebView {
790  if (self.popupWebViews.count) {
791    [self.popupWebViews.lastObject removeFromSuperview];
792    [self.popupWebViews removeLastObject];
793  }
794}
795
796#pragma mark CWVUIDelegate methods
797
798- (CWVWebView*)webView:(CWVWebView*)webView
799    createWebViewWithConfiguration:(CWVWebViewConfiguration*)configuration
800               forNavigationAction:(CWVNavigationAction*)action {
801  NSLog(@"Create new CWVWebView for %@. User initiated? %@", action.request.URL,
802        action.userInitiated ? @"Yes" : @"No");
803
804  CWVWebView* newWebView = [self createWebViewWithConfiguration:configuration];
805  [self.popupWebViews addObject:newWebView];
806
807  UIButton* closeWindowButton = [[UIButton alloc] init];
808  [closeWindowButton setImage:[UIImage imageNamed:@"ic_stop"]
809                     forState:UIControlStateNormal];
810  closeWindowButton.tintColor = [UIColor blackColor];
811  closeWindowButton.backgroundColor = [UIColor whiteColor];
812  [closeWindowButton addTarget:self
813                        action:@selector(closePopupWebView)
814              forControlEvents:UIControlEventTouchUpInside];
815
816  [newWebView addSubview:closeWindowButton];
817  closeWindowButton.translatesAutoresizingMaskIntoConstraints = NO;
818  [NSLayoutConstraint activateConstraints:@[
819    [closeWindowButton.topAnchor constraintEqualToAnchor:newWebView.topAnchor
820                                                constant:16.0],
821    [closeWindowButton.centerXAnchor
822        constraintEqualToAnchor:newWebView.centerXAnchor],
823  ]];
824
825  return newWebView;
826}
827
828- (void)webViewDidClose:(CWVWebView*)webView {
829  NSLog(@"webViewDidClose");
830}
831
832- (void)webView:(CWVWebView*)webView
833    runContextMenuWithTitle:(NSString*)menuTitle
834             forHTMLElement:(CWVHTMLElement*)element
835                     inView:(UIView*)view
836        userGestureLocation:(CGPoint)location {
837  if (!element.hyperlink) {
838    return;
839  }
840
841  UIAlertController* alert = [UIAlertController
842      alertControllerWithTitle:menuTitle
843                       message:nil
844                preferredStyle:UIAlertControllerStyleActionSheet];
845  alert.popoverPresentationController.sourceView = view;
846  alert.popoverPresentationController.sourceRect =
847      CGRectMake(location.x, location.y, 1.0, 1.0);
848
849  void (^copyHandler)(UIAlertAction*) = ^(UIAlertAction* action) {
850    NSDictionary* item = @{
851      (NSString*)(kUTTypeURL) : element.hyperlink,
852      (NSString*)(kUTTypeUTF8PlainText) : [[element.hyperlink absoluteString]
853          dataUsingEncoding:NSUTF8StringEncoding],
854    };
855    [[UIPasteboard generalPasteboard] setItems:@[ item ]];
856  };
857  [alert addAction:[UIAlertAction actionWithTitle:@"Copy Link"
858                                            style:UIAlertActionStyleDefault
859                                          handler:copyHandler]];
860
861  [alert addAction:[UIAlertAction actionWithTitle:@"Cancel"
862                                            style:UIAlertActionStyleCancel
863                                          handler:nil]];
864
865  [self presentViewController:alert animated:YES completion:nil];
866}
867
868- (void)webView:(CWVWebView*)webView
869    contextMenuConfigurationForLinkWithURL:(NSURL*)linkURL
870                         completionHandler:
871                             (void (^)(UIContextMenuConfiguration*))
872                                 completionHandler API_AVAILABLE(ios(13.0)) {
873  void (^copyHandler)(UIAction*) = ^(UIAction* action) {
874    NSDictionary* item = @{
875      (NSString*)(kUTTypeURL) : linkURL.absoluteString,
876      (NSString*)(kUTTypeUTF8PlainText) :
877          [linkURL.absoluteString dataUsingEncoding:NSUTF8StringEncoding],
878    };
879    [[UIPasteboard generalPasteboard] setItems:@[ item ]];
880  };
881
882  UIContextMenuConfiguration* configuration = [UIContextMenuConfiguration
883      configurationWithIdentifier:nil
884      previewProvider:^{
885        UIViewController* controller = [[UIViewController alloc] init];
886        CGRect frame = CGRectMake(10, 200, 200, 21);
887        UILabel* label = [[UILabel alloc] initWithFrame:frame];
888        label.text = @"iOS13 Preview Page";
889        [controller.view addSubview:label];
890        return controller;
891      }
892      actionProvider:^(id _) {
893        NSArray* actions = @[
894          [UIAction actionWithTitle:@"Copy Link"
895                              image:nil
896                         identifier:nil
897                            handler:copyHandler],
898          [UIAction actionWithTitle:@"Cancel"
899                              image:nil
900                         identifier:nil
901                            handler:^(id _){
902                            }]
903        ];
904        NSString* menuTitle = [NSString
905            stringWithFormat:@"iOS13 Context Menu: %@", linkURL.absoluteString];
906        return [UIMenu menuWithTitle:menuTitle children:actions];
907      }];
908
909  completionHandler(configuration);
910}
911
912- (void)webView:(CWVWebView*)webView
913    contextMenuWillPresentForLinkWithURL:(NSURL*)linkURL
914    API_AVAILABLE(ios(13.0)) {
915  NSLog(@"webView:contextMenuWillPresentForLinkWithURL: %@",
916        linkURL.absoluteString);
917}
918
919- (void)webView:(CWVWebView*)webView
920    contextMenuForLinkWithURL:(NSURL*)linkURL
921       willCommitWithAnimator:
922           (id<UIContextMenuInteractionCommitAnimating>)animator
923    API_AVAILABLE(ios(13.0)) {
924  NSLog(@"webView:contextMenuForLinkWithURL:willCommitWithAnimator: %@",
925        linkURL.absoluteString);
926}
927
928- (void)webView:(CWVWebView*)webView
929    contextMenuDidEndForLinkWithURL:(NSURL*)linkURL API_AVAILABLE(ios(13.0)) {
930  NSLog(@"webView:contextMenuDidEndForLinkWithURL: %@", linkURL.absoluteString);
931}
932
933- (void)webView:(CWVWebView*)webView
934    runJavaScriptAlertPanelWithMessage:(NSString*)message
935                               pageURL:(NSURL*)URL
936                     completionHandler:(void (^)(void))handler {
937  UIAlertController* alert =
938      [UIAlertController alertControllerWithTitle:nil
939                                          message:message
940                                   preferredStyle:UIAlertControllerStyleAlert];
941
942  [alert addAction:[UIAlertAction actionWithTitle:@"Ok"
943                                            style:UIAlertActionStyleDefault
944                                          handler:^(UIAlertAction* action) {
945                                            handler();
946                                          }]];
947
948  [self presentViewController:alert animated:YES completion:nil];
949}
950
951- (void)webView:(CWVWebView*)webView
952    runJavaScriptConfirmPanelWithMessage:(NSString*)message
953                                 pageURL:(NSURL*)URL
954                       completionHandler:(void (^)(BOOL))handler {
955  UIAlertController* alert =
956      [UIAlertController alertControllerWithTitle:nil
957                                          message:message
958                                   preferredStyle:UIAlertControllerStyleAlert];
959
960  [alert addAction:[UIAlertAction actionWithTitle:@"Ok"
961                                            style:UIAlertActionStyleDefault
962                                          handler:^(UIAlertAction* action) {
963                                            handler(YES);
964                                          }]];
965  [alert addAction:[UIAlertAction actionWithTitle:@"Cancel"
966                                            style:UIAlertActionStyleCancel
967                                          handler:^(UIAlertAction* action) {
968                                            handler(NO);
969                                          }]];
970
971  [self presentViewController:alert animated:YES completion:nil];
972}
973
974- (void)webView:(CWVWebView*)webView
975    runJavaScriptTextInputPanelWithPrompt:(NSString*)prompt
976                              defaultText:(NSString*)defaultText
977                                  pageURL:(NSURL*)URL
978                        completionHandler:(void (^)(NSString*))handler {
979  UIAlertController* alert =
980      [UIAlertController alertControllerWithTitle:nil
981                                          message:prompt
982                                   preferredStyle:UIAlertControllerStyleAlert];
983
984  [alert addTextFieldWithConfigurationHandler:^(UITextField* textField) {
985    textField.text = defaultText;
986    textField.accessibilityIdentifier =
987        kWebViewShellJavaScriptDialogTextFieldAccessibilityIdentifier;
988  }];
989
990  __weak UIAlertController* weakAlert = alert;
991  [alert addAction:[UIAlertAction
992                       actionWithTitle:@"Ok"
993                                 style:UIAlertActionStyleDefault
994                               handler:^(UIAlertAction* action) {
995                                 NSString* textInput =
996                                     weakAlert.textFields.firstObject.text;
997                                 handler(textInput);
998                               }]];
999  [alert addAction:[UIAlertAction actionWithTitle:@"Cancel"
1000                                            style:UIAlertActionStyleCancel
1001                                          handler:^(UIAlertAction* action) {
1002                                            handler(nil);
1003                                          }]];
1004
1005  [self presentViewController:alert animated:YES completion:nil];
1006}
1007
1008- (void)webView:(CWVWebView*)webView
1009    didLoadFavicons:(NSArray<CWVFavicon*>*)favIcons {
1010  NSLog(@"%@", NSStringFromSelector(_cmd));
1011}
1012
1013#pragma mark CWVNavigationDelegate methods
1014
1015- (BOOL)webView:(CWVWebView*)webView
1016    shouldStartLoadWithRequest:(NSURLRequest*)request
1017                navigationType:(CWVNavigationType)navigationType {
1018  NSLog(@"%@", NSStringFromSelector(_cmd));
1019  return YES;
1020}
1021
1022- (BOOL)webView:(CWVWebView*)webView
1023    shouldContinueLoadWithResponse:(NSURLResponse*)response
1024                      forMainFrame:(BOOL)forMainFrame {
1025  NSLog(@"%@", NSStringFromSelector(_cmd));
1026  return YES;
1027}
1028
1029- (void)webViewDidStartProvisionalNavigation:(CWVWebView*)webView {
1030  NSLog(@"%@", NSStringFromSelector(_cmd));
1031  [self updateToolbar];
1032}
1033
1034- (void)webViewDidCommitNavigation:(CWVWebView*)webView {
1035  NSLog(@"%@", NSStringFromSelector(_cmd));
1036  [self updateToolbar];
1037}
1038
1039- (void)webViewDidFinishNavigation:(CWVWebView*)webView {
1040  NSLog(@"%@", NSStringFromSelector(_cmd));
1041  // TODO(crbug.com/679895): Add some visual indication that the page load has
1042  // finished.
1043  [self updateToolbar];
1044}
1045
1046- (void)webView:(CWVWebView*)webView
1047    didFailNavigationWithError:(NSError*)error {
1048  NSLog(@"%@", NSStringFromSelector(_cmd));
1049  [self updateToolbar];
1050}
1051
1052- (void)webViewWebContentProcessDidTerminate:(CWVWebView*)webView {
1053  NSLog(@"%@", NSStringFromSelector(_cmd));
1054}
1055
1056- (BOOL)webView:(CWVWebView*)webView
1057    shouldPreviewElement:(CWVPreviewElementInfo*)elementInfo {
1058  NSLog(@"%@", NSStringFromSelector(_cmd));
1059  return YES;
1060}
1061
1062- (UIViewController*)webView:(CWVWebView*)webView
1063    previewingViewControllerForElement:(CWVPreviewElementInfo*)elementInfo {
1064  NSLog(@"%@", NSStringFromSelector(_cmd));
1065  return nil;
1066}
1067
1068- (void)webView:(CWVWebView*)webView
1069    commitPreviewingViewController:(UIViewController*)previewingViewController {
1070  NSLog(@"%@", NSStringFromSelector(_cmd));
1071}
1072
1073- (void)webView:(CWVWebView*)webView
1074    didFailNavigationWithSSLError:(NSError*)error
1075                      overridable:(BOOL)overridable
1076                  decisionHandler:
1077                      (void (^)(CWVSSLErrorDecision))decisionHandler {
1078  NSLog(@"%@", NSStringFromSelector(_cmd));
1079  decisionHandler(CWVSSLErrorDecisionDoNothing);
1080}
1081
1082- (void)webView:(CWVWebView*)webView
1083    didRequestDownloadWithTask:(CWVDownloadTask*)task {
1084  NSLog(@"%@", NSStringFromSelector(_cmd));
1085  self.downloadTask = task;
1086  NSString* documentDirectoryPath = NSSearchPathForDirectoriesInDomains(
1087      NSDocumentDirectory, NSUserDomainMask, YES)[0];
1088  self.downloadFilePath = [documentDirectoryPath
1089      stringByAppendingPathComponent:task.suggestedFileName];
1090  task.delegate = self;
1091  [task startDownloadToLocalFileAtPath:self.downloadFilePath];
1092}
1093
1094#pragma mark CWVScriptCommandHandler
1095
1096- (BOOL)webView:(CWVWebView*)webView
1097    handleScriptCommand:(nonnull CWVScriptCommand*)command
1098          fromMainFrame:(BOOL)fromMainFrame {
1099  NSLog(@"%@ command.content=%@", NSStringFromSelector(_cmd), command.content);
1100  return YES;
1101}
1102
1103#pragma mark CWVDownloadTaskDelegate
1104
1105- (void)downloadTask:(CWVDownloadTask*)downloadTask
1106    didFinishWithError:(nullable NSError*)error {
1107  NSLog(@"%@", NSStringFromSelector(_cmd));
1108  if (!error) {
1109    NSURL* url = [NSURL fileURLWithPath:self.downloadFilePath];
1110    self.documentInteractionController =
1111        [UIDocumentInteractionController interactionControllerWithURL:url];
1112    [self.documentInteractionController presentOptionsMenuFromRect:CGRectZero
1113                                                            inView:self.view
1114                                                          animated:YES];
1115  }
1116  self.downloadTask = nil;
1117  self.downloadFilePath = nil;
1118}
1119
1120- (void)downloadTaskProgressDidChange:(CWVDownloadTask*)downloadTask {
1121  NSLog(@"%@", NSStringFromSelector(_cmd));
1122}
1123
1124#pragma mark CWVSyncControllerDelegate
1125
1126- (void)syncControllerDidStartSync:(CWVSyncController*)syncController {
1127  NSLog(@"%@", NSStringFromSelector(_cmd));
1128}
1129
1130- (void)syncController:(CWVSyncController*)syncController
1131      didFailWithError:(NSError*)error {
1132  NSLog(@"%@:%@", NSStringFromSelector(_cmd), error);
1133}
1134
1135- (void)syncControllerDidStopSync:(CWVSyncController*)syncController {
1136  NSLog(@"%@", NSStringFromSelector(_cmd));
1137}
1138
1139#pragma mark UIScrollViewDelegate
1140
1141- (void)scrollViewWillBeginDragging:(UIScrollView*)scrollView {
1142  NSLog(@"%@", NSStringFromSelector(_cmd));
1143}
1144
1145@end
1146