blob: 634095883a4a03c4939231743ababf1bbc6473f2 [file] [log] [blame]
sdefresnee65fd872016-12-19 13:38:131// Copyright 2012 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/browser/ui/browser_view_controller.h"
6
7#import <AssetsLibrary/AssetsLibrary.h>
8#import <MobileCoreServices/MobileCoreServices.h>
9#import <PassKit/PassKit.h>
10#import <Photos/Photos.h>
11#import <QuartzCore/QuartzCore.h>
12
13#include <stdint.h>
14#include <cmath>
15#include <memory>
16
17#include "base/base64.h"
18#include "base/command_line.h"
19#include "base/format_macros.h"
20#include "base/i18n/rtl.h"
21#include "base/ios/block_types.h"
22#include "base/ios/ios_util.h"
23#include "base/ios/weak_nsobject.h"
24#include "base/logging.h"
25#include "base/mac/bind_objc_block.h"
26#include "base/mac/bundle_locations.h"
27#include "base/mac/foundation_util.h"
28#include "base/mac/objc_property_releaser.h"
29#import "base/mac/scoped_block.h"
30#import "base/mac/scoped_nsobject.h"
31#include "base/macros.h"
32#include "base/memory/ptr_util.h"
33#include "base/metrics/histogram.h"
34#include "base/metrics/user_metrics.h"
35#include "base/metrics/user_metrics_action.h"
sdefresnee65fd872016-12-19 13:38:1336#include "base/strings/sys_string_conversions.h"
37#include "components/bookmarks/browser/base_bookmark_model_observer.h"
38#include "components/bookmarks/browser/bookmark_model.h"
39#include "components/infobars/core/infobar_manager.h"
40#include "components/prefs/pref_service.h"
41#include "components/reading_list/core/reading_list_switches.h"
42#include "components/reading_list/ios/reading_list_model.h"
43#include "components/search_engines/search_engines_pref_names.h"
44#include "components/search_engines/template_url_service.h"
45#include "components/sessions/core/tab_restore_service_helper.h"
46#include "components/strings/grit/components_strings.h"
47#include "components/toolbar/toolbar_model_impl.h"
48#include "ios/chrome/app/tests_hook.h"
49#include "ios/chrome/browser/bookmarks/bookmark_model_factory.h"
50#include "ios/chrome/browser/browser_state/chrome_browser_state.h"
51#include "ios/chrome/browser/chrome_url_constants.h"
52#include "ios/chrome/browser/chrome_url_util.h"
53#include "ios/chrome/browser/experimental_flags.h"
54#import "ios/chrome/browser/favicon/favicon_loader.h"
55#include "ios/chrome/browser/favicon/ios_chrome_favicon_loader_factory.h"
56#import "ios/chrome/browser/find_in_page/find_in_page_controller.h"
57#import "ios/chrome/browser/find_in_page/find_in_page_model.h"
58#include "ios/chrome/browser/first_run/first_run.h"
59#import "ios/chrome/browser/geolocation/omnibox_geolocation_controller.h"
60#include "ios/chrome/browser/infobars/infobar_container_ios.h"
61#include "ios/chrome/browser/infobars/infobar_container_view.h"
62#import "ios/chrome/browser/metrics/new_tab_page_uma.h"
63#include "ios/chrome/browser/metrics/tab_usage_recorder.h"
64#import "ios/chrome/browser/native_app_launcher/native_app_navigation_controller.h"
65#import "ios/chrome/browser/open_url_util.h"
66#import "ios/chrome/browser/passwords/password_controller.h"
67#import "ios/chrome/browser/payments/payment_request_manager.h"
68#include "ios/chrome/browser/pref_names.h"
69#include "ios/chrome/browser/reading_list/reading_list_model_factory.h"
70#include "ios/chrome/browser/search_engines/template_url_service_factory.h"
71#include "ios/chrome/browser/sessions/ios_chrome_tab_restore_service_factory.h"
72#include "ios/chrome/browser/sessions/tab_restore_service_delegate_impl_ios.h"
73#include "ios/chrome/browser/sessions/tab_restore_service_delegate_impl_ios_factory.h"
74#import "ios/chrome/browser/snapshots/snapshot_cache.h"
75#import "ios/chrome/browser/snapshots/snapshot_overlay.h"
76#import "ios/chrome/browser/snapshots/snapshot_overlay_provider.h"
77#import "ios/chrome/browser/storekit_launcher.h"
78#import "ios/chrome/browser/tabs/tab.h"
79#import "ios/chrome/browser/tabs/tab_dialog_delegate.h"
olivierrobin9ce77b82017-01-12 17:29:1980#import "ios/chrome/browser/tabs/tab_headers_delegate.h"
sdefresnee65fd872016-12-19 13:38:1381#import "ios/chrome/browser/tabs/tab_model.h"
82#import "ios/chrome/browser/tabs/tab_model_observer.h"
83#import "ios/chrome/browser/tabs/tab_snapshotting_delegate.h"
84#import "ios/chrome/browser/ui/activity_services/share_protocol.h"
85#import "ios/chrome/browser/ui/activity_services/share_to_data.h"
86#import "ios/chrome/browser/ui/alert_coordinator/alert_coordinator.h"
87#import "ios/chrome/browser/ui/authentication/re_signin_infobar_delegate.h"
88#import "ios/chrome/browser/ui/background_generator.h"
89#import "ios/chrome/browser/ui/bookmarks/bookmark_interaction_controller.h"
90#import "ios/chrome/browser/ui/browser_container_view.h"
sdefresnee65fd872016-12-19 13:38:1391#import "ios/chrome/browser/ui/browser_view_controller_dependency_factory.h"
92#import "ios/chrome/browser/ui/chrome_web_view_factory.h"
93#import "ios/chrome/browser/ui/commands/UIKit+ChromeExecuteCommand.h"
94#import "ios/chrome/browser/ui/commands/generic_chrome_command.h"
95#include "ios/chrome/browser/ui/commands/ios_command_ids.h"
96#import "ios/chrome/browser/ui/commands/open_url_command.h"
97#import "ios/chrome/browser/ui/commands/reading_list_add_command.h"
98#import "ios/chrome/browser/ui/commands/show_mail_composer_command.h"
99#import "ios/chrome/browser/ui/context_menu/context_menu_coordinator.h"
100#import "ios/chrome/browser/ui/contextual_search/contextual_search_controller.h"
101#import "ios/chrome/browser/ui/contextual_search/contextual_search_mask_view.h"
102#import "ios/chrome/browser/ui/contextual_search/contextual_search_metrics.h"
103#import "ios/chrome/browser/ui/contextual_search/contextual_search_panel_protocols.h"
104#import "ios/chrome/browser/ui/contextual_search/contextual_search_panel_view.h"
105#import "ios/chrome/browser/ui/contextual_search/touch_to_search_permissions_mediator.h"
106#import "ios/chrome/browser/ui/dialogs/dialog_presenter.h"
107#import "ios/chrome/browser/ui/dialogs/java_script_dialog_presenter_impl.h"
108#import "ios/chrome/browser/ui/elements/activity_overlay_coordinator.h"
109#import "ios/chrome/browser/ui/external_file_controller.h"
110#import "ios/chrome/browser/ui/external_file_remover.h"
111#include "ios/chrome/browser/ui/file_locations.h"
112#import "ios/chrome/browser/ui/find_bar/find_bar_controller_ios.h"
113#import "ios/chrome/browser/ui/first_run/welcome_to_chrome_view_controller.h"
114#import "ios/chrome/browser/ui/fullscreen_controller.h"
115#import "ios/chrome/browser/ui/history/tab_history_cell.h"
116#import "ios/chrome/browser/ui/key_commands_provider.h"
117#import "ios/chrome/browser/ui/no_tabs/no_tabs_controller.h"
118#import "ios/chrome/browser/ui/ntp/new_tab_page_controller.h"
119#import "ios/chrome/browser/ui/ntp/recent_tabs/recent_tabs_panel_view_controller.h"
120#include "ios/chrome/browser/ui/omnibox/page_info_model.h"
121#import "ios/chrome/browser/ui/omnibox/page_info_view_controller.h"
122#import "ios/chrome/browser/ui/overscroll_actions/overscroll_actions_controller.h"
123#import "ios/chrome/browser/ui/page_not_available_controller.h"
124#import "ios/chrome/browser/ui/preload_controller.h"
125#import "ios/chrome/browser/ui/preload_controller_delegate.h"
126#import "ios/chrome/browser/ui/print/print_controller.h"
127#import "ios/chrome/browser/ui/qr_scanner/qr_scanner_view_controller.h"
128#import "ios/chrome/browser/ui/reading_list/offline_page_native_content.h"
129#import "ios/chrome/browser/ui/reading_list/reading_list_menu_notifier.h"
130#import "ios/chrome/browser/ui/reading_list/reading_list_view_controller_builder.h"
131#include "ios/chrome/browser/ui/rtl_geometry.h"
132#import "ios/chrome/browser/ui/side_swipe/side_swipe_controller.h"
133#import "ios/chrome/browser/ui/stack_view/card_view.h"
134#import "ios/chrome/browser/ui/stack_view/page_animation_util.h"
135#import "ios/chrome/browser/ui/static_content/static_html_native_content.h"
136#import "ios/chrome/browser/ui/sync/sync_util.h"
137#import "ios/chrome/browser/ui/tab_switcher/tab_switcher_controller.h"
138#import "ios/chrome/browser/ui/tabs/tab_strip_controller.h"
139#import "ios/chrome/browser/ui/toolbar/toolbar_controller.h"
140#include "ios/chrome/browser/ui/toolbar/toolbar_model_delegate_ios.h"
141#include "ios/chrome/browser/ui/toolbar/toolbar_model_ios.h"
142#import "ios/chrome/browser/ui/tools_menu/tools_menu_context.h"
143#import "ios/chrome/browser/ui/tools_menu/tools_menu_view_item.h"
144#import "ios/chrome/browser/ui/tools_menu/tools_popup_controller.h"
145#include "ios/chrome/browser/ui/ui_util.h"
146#import "ios/chrome/browser/ui/uikit_ui_util.h"
147#import "ios/chrome/browser/ui/voice/text_to_speech_player.h"
148#include "ios/chrome/browser/upgrade/upgrade_center.h"
149#import "ios/chrome/browser/web/error_page_content.h"
150#import "ios/chrome/browser/web/passkit_dialog_provider.h"
151#import "ios/chrome/browser/xcallback_parameters.h"
152#import "ios/chrome/common/material_timing.h"
153#include "ios/chrome/grit/ios_chromium_strings.h"
154#include "ios/chrome/grit/ios_strings.h"
155#import "ios/net/request_tracker.h"
156#include "ios/public/provider/chrome/browser/chrome_browser_provider.h"
157#include "ios/public/provider/chrome/browser/ui/app_rating_prompt.h"
158#include "ios/public/provider/chrome/browser/ui/default_ios_web_view_factory.h"
159#import "ios/public/provider/chrome/browser/voice/voice_search_bar.h"
160#import "ios/public/provider/chrome/browser/voice/voice_search_bar_owner.h"
161#include "ios/public/provider/chrome/browser/voice/voice_search_controller.h"
162#include "ios/public/provider/chrome/browser/voice/voice_search_controller_delegate.h"
163#include "ios/public/provider/chrome/browser/voice/voice_search_provider.h"
sdefresnee65fd872016-12-19 13:38:13164#import "ios/web/navigation/crw_session_controller.h"
165#import "ios/web/navigation/crw_session_entry.h"
166#include "ios/web/navigation/navigation_manager_impl.h"
167#include "ios/web/public/active_state_manager.h"
168#include "ios/web/public/image_fetcher/image_data_fetcher.h"
169#include "ios/web/public/navigation_item.h"
170#import "ios/web/public/navigation_manager.h"
171#include "ios/web/public/referrer_util.h"
172#include "ios/web/public/ssl_status.h"
173#include "ios/web/public/url_scheme_util.h"
174#include "ios/web/public/web_client.h"
175#import "ios/web/public/web_state/context_menu_params.h"
176#import "ios/web/public/web_state/crw_web_view_proxy.h"
177#import "ios/web/public/web_state/ui/crw_native_content_provider.h"
178#include "ios/web/public/web_state/web_state.h"
179#import "ios/web/public/web_state/web_state_delegate_bridge.h"
180#include "ios/web/public/web_thread.h"
181#import "ios/web/web_state/ui/crw_web_controller.h"
182#import "net/base/mac/url_conversions.h"
183#include "net/base/registry_controlled_domains/registry_controlled_domain.h"
184#include "net/ssl/ssl_info.h"
185#include "net/url_request/url_request_context_getter.h"
186#include "third_party/google_toolbox_for_mac/src/iPhone/GTMUIImage+Resize.h"
187#include "ui/base/l10n/l10n_util.h"
188#include "ui/base/l10n/l10n_util_mac.h"
189#include "ui/base/page_transition_types.h"
190#include "url/gurl.h"
191
192using base::UserMetricsAction;
193using bookmarks::BookmarkNode;
194
195class BrowserBookmarkModelBridge;
196class InfoBarContainerDelegateIOS;
197
198namespace ios_internal {
199NSString* const kPageInfoWillShowNotification =
200 @"kPageInfoWillShowNotification";
201NSString* const kPageInfoWillHideNotification =
202 @"kPageInfoWillHideNotification";
203NSString* const kLocationBarBecomesFirstResponderNotification =
204 @"kLocationBarBecomesFirstResponderNotification";
205NSString* const kLocationBarResignsFirstResponderNotification =
206 @"kLocationBarResignsFirstResponderNotification";
207} // namespace ios_internal
208
209namespace {
210
211typedef NS_ENUM(NSInteger, ContextMenuHistogram) {
212 // Note: these values must match the ContextMenuOption enum in histograms.xml.
213 ACTION_OPEN_IN_NEW_TAB = 0,
214 ACTION_OPEN_IN_INCOGNITO_TAB = 1,
215 ACTION_COPY_LINK_ADDRESS = 2,
216 ACTION_SAVE_IMAGE = 6,
217 ACTION_OPEN_IMAGE = 7,
218 ACTION_OPEN_IMAGE_IN_NEW_TAB = 8,
219 ACTION_SEARCH_BY_IMAGE = 11,
220 ACTION_OPEN_JAVASCRIPT = 21,
221 ACTION_READ_LATER = 22,
222 NUM_ACTIONS = 23,
223};
224
225void Record(NSInteger action, bool is_image, bool is_link) {
226 if (is_image) {
227 if (is_link) {
228 UMA_HISTOGRAM_ENUMERATION("ContextMenu.SelectedOption.ImageLink", action,
229 NUM_ACTIONS);
230 } else {
231 UMA_HISTOGRAM_ENUMERATION("ContextMenu.SelectedOption.Image", action,
232 NUM_ACTIONS);
233 }
234 } else {
235 UMA_HISTOGRAM_ENUMERATION("ContextMenu.SelectedOption.Link", action,
236 NUM_ACTIONS);
237 }
238}
239
240// Duration to show or hide the No-Tabs UI.
241const NSTimeInterval kNoTabsAnimationDuration = 0.25;
242
243const CGFloat kVoiceSearchBarHeight = 59.0;
244
245// Dimensions to use when downsizing an image for search-by-image.
246const CGFloat kSearchByImageMaxImageArea = 90000.0;
247const CGFloat kSearchByImageMaxImageWidth = 600.0;
248const CGFloat kSearchByImageMaxImageHeight = 400.0;
249
250// The delay, in seconds, after startup before cleaning up the files received
251// from other applications that are not bookmarked nor referenced by an open or
252// recently closed tab.
253const int kExternalFilesCleanupDelaySeconds = 60;
254
255enum HeaderBehaviour {
256 // The header moves completely out of the screen.
257 Hideable = 0,
258 // This header stays on screen and doesn't overlap with the content.
259 Visible,
260 // This header stay on screen and covers part of the content.
261 Overlap
262};
263
264struct HeaderDefinition {
265 // The header view.
266 base::scoped_nsobject<UIView> view;
267 // How to place the view, and its behaviour when the headers move.
268 HeaderBehaviour behaviour;
269 // Reduces the height of a header to adjust for shadows.
270 CGFloat heightAdjustement;
271 // Nudges that particular header up by this number of points.
272 CGFloat inset;
273};
274
275const CGFloat kIPadFindBarOverlap = 11;
276
277bool IsURLAllowedInIncognito(const GURL& url) {
278 // Most URLs are allowed in incognito; the following are exceptions.
279 if (!url.SchemeIs(kChromeUIScheme))
280 return true;
281 std::string url_host = url.host();
282 return url_host != kChromeUIHistoryHost &&
283 url_host != kChromeUIHistoryFrameHost;
284}
285
286// Temporary key to use when storing native controllers vended to tabs before
287// they are added to the tab model.
288NSString* const kNativeControllerTemporaryKey = @"NativeControllerTemporaryKey";
289
sdefresnee65fd872016-12-19 13:38:13290} // anonymous namespace
291
292@interface BrowserViewController ()<AppRatingPromptDelegate,
293 ContextualSearchControllerDelegate,
294 ContextualSearchPanelMotionObserver,
295 CRWNativeContentProvider,
296 CRWWebStateDelegate,
297 DialogPresenterDelegate,
298 FullScreenControllerDelegate,
299 KeyCommandsPlumbing,
300 MFMailComposeViewControllerDelegate,
301 NewTabPageControllerObserver,
302 OverscrollActionsControllerDelegate,
303 PassKitDialogProvider,
304 PreloadControllerDelegate,
305 ShareToDelegate,
306 SKStoreProductViewControllerDelegate,
307 SnapshotOverlayProvider,
308 StoreKitLauncher,
309 TabDialogDelegate,
olivierrobin9ce77b82017-01-12 17:29:19310 TabHeadersDelegate,
sdefresnee65fd872016-12-19 13:38:13311 TabModelObserver,
312 TabSnapshottingDelegate,
313 UIGestureRecognizerDelegate,
314 UpgradeCenterClientProtocol,
315 VoiceSearchBarDelegate,
316 VoiceSearchBarOwner> {
317 // The dependency factory passed on initialization. Used to vend objects used
318 // by the BVC.
319 base::scoped_nsobject<BrowserViewControllerDependencyFactory>
320 _dependencyFactory;
321
322 // The browser's tab model.
323 base::scoped_nsobject<TabModel> _model;
324
325 // Facade objects used by |_toolbarController|.
326 // Must outlive |_toolbarController|.
327 std::unique_ptr<ToolbarModelDelegateIOS> _toolbarModelDelegate;
328 std::unique_ptr<ToolbarModelIOS> _toolbarModelIOS;
329
330 // Preload controller. Must outlive |_toolbarController|.
331 base::scoped_nsobject<PreloadController> _preloadController;
332
333 // The WebToolbarController used to display the omnibox.
334 base::scoped_nsobject<WebToolbarController> _toolbarController;
335
336 // Controller for edge swipe gestures for page and tab navigation.
337 base::scoped_nsobject<SideSwipeController> _sideSwipeController;
338
339 // Handles displaying the context menu for all form factors.
340 base::scoped_nsobject<ContextMenuCoordinator> _contextMenuCoordinator;
341
342 // Backing object for property of the same name.
343 base::scoped_nsobject<DialogPresenter> _dialogPresenter;
344
345 // Handles presentation of JavaScript dialogs.
346 std::unique_ptr<JavaScriptDialogPresenterImpl> _javaScriptDialogPresenter;
347
348 // Keyboard commands provider. It offloads most of the keyboard commands
349 // management off of the BVC.
350 base::scoped_nsobject<KeyCommandsProvider> _keyCommandsProvider;
351
352 // Calls to |-relinquishedToolbarController| will set this to yes, and calls
353 // to |-reparentToolbarController| will reset it to NO.
354 BOOL _isToolbarControllerRelinquished;
355
356 // The controller that owns the currently relinquished toolbar controller.
357 // The reference is weak because it's possible for the toolbar owner to be
358 // deallocated mid-animation due to memory pressure or a tab being closed
359 // before the animation is finished.
360 base::WeakNSObject<id> _relinquishedToolbarOwner;
361
362 // Always present on tablet; always nil on phone.
363 base::scoped_nsobject<TabStripController> _tabStripController;
364
365 // The contextual search controller.
366 base::scoped_nsobject<ContextualSearchController> _contextualSearchController;
367
368 // The contextual search panel (always a subview of |self.view| if it exists).
369 ContextualSearchPanelView* _contextualSearchPanel;
370
371 // The contextual search mask (always a subview of |self.view| if it exists).
372 ContextualSearchMaskView* _contextualSearchMask;
373
374 // Used to inject Javascript implementing the PaymentRequest API and to
375 // display the UI.
376 base::scoped_nsobject<PaymentRequestManager> _paymentRequestManager;
377
378 // Used to display the Page Info UI. Nil if not visible.
379 base::scoped_nsobject<PageInfoViewController> _pageInfoController;
380
381 // Used to display the Voice Search UI. Nil if not visible.
382 scoped_refptr<VoiceSearchController> _voiceSearchController;
383
384 // Used to display the QR Scanner UI. Nil if not visible.
385 base::scoped_nsobject<QRScannerViewController> _qrScannerViewController;
386
387 // Used to display the Find In Page UI. Nil if not visible.
388 base::scoped_nsobject<FindBarControllerIOS> _findBarController;
389
390 // Used to display the No-Tabs UI for iPads. Nil if not visible.
391 base::scoped_nsobject<NoTabsController> _noTabsController;
392
393 // Used to display the Print UI. Nil if not visible.
394 base::scoped_nsobject<PrintController> _printController;
395
396 // Records the set of domains for which full screen alert has already been
397 // shown.
398 base::scoped_nsobject<NSMutableSet> _fullScreenAlertShown;
399
400 // Adapter to let BVC be the delegate for WebState.
401 std::unique_ptr<web::WebStateDelegateBridge> _webStateDelegate;
402
403 // YES if new tab is animating in.
404 BOOL _inNewTabAnimation;
405
406 // YES if Voice Search should be started when the new tab animation is
407 // finished.
408 BOOL _startVoiceSearchAfterNewTabAnimation;
409
410 // YES if the user interacts with the location bar.
411 BOOL _locationBarHasFocus;
412 // YES if a load was cancelled due to typing in the location bar.
413 BOOL _locationBarEditCancelledLoad;
414 // YES if waiting for a foreground tab due to expectNewForegroundTab.
415 BOOL _expectingForegroundTab;
416
417 // The ChromeBrowserState associated with this BVC.
418 ios::ChromeBrowserState* _browserState; // weak
419
420 // Whether or not Incognito* is enabled.
421 BOOL _isOffTheRecord;
422
423 // The last point within |_contentArea| that's received a touch.
424 CGPoint _lastTapPoint;
425
426 // The time at which |_lastTapPoint| was most recently set.
427 CFTimeInterval _lastTapTime;
428
429 // A single infobar container handles all infobars in all tabs. It keeps
430 // track of infobars for current tab (accessed via infobar helper of
431 // the current tab).
432 std::unique_ptr<InfoBarContainerIOS> _infoBarContainer;
433
434 // Bridge class to deliver container change notifications to BVC.
435 std::unique_ptr<InfoBarContainerDelegateIOS> _infoBarContainerDelegate;
436
437 // Voice search bar at the bottom of the view overlayed on |_contentArea|
kkhorimotoc2cdf6f42017-01-24 21:37:37438 // when displaying voice search results.
sdefresnee65fd872016-12-19 13:38:13439 base::scoped_nsprotocol<UIView<VoiceSearchBar>*> _voiceSearchBar;
440
441 // The image fetcher used to save images and perform image-based searches.
442 std::unique_ptr<web::ImageDataFetcher> _imageFetcher;
443
444 // Card side swipe view.
445 base::scoped_nsobject<CardSideSwipeView> _sideSwipeView;
446
447 // Used to cache value of |hasModeToggleSwitch| if set before the tab strip
448 // controller has been created.
449 BOOL _modeToggleNeedsSetting;
450
451 // Dominant color cache. Key: (NSString*)url, val: (UIColor*)dominantColor.
452 base::scoped_nsobject<NSMutableDictionary> _dominantColorCache;
453
454 // Bridge to register for bookmark changes.
455 std::unique_ptr<BrowserBookmarkModelBridge> _bookmarkModelBridge;
456
457 // Cached pointer to the bookmarks model.
458 bookmarks::BookmarkModel* _bookmarkModel; // weak
459
460 // The controller that shows the bookmarking UI after the user taps the star
461 // button.
462 base::scoped_nsobject<BookmarkInteractionController>
463 _bookmarkInteractionController;
464
465 // Used to remove unreferenced external files.
466 std::unique_ptr<ExternalFileRemover> _externalFileRemover;
467
468 // The currently displayed "Rate This App" dialog, if one exists.
469 base::scoped_nsprotocol<id<AppRatingPrompt>> _rateThisAppDialog;
470
471 // Maps tab IDs to the most recent native content controller vended to that
472 // tab's web controller.
473 base::scoped_nsobject<NSMapTable> _nativeControllersForTabIDs;
474
475 // Notifies the toolbar menu of reading list changes.
476 base::scoped_nsobject<ReadingListMenuNotifier> _readingListMenuNotifier;
477
478 // The sender for the last received IDC_VOICE_SEARCH command.
479 base::WeakNSObject<UIView> _voiceSearchButton;
480
481 // Coordinator for displaying alerts.
482 base::scoped_nsobject<AlertCoordinator> _alertCoordinator;
483
484 // Releaser for properties that aren't backed by scoped_nsobjects.
485 base::mac::ObjCPropertyReleaser _propertyReleaser_BrowserViewController;
486}
487
488// The browser's side swipe controller. Lazily instantiated on the first call.
489@property(nonatomic, retain, readonly) SideSwipeController* sideSwipeController;
490// The browser's preload controller.
491@property(nonatomic, retain, readonly) PreloadController* preloadController;
492// The dialog presenter for this BVC's tab model.
493@property(nonatomic, retain, readonly) DialogPresenter* dialogPresenter;
494// The object that manages keyboard commands on behalf of the BVC.
495@property(nonatomic, retain, readonly) KeyCommandsProvider* keyCommandsProvider;
496// Whether the current tab can enable the reader mode menu item.
497@property(nonatomic, assign, readonly) BOOL canUseReaderMode;
498// Whether the current tab can enable the request desktop menu item.
499@property(nonatomic, assign, readonly) BOOL canUseDesktopUserAgent;
500// Whether the sharing menu should be enabled.
501@property(nonatomic, assign, readonly) BOOL canShowShareMenu;
502// Helper method to check web controller canShowFindBar method.
503@property(nonatomic, assign, readonly) BOOL canShowFindBar;
504// Whether the controller's view is currently available.
505// YES from viewWillAppear to viewWillDisappear.
506@property(nonatomic, assign, getter=isVisible) BOOL visible;
507// Whether the controller's view is currently visible.
508// YES from viewDidAppear to viewWillDisappear.
509@property(nonatomic, assign) BOOL viewVisible;
510// Whether the controller is currently dismissing a presented view controller.
511@property(nonatomic, assign, getter=isDismissingModal) BOOL dismissingModal;
512// Returns YES if the toolbar has not been scrolled out by fullscreen.
513@property(nonatomic, assign, readonly, getter=isToolbarOnScreen)
514 BOOL toolbarOnScreen;
515// Whether a new tab animation is occurring.
516@property(nonatomic, assign, readonly, getter=isInNewTabAnimation)
517 BOOL inNewTabAnimation;
518// Whether BVC prefers to hide the status bar. This value is used to determine
519// the response from the |prefersStatusBarHidden| method.
520@property(nonatomic, assign) BOOL hideStatusBar;
521// Whether the VoiceSearchBar should be displayed.
522@property(nonatomic, readonly) BOOL shouldShowVoiceSearchBar;
523// Coordinator for displaying a modal overlay with activity indicator to prevent
524// the user from interacting with the browser view.
525@property(nonatomic, retain)
526 ActivityOverlayCoordinator* activityOverlayCoordinator;
527
528// BVC initialization:
529// If the BVC is initialized with a valid browser state & tab model immediately,
530// the path is straightforward: functionality is enabled, and the UI is built
531// when -viewDidLoad is called.
532// If the BVC is initialized without a browser state or tab model, the tab model
533// and browser state may or may not be provided before -viewDidLoad is called.
534// In most cases, they will not, to improve startup performance.
535// In order to handle this, initialization of various aspects of BVC have been
536// broken out into the following functions, which have expectations (enforced
537// with DCHECKs) regarding |_browserState|, |_model|, and [self isViewLoaded].
538
539// Registers for notifications.
540- (void)registerForNotifications;
541// Called when a tab is starting to load. If it's a link click or form
542// submission, the user is navigating away from any entries in the forward
543// history. Tell the toolbar so it can update the UI appropriately.
544// See the warning on [Tab webWillStartLoadingURL] about invocation of this
545// method sequence by malicious pages.
546- (void)pageLoadStarting:(NSNotification*)notify;
547// Called when a tab actually starts loading.
548- (void)pageLoadStarted:(NSNotification*)notify;
549// Called when a tab finishes loading. Update the Omnibox with the url and
550// stop any page load progess display.
551- (void)pageLoadComplete:(NSNotification*)notify;
552// Called when a tab is deselected in the model.
553// This notification also occurs when a tab is closed.
554- (void)tabDeselected:(NSNotification*)notify;
555// Animates sliding current tab and rotate-entering new tab while new tab loads
556// in background on the iPhone only.
557- (void)tabWasAdded:(NSNotification*)notify;
558
559// Updates non-view-related functionality with the given browser state and tab
560// model.
561// Does not matter whether or not the view has been loaded.
562- (void)updateWithTabModel:(TabModel*)model
563 browserState:(ios::ChromeBrowserState*)browserState;
564// On iOS7, iPad should match iOS6 status bar. Install a simple black bar under
565// the status bar to mimic this layout.
566- (void)installFakeStatusBar;
567// Builds the UI parts of tab strip and the toolbar. Does not matter whether
568// or not browser state and tab model are valid.
569- (void)buildToolbarAndTabStrip;
570// Updates view-related functionality with the given tab model and browser
571// state. The view must have been loaded. Uses |_browserState| and |_model|.
572- (void)addUIFunctionalityForModelAndBrowserState;
573// Sets the correct frame and heirarchy for subviews and helper views.
574- (void)setUpViewLayout;
575// Sets the correct frame for the tab strip based on the given maximum width.
576- (void)layoutTabStripForWidth:(CGFloat)maxWidth;
577// Makes |tab| the currently visible tab, displaying its view. Calls
578// -selectedTabChanged on the toolbar only if |newSelection| is YES.
579- (void)displayTab:(Tab*)tab isNewSelection:(BOOL)newSelection;
580// Initializes the bookmark interaction controller if not already initialized.
581- (void)initializeBookmarkInteractionController;
582
583// Shows the No-Tabs UI with animation.
584- (void)showNoTabsUI;
585// Dismisses the No-Tabs UI with animation.
586- (void)dismissNoTabsUI;
587
588// Shows the tools menu popup.
589- (void)showToolsMenuPopup;
590// Add all delegates to the provided |tab|.
591- (void)installDelegatesForTab:(Tab*)tab;
592// Closes the current tab, with animation if applicable.
593- (void)closeCurrentTab;
594// Returns an autoreleased share to data for |tab|.
595- (ShareToData*)shareToDataForTab:(Tab*)tab;
596// Shows the menu to initiate sharing |data|.
597- (void)sharePageWithData:(ShareToData*)data;
598// Convenience method to share the current page.
599- (void)sharePage;
600// Prints the web page in the current tab.
601- (void)print;
602// Shows the Online Help Page in a tab.
603- (void)showHelpPage;
604// Show the bookmarks page.
605- (void)showAllBookmarks;
606// Shows a panel within the New Tab Page.
607- (void)showNTPPanel:(NewTabPage::PanelIdentifier)panel;
608// Shows the "rate this app" dialog.
609- (void)showRateThisAppDialog;
610// Dismisses the "rate this app" dialog.
611- (void)dismissRateThisAppDialog;
612#if !defined(NDEBUG)
613// Shows the source of the current page.
614- (void)viewSource;
615#endif
616// Whether the given tab's url begins with the chrome prefix.
617- (BOOL)isTabNativePage:(Tab*)tab;
618// Returns the view to use when animating a page in or out, positioning it to
619// fill the content area but not actually adding it to the view hierarchy.
620- (UIImageView*)pageOpenCloseAnimationView;
621// Returns the view to use when animating full screen NTP paper in, filling the
622// entire screen but not actually adding it to the view hierarchy.
623- (UIImageView*)pageFullScreenOpenCloseAnimationView;
624// Updates the toolbar display based on the current tab.
625- (void)updateToolbar;
626// Updates |dialogPresenter|'s |active| property to account for the BVC's
627// |active| and |visible| properties.
628- (void)updateDialogPresenterActiveState;
629// Dismisses popups and modal dialogs that are displayed above the BVC upon size
630// changes (e.g. rotation, resizing,…) or when the accessibility escape gesture
631// is performed.
632// TODO(crbug.com/522721): Support size changes for all popups and modal
633// dialogs.
634- (void)dismissPopups;
635// Create and show the find bar.
636- (void)initFindBarForTab;
637// Search for find bar query string.
638- (void)searchFindInPage;
639// Update find bar with model data. If |shouldFocus| is set to YES, the text
640// field will become first responder.
641- (void)updateFindBar:(BOOL)initialUpdate shouldFocus:(BOOL)shouldFocus;
642// Close and disable find in page bar.
643- (void)closeFindInPage;
644// Hide find bar.
645- (void)hideFindBarWithAnimation:(BOOL)animate;
646// Shows find bar. If |selectText| is YES, all text inside the Find Bar
647// textfield will be selected. If |shouldFocus| is set to YES, the textfield is
648// set to be first responder.
649- (void)showFindBarWithAnimation:(BOOL)animate
650 selectText:(BOOL)selectText
651 shouldFocus:(BOOL)shouldFocus;
652// Show the Page Security Info.
653- (void)showPageInfoPopupForView:(UIView*)sourceView;
654// Hide the Page Security Info.
655- (void)hidePageInfoPopupForView:(UIView*)sourceView;
656// Shows the tab history popup containing the tab's backward history.
657- (void)showTabHistoryPopupForBackwardHistory;
658// Shows the tab history popup containing the tab's forward history.
659- (void)showTabHistoryPopupForForwardHistory;
660// Navigate back/forward to the selected entry in the tab's history.
661- (void)navigateToSelectedEntry:(id)sender;
662// The infobar state (typically height) has changed.
663- (void)infoBarContainerStateChanged:(bool)is_animating;
664// Adds a CardView on top of the contentArea either taking the size of the full
665// screen or just the size of the space under the header.
666// Returns the CardView that was added.
667- (CardView*)addCardViewInFullscreen:(BOOL)fullScreen;
668// Called when either a tab finishes loading or when a tab with finished content
669// is added directly to the model via pre-rendering. The tab must be non-nil and
670// must be a member of the tab model controlled by this BrowserViewController.
671- (void)tabLoadComplete:(Tab*)tab withSuccess:(BOOL)success;
672// Evaluates Javascript asynchronously using the current page context.
673- (void)openJavascript:(NSString*)javascript;
674// Sets the desktop user agent flag and reload the current page.
675- (void)enableDesktopUserAgent;
676// Helper methods used by ShareToDelegate methods.
677// Shows an alert with the given title and message id.
678- (void)showErrorAlert:(int)titleMessageId message:(int)messageId;
679// Helper method displaying an alert with the given title and message.
680// Dismisses previous alert if it has not been dismissed yet.
681- (void)showErrorAlertWithStringTitle:(NSString*)title
682 message:(NSString*)message;
683// Shows a self-dismissing snackbar displaying |message|.
684- (void)showSnackbar:(NSString*)message;
685// Induces an intentional crash in the browser process.
686- (void)induceBrowserCrash;
eugenebut75a06fa72017-01-09 17:09:55687// Returns Tab that corresponds to the given |webState|.
688- (Tab*)tabForWebState:(web::WebState*)webState;
sdefresnee65fd872016-12-19 13:38:13689// Saves the image or display error message, based on privacy settings.
gambard16cfe1f2016-12-21 13:12:09690- (void)managePermissionAndSaveImage:(NSData*)data;
sdefresnee65fd872016-12-19 13:38:13691// Saves the image. In order to keep the metadata of the image, the image is
692// saved as a temporary file on disk then saved in photos.
693// This should be called on FILE thread.
gambard16cfe1f2016-12-21 13:12:09694- (void)saveImage:(NSData*)data;
sdefresnee65fd872016-12-19 13:38:13695// Called when Chrome has been denied access to the photos or videos and the
696// user can change it.
697// Shows a privacy alert on the main queue, allowing the user to go to Chrome's
698// settings. Dismiss previous alert if it has not been dismissed yet.
699- (void)displayImageErrorAlertWithSettingsOnMainQueue;
700// Shows a privacy alert allowing the user to go to Chrome's settings. Dismiss
701// previous alert if it has not been dismissed yet.
702- (void)displayImageErrorAlertWithSettings:(NSURL*)settingURL;
703// Called when Chrome has been denied access to the photos or videos and the
704// user cannot change it.
705// Shows a privacy alert on the main queue, with errorContent as the message.
706// Dismisses previous alert if it has not been dismissed yet.
707- (void)displayPrivacyErrorAlertOnMainQueue:(NSString*)errorContent;
708// Called with the results of saving a picture in the photo album. If error is
709// nil the save succeeded.
710- (void)finishSavingImageWithError:(NSError*)error;
711// Provides a view that encompasses currently displayed infobar(s) or nil
712// if no infobar is presented.
713- (UIView*)infoBarOverlayViewForTab:(Tab*)tab;
714// Returns a vertical infobar offset relative to the tab content.
715- (CGFloat)infoBarOverlayYOffsetForTab:(Tab*)tab;
716// Provides a view that encompasses the voice search bar if it's displayed or
717// nil if the voice search bar isn't displayed.
718- (UIView*)voiceSearchOverlayViewForTab:(Tab*)tab;
719// Returns a vertical voice search bar offset relative to the tab content.
720- (CGFloat)voiceSearchOverlayYOffsetForTab:(Tab*)tab;
721// Lazily instantiates |_voiceSearchController|.
722- (void)ensureVoiceSearchControllerCreated;
723// Lazily instantiates |_voiceSearchBar| and adds it to the view.
724- (void)ensureVoiceSearchBarCreated;
725// Shows/hides the voice search bar.
726- (void)updateVoiceSearchBarVisibilityAnimated:(BOOL)animated;
727// The LogoAnimationControllerOwner to be used for the next logo transition
728// animation.
729- (id<LogoAnimationControllerOwner>)currentLogoAnimationControllerOwner;
730// Returns the header views, all the chrome on top of the page, including the
731// ones that cannot be scrolled off screen by full screen.
732- (const std::vector<HeaderDefinition>)headerViews;
733// Returns the footer view if one exists (e.g. the voice search bar).
734- (UIView*)footerView;
735// Returns the height of the header view for the tab model's current tab.
736- (CGFloat)headerHeight;
sdefresnee65fd872016-12-19 13:38:13737// Sets the frame for the headers.
738- (void)setFramesForHeaders:(const std::vector<HeaderDefinition>)headers
739 atOffset:(CGFloat)headerOffset;
740// Returns the y coordinate for the footer's frame when animating the footer
741// in/out of fullscreen.
742- (CGFloat)footerYForHeaderOffset:(CGFloat)headerOffset;
743// Called when the animation for setting the header view's offset is finished.
744// |completed| should indicate if the animation finished completely or was
745// interrupted. |offset| should indicate the header offset after the animation.
746// |dragged| should indicate if the header moved due to the user dragging.
747- (void)fullScreenController:(FullScreenController*)controller
748 headerAnimationCompleted:(BOOL)completed
749 offset:(CGFloat)offset;
750// Performs a search with the image at the given url. The referrer is used to
751// download the image.
752- (void)searchByImageAtURL:(const GURL&)url
753 referrer:(const web::Referrer)referrer;
754// Saves the image at the given URL on the system's album. The referrer is used
755// to download the image.
756- (void)saveImageAtURL:(const GURL&)url referrer:(const web::Referrer&)referrer;
757
758// Determines the center of |sender| if it's a view or a toolbar item, and save
759// the CGPoint and timestamp.
760- (void)setLastTapPoint:(id)sender;
761// Get return the last stored |_lastTapPoint| if it's been set within the past
762// second.
763- (CGPoint)lastTapPoint;
764// Store the tap CGPoint in |_lastTapPoint| and the current timestamp.
765- (void)saveContentAreaTapLocation:(UIGestureRecognizer*)gestureRecognizer;
766// Returns the native controller being used by |tab|'s web controller.
767- (id)nativeControllerForTab:(Tab*)tab;
768// Installs the BVC as overscroll actions controller of |nativeContent| if
769// needed. Sets the style of the overscroll actions toolbar.
770- (void)setOverScrollActionControllerToStaticNativeContent:
771 (StaticHtmlNativeContent*)nativeContent;
772// Whether the BVC should declare keyboard commands.
773- (BOOL)shouldRegisterKeyboardCommands;
774// Adds the given url to the reading list.
775- (void)addToReadingListURL:(const GURL&)URL title:(NSString*)title;
776@end
777
778class InfoBarContainerDelegateIOS
779 : public infobars::InfoBarContainer::Delegate {
780 public:
781 explicit InfoBarContainerDelegateIOS(BrowserViewController* controller)
782 : controller_(controller) {}
783
784 ~InfoBarContainerDelegateIOS() override {}
785
786 private:
787 SkColor GetInfoBarSeparatorColor() const override {
788 NOTIMPLEMENTED();
789 return SK_ColorBLACK;
790 }
791
792 int ArrowTargetHeightForInfoBar(
793 size_t index,
794 const gfx::SlideAnimation& animation) const override {
795 return 0;
796 }
797
798 void ComputeInfoBarElementSizes(const gfx::SlideAnimation& animation,
799 int arrow_target_height,
800 int bar_target_height,
801 int* arrow_height,
802 int* arrow_half_width,
803 int* bar_height) const override {
804 DCHECK_NE(-1, bar_target_height)
805 << "Infobars don't have a default height on iOS";
806 *arrow_height = 0;
807 *arrow_half_width = 0;
808 *bar_height = animation.CurrentValueBetween(0, bar_target_height);
809 }
810
811 void InfoBarContainerStateChanged(bool is_animating) override {
812 [controller_ infoBarContainerStateChanged:is_animating];
813 }
814
815 bool DrawInfoBarArrows(int* x) const override { return false; }
816
817 BrowserViewController* controller_; // weak
818};
819
820// Called from the BrowserBookmarkModelBridge from C++ -> ObjC.
821@interface BrowserViewController (BookmarkBridgeMethods)
822// If a bookmark matching the currentTab url is added or moved, update the
823// toolbar state so the star highlight is in sync.
824- (void)bookmarkNodeModified:(const BookmarkNode*)node;
825- (void)allBookmarksRemoved;
826@end
827
828// Handle notification that bookmarks has been removed changed so we can update
829// the bookmarked star icon.
830class BrowserBookmarkModelBridge : public bookmarks::BookmarkModelObserver {
831 public:
832 explicit BrowserBookmarkModelBridge(BrowserViewController* owner)
833 : owner_(owner) {}
834
835 ~BrowserBookmarkModelBridge() override {}
836
837 void BookmarkNodeRemoved(bookmarks::BookmarkModel* model,
838 const BookmarkNode* parent,
839 int old_index,
840 const BookmarkNode* node,
841 const std::set<GURL>& removed_urls) override {
842 [owner_ bookmarkNodeModified:node];
843 }
844
845 void BookmarkModelLoaded(bookmarks::BookmarkModel* model,
846 bool ids_reassigned) override {}
847
848 void BookmarkNodeMoved(bookmarks::BookmarkModel* model,
849 const BookmarkNode* old_parent,
850 int old_index,
851 const BookmarkNode* new_parent,
852 int new_index) override {}
853
854 void BookmarkNodeAdded(bookmarks::BookmarkModel* model,
855 const BookmarkNode* parent,
856 int index) override {
857 [owner_ bookmarkNodeModified:parent->GetChild(index)];
858 }
859
860 void BookmarkNodeChanged(bookmarks::BookmarkModel* model,
861 const BookmarkNode* node) override {}
862
863 void BookmarkNodeFaviconChanged(bookmarks::BookmarkModel* model,
864 const BookmarkNode* node) override {}
865
866 void BookmarkNodeChildrenReordered(bookmarks::BookmarkModel* model,
867 const BookmarkNode* node) override {}
868
869 void BookmarkAllUserNodesRemoved(
870 bookmarks::BookmarkModel* model,
871 const std::set<GURL>& removed_urls) override {
872 [owner_ allBookmarksRemoved];
873 }
874
875 private:
876 BrowserViewController* owner_; // Weak.
877};
878
879@implementation BrowserViewController
880
881@synthesize contentArea = _contentArea;
882@synthesize typingShield = _typingShield;
883@synthesize active = _active;
884@synthesize visible = _visible;
885@synthesize viewVisible = _viewVisible;
886@synthesize dismissingModal = _dismissingModal;
887@synthesize hideStatusBar = _hideStatusBar;
888@synthesize activityOverlayCoordinator = _activityOverlayCoordinator;
889@synthesize presenting = _presenting;
890
891#pragma mark - Object lifecycle
892
893- (instancetype)initWithTabModel:(TabModel*)model
894 browserState:(ios::ChromeBrowserState*)browserState
895 dependencyFactory:
896 (BrowserViewControllerDependencyFactory*)factory {
897 self = [super initWithNibName:nil bundle:base::mac::FrameworkBundle()];
898 if (self) {
899 DCHECK(factory);
900 _propertyReleaser_BrowserViewController.Init(self,
901 [BrowserViewController class]);
902 _imageFetcher = base::MakeUnique<web::ImageDataFetcher>(
903 web::WebThread::GetBlockingPool());
904 _dependencyFactory.reset([factory retain]);
905 _nativeControllersForTabIDs.reset(
906 [[NSMapTable strongToWeakObjectsMapTable] retain]);
907 _dialogPresenter.reset([[DialogPresenter alloc] initWithDelegate:self
908 presentingViewController:self]);
909 _javaScriptDialogPresenter.reset(
910 new JavaScriptDialogPresenterImpl(_dialogPresenter));
911 _webStateDelegate.reset(new web::WebStateDelegateBridge(self));
912 // TODO(leng): Delay this.
913 [[UpgradeCenter sharedInstance] registerClient:self];
914 _inNewTabAnimation = NO;
sdefresnee65fd872016-12-19 13:38:13915 if (model && browserState)
916 [self updateWithTabModel:model browserState:browserState];
917 if ([[NSUserDefaults standardUserDefaults]
918 boolForKey:@"fullScreenShowAlert"]) {
919 _fullScreenAlertShown.reset([[NSMutableSet alloc] init]);
920 }
921 }
922 return self;
923}
924
925- (instancetype)initWithNibName:(NSString*)nibNameOrNil
926 bundle:(NSBundle*)nibBundleOrNil {
927 NOTREACHED();
928 return nil;
929}
930
931- (instancetype)initWithCoder:(NSCoder*)aDecoder {
932 NOTREACHED();
933 return nil;
934}
935
936- (void)dealloc {
937 _tabStripController.reset();
938 _infoBarContainer.reset();
939 _readingListMenuNotifier.reset();
sdefresnedc432f42017-01-17 14:36:59940 if (_bookmarkModel)
941 _bookmarkModel->RemoveObserver(_bookmarkModelBridge.get());
sdefresnee65fd872016-12-19 13:38:13942 [_model removeObserver:self];
943 [[UpgradeCenter sharedInstance] unregisterClient:self];
944 [[NSNotificationCenter defaultCenter] removeObserver:self];
945 [_toolbarController setDelegate:nil];
946 if (_voiceSearchController.get())
947 _voiceSearchController->SetDelegate(nil);
948 [_rateThisAppDialog setDelegate:nil];
949 [_model closeAllTabs];
950 [super dealloc];
951}
952
953#pragma mark - Accessibility
954
955- (BOOL)accessibilityPerformEscape {
956 [self dismissPopups];
957 return YES;
958}
959
960#pragma mark - Properties
961
962// Implements |hasModeToggleSwitch| property as pass-throughs to tab strip
963// controller and no-tabs controller. If set before the controller has been
964// created, cache it.
965- (void)setHasModeToggleSwitch:(BOOL)hasModeToggleSwitch {
966 if (!experimental_flags::IsTabSwitcherEnabled()) {
967 if (_tabStripController)
968 _tabStripController.get().hasModeToggleSwitch = hasModeToggleSwitch;
969 else
970 _modeToggleNeedsSetting = hasModeToggleSwitch;
971 }
972 [_noTabsController setHasModeToggleSwitch:hasModeToggleSwitch];
973}
974
975// Implements |hasModeToggleSwitch| property as pass-throughs to tab strip
976// controller, unless it hasn't been created in which return the cached version.
977- (BOOL)hasModeToggleSwitch {
978 if (_tabStripController)
979 return _tabStripController.get().hasModeToggleSwitch;
980 return _modeToggleNeedsSetting;
981}
982
983- (void)setActive:(BOOL)active {
984 if (_active == active) {
985 return;
986 }
987 _active = active;
988
989 // If not active, display an activity indicator overlay over the view to
990 // prevent interaction with the web page.
991 // TODO(crbug.com/637093): This coordinator should be managed by the
992 // coordinator used to present BrowserViewController, when implemented.
993 if (active) {
994 [self.activityOverlayCoordinator stop];
995 self.activityOverlayCoordinator = nil;
996 } else if (!self.activityOverlayCoordinator) {
997 self.activityOverlayCoordinator = [[[ActivityOverlayCoordinator alloc]
998 initWithBaseViewController:self] autorelease];
999 [self.activityOverlayCoordinator start];
1000 }
1001
1002 if (_browserState) {
1003 web::ActiveStateManager* active_state_manager =
1004 web::BrowserState::GetActiveStateManager(_browserState);
1005 active_state_manager->SetActive(active);
1006 }
1007
1008 [_model setWebUsageEnabled:active];
1009 [self updateDialogPresenterActiveState];
1010
1011 if (active) {
1012 // Make sure the tab (if any; it's possible to get here without a current
1013 // tab if the caller is about to create one) ends up on screen completely.
1014 Tab* currentTab = [_model currentTab];
1015 // Force loading the view in case it was not loaded yet.
1016 [self ensureViewCreated];
1017 if (_expectingForegroundTab)
1018 [currentTab.webController setOverlayPreviewMode:YES];
1019 if (currentTab)
1020 [self displayTab:currentTab isNewSelection:YES];
1021 }
1022 [_contextualSearchController enableContextualSearch:active];
1023 [_paymentRequestManager enablePaymentRequest:active];
1024
1025 [self setNeedsStatusBarAppearanceUpdate];
1026}
1027
1028- (void)setPrimary:(BOOL)primary {
1029 [_model setPrimary:primary];
1030 if (primary) {
1031 [self updateDialogPresenterActiveState];
1032 } else {
1033 self.dialogPresenter.active = false;
1034 }
1035}
1036
1037- (BOOL)isPlayingTTS {
1038 return _voiceSearchController && _voiceSearchController->IsPlayingAudio();
1039}
1040
sdefresne6165c8742017-01-16 15:42:021041- (ios::ChromeBrowserState*)browserState {
1042 return _browserState;
1043}
1044
1045- (TabModel*)tabModel {
1046 return _model.get();
1047}
1048
sdefresnee65fd872016-12-19 13:38:131049- (SideSwipeController*)sideSwipeController {
1050 if (!_sideSwipeController) {
1051 _sideSwipeController.reset([[SideSwipeController alloc]
1052 initWithTabModel:_model
1053 browserState:_browserState]);
1054 [_sideSwipeController setSnapshotDelegate:self];
1055 [_sideSwipeController setSwipeDelegate:self];
1056 }
1057 return _sideSwipeController;
1058}
1059
1060- (PreloadController*)preloadController {
1061 return _preloadController.get();
1062}
1063
1064- (DialogPresenter*)dialogPresenter {
1065 return _dialogPresenter;
1066}
1067
1068- (BOOL)canUseReaderMode {
1069 Tab* tab = [_model currentTab];
1070 if ([self isTabNativePage:tab])
1071 return NO;
1072
1073 return [tab canSwitchToReaderMode];
1074}
1075
1076- (BOOL)canUseDesktopUserAgent {
1077 Tab* tab = [_model currentTab];
1078 if ([self isTabNativePage:tab])
1079 return NO;
1080
1081 // If |useDesktopUserAgent| is |NO|, allow useDesktopUserAgent.
1082 return !tab.useDesktopUserAgent;
1083}
1084
1085// Whether the sharing menu should be shown.
1086- (BOOL)canShowShareMenu {
1087 Tab* tab = [_model currentTab];
1088 // TODO(shreyasv): Make it so the URL returned by the tab is always valid and
1089 // remove check on net::NSURLWithGURL(tab.url) ( http://crbug.com/400999 ).
1090 return tab && !tab.url.SchemeIs(kChromeUIScheme) &&
1091 net::NSURLWithGURL(tab.url);
1092}
1093
1094- (BOOL)canShowFindBar {
1095 // Make sure web controller can handle find in page.
1096 Tab* tab = [_model currentTab];
1097 if (![tab.findInPageController canFindInPage])
1098 return NO;
1099
1100 // Don't show twice.
1101 if (tab.findInPageController.findInPageModel.enabled)
1102 return NO;
1103
1104 return YES;
1105}
1106
1107- (void)setVisible:(BOOL)visible {
1108 if (_visible == visible)
1109 return;
1110 _visible = visible;
1111}
1112
1113- (void)setViewVisible:(BOOL)viewVisible {
1114 if (_viewVisible == viewVisible)
1115 return;
1116 _viewVisible = viewVisible;
1117 self.visible = viewVisible;
1118 [self updateDialogPresenterActiveState];
1119}
1120
1121- (BOOL)isToolbarOnScreen {
1122 return [self headerHeight] - [self currentHeaderOffset] > 0;
1123}
1124
1125- (BOOL)isInNewTabAnimation {
1126 return _inNewTabAnimation;
1127}
1128
1129- (BOOL)shouldShowVoiceSearchBar {
1130 // On iPads, the voice search bar should only be shown for regular horizontal
1131 // size class configurations. It should always be shown for voice search
1132 // results Tabs on iPhones, including configurations with regular horizontal
1133 // size classes (i.e. landscape iPhone 6 Plus).
1134 BOOL compactWidth = self.traitCollection.horizontalSizeClass ==
1135 UIUserInterfaceSizeClassCompact;
1136 return self.tabModel.currentTab.isVoiceSearchResultsTab &&
1137 (!IsIPadIdiom() || compactWidth);
1138}
1139
1140- (void)setHideStatusBar:(BOOL)hideStatusBar {
1141 if (_hideStatusBar == hideStatusBar)
1142 return;
1143 _hideStatusBar = hideStatusBar;
1144 [self setNeedsStatusBarAppearanceUpdate];
1145}
1146
1147#pragma mark - IBActions
1148
1149- (void)shieldWasTapped:(id)sender {
1150 [_toolbarController cancelOmniboxEdit];
1151}
1152
1153- (void)newTab:(id)sender {
1154 [self setLastTapPoint:sender];
1155 DCHECK(self.visible || self.dismissingModal);
1156 Tab* currentTab = [_model currentTab];
1157 if (currentTab) {
1158 BOOL isChromeScheme =
1159 web::GetWebClient()->IsAppSpecificURL([currentTab url]);
1160 BOOL snapshotOnIpad =
1161 (!IsIPadIdiom() || experimental_flags::IsTabSwitcherEnabled());
1162 if (snapshotOnIpad || !isChromeScheme) {
1163 [currentTab updateSnapshotWithOverlay:YES visibleFrameOnly:YES];
1164 }
1165 }
1166 [self addSelectedTabWithURL:GURL(kChromeUINewTabURL)
1167 transition:ui::PAGE_TRANSITION_TYPED];
1168}
1169
1170#pragma mark - UIViewController methods
1171
1172// Perform additional set up after loading the view, typically from a nib.
1173- (void)viewDidLoad {
jif50d5ba252016-12-20 14:00:281174 CGRect initialViewsRect = self.view.frame;
1175 initialViewsRect.origin.y += StatusBarHeight();
1176 initialViewsRect.size.height -= StatusBarHeight();
sdefresnee65fd872016-12-19 13:38:131177 UIViewAutoresizing initialViewAutoresizing =
1178 UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
1179
1180 self.contentArea = [[[BrowserContainerView alloc]
1181 initWithFrame:initialViewsRect] autorelease];
1182 self.contentArea.autoresizingMask = initialViewAutoresizing;
1183 self.typingShield =
1184 [[[UIButton alloc] initWithFrame:initialViewsRect] autorelease];
1185 self.typingShield.autoresizingMask = initialViewAutoresizing;
1186 [self.typingShield addTarget:self
1187 action:@selector(shieldWasTapped:)
1188 forControlEvents:UIControlEventTouchUpInside];
sdefresnee65fd872016-12-19 13:38:131189 self.view.autoresizingMask = initialViewAutoresizing;
1190 self.view.backgroundColor = [UIColor colorWithWhite:0.75 alpha:1.0];
1191 [self.view addSubview:self.contentArea];
1192 [self.view addSubview:self.typingShield];
1193 [super viewDidLoad];
1194
1195 // Install fake status bar for iPad iOS7
1196 [self installFakeStatusBar];
1197 [self buildToolbarAndTabStrip];
1198 [self setUpViewLayout];
1199 // If the tab model and browser state are valid, finish initialization.
1200 if (_model && _browserState)
1201 [self addUIFunctionalityForModelAndBrowserState];
1202
1203 // Add a tap gesture recognizer to save the last tap location for the source
1204 // location of the new tab animation.
1205 base::scoped_nsobject<UITapGestureRecognizer> tapRecognizer(
1206 [[UITapGestureRecognizer alloc]
1207 initWithTarget:self
1208 action:@selector(saveContentAreaTapLocation:)]);
1209 [tapRecognizer setDelegate:self];
1210 [tapRecognizer setCancelsTouchesInView:NO];
1211 [_contentArea addGestureRecognizer:tapRecognizer];
1212}
1213
1214- (void)viewDidAppear:(BOOL)animated {
1215 [super viewDidAppear:animated];
1216 self.viewVisible = YES;
1217 [self updateDialogPresenterActiveState];
1218}
1219
1220- (void)viewWillAppear:(BOOL)animated {
1221 [super viewWillAppear:animated];
1222
1223 // Reparent the toolbar if it's been relinquished.
1224 if (_isToolbarControllerRelinquished)
1225 [self reparentToolbarController];
1226
1227 self.visible = YES;
1228
1229 // Restore hidden infobars.
1230 if (IsIPadIdiom() && experimental_flags::IsTabSwitcherEnabled()) {
1231 _infoBarContainer->RestoreInfobars();
1232 }
1233
1234 // If the controller is suspended, or has been paged out due to low memory,
1235 // updating the view will be handled when it's displayed again.
1236 if (![_model webUsageEnabled] || !self.contentArea)
1237 return;
1238 // Update the displayed tab (if any; the switcher may not have created one
1239 // yet) in case it changed while showing the switcher.
1240 Tab* currentTab = [_model currentTab];
1241 if (currentTab)
1242 [self displayTab:currentTab isNewSelection:YES];
1243}
1244
1245- (void)viewWillDisappear:(BOOL)animated {
1246 self.viewVisible = NO;
1247 [self updateDialogPresenterActiveState];
1248 [[_model currentTab] updateFullscreenWithToolbarVisible:YES];
1249 [[_model currentTab] wasHidden];
1250 [_bookmarkInteractionController dismissSnackbar];
1251 if (IsIPadIdiom() && experimental_flags::IsTabSwitcherEnabled()) {
1252 _infoBarContainer->SuspendInfobars();
1253 }
1254 [super viewWillDisappear:animated];
1255}
1256
1257- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)orient
1258 duration:(NSTimeInterval)duration {
1259 [super willRotateToInterfaceOrientation:orient duration:duration];
1260 [self dismissPopups];
1261 [self reshowFindBarIfNeededWithCoordinator:nil];
1262}
1263
1264- (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)orient {
1265 [super didRotateFromInterfaceOrientation:orient];
1266
1267 // This reinitializes the toolbar, including updating the Overlay View,
1268 // if there is one.
1269 [self updateToolbar];
1270 [self infoBarContainerStateChanged:false];
1271}
1272
1273- (BOOL)prefersStatusBarHidden {
1274 return self.hideStatusBar;
1275}
1276
1277// Called when in the foreground and the OS needs more memory. Release as much
1278// as possible.
1279- (void)didReceiveMemoryWarning {
1280 // Releases the view if it doesn't have a superview.
1281 [super didReceiveMemoryWarning];
1282
1283 // Release any cached data, images, etc that aren't in use.
1284 // TODO(pinkerton): This feels like it should go in the MemoryPurger class,
1285 // but since the FaviconCache uses obj-c in the header, it can't be included
1286 // there.
1287 if (_browserState) {
1288 FaviconLoader* loader =
1289 IOSChromeFaviconLoaderFactory::GetForBrowserStateIfExists(
1290 _browserState);
1291 if (loader)
1292 loader->PurgeCache();
1293 }
1294
1295 if (![self isViewLoaded]) {
1296 // Do not release |_infoBarContainer|, as this must have the same lifecycle
1297 // as the BrowserViewController.
1298 self.contentArea = nil;
1299 self.typingShield = nil;
1300 if (_voiceSearchController.get())
1301 _voiceSearchController->SetDelegate(nil);
1302 _qrScannerViewController.reset();
1303 _toolbarController.reset();
1304 _toolbarModelDelegate.reset();
1305 _toolbarModelIOS.reset();
1306 _tabStripController.reset();
1307 _noTabsController.reset();
1308 _sideSwipeController.reset();
1309 }
1310}
1311
1312- (void)traitCollectionDidChange:(UITraitCollection*)previousTraitCollection {
1313 [super traitCollectionDidChange:previousTraitCollection];
1314 // TODO(crbug.com/527092): - traitCollectionDidChange: is not always forwarded
1315 // because in some cases the presented view controller isn't a child of the
1316 // BVC in the view controller hierarchy (some intervening object isn't a
1317 // view controller).
1318 [self.presentedViewController
1319 traitCollectionDidChange:previousTraitCollection];
1320 [_toolbarController traitCollectionDidChange:previousTraitCollection];
1321 // Update voice search bar visibility.
1322 [self updateVoiceSearchBarVisibilityAnimated:NO];
1323}
1324
1325- (void)viewWillTransitionToSize:(CGSize)size
1326 withTransitionCoordinator:
1327 (id<UIViewControllerTransitionCoordinator>)coordinator {
1328 [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
1329 [self dismissPopups];
1330 [self reshowFindBarIfNeededWithCoordinator:coordinator];
1331}
1332
1333- (void)reshowFindBarIfNeededWithCoordinator:
1334 (id<UIViewControllerTransitionCoordinator>)coordinator {
1335 if (![_findBarController isFindInPageShown])
1336 return;
1337
1338 // Record focused state.
1339 BOOL isFocusedBeforeReshow = [_findBarController isFocused];
1340
1341 [self hideFindBarWithAnimation:NO];
1342
1343 base::WeakNSObject<BrowserViewController> weakSelf(self);
1344 void (^completion)(id<UIViewControllerTransitionCoordinatorContext>) = ^(
1345 id<UIViewControllerTransitionCoordinatorContext> context) {
1346 base::scoped_nsobject<BrowserViewController> strongSelf([weakSelf retain]);
1347 if (strongSelf)
1348 [strongSelf showFindBarWithAnimation:NO
1349 selectText:NO
1350 shouldFocus:isFocusedBeforeReshow];
1351 };
1352
1353 BOOL enqueued =
1354 [coordinator animateAlongsideTransition:nil completion:completion];
1355 if (!enqueued) {
1356 completion(nil);
1357 }
1358}
1359
1360- (void)dismissViewControllerAnimated:(BOOL)flag
1361 completion:(void (^)())completion {
1362 self.dismissingModal = YES;
1363 base::WeakNSObject<BrowserViewController> weakSelf(self);
1364 [super dismissViewControllerAnimated:flag
1365 completion:^{
1366 base::scoped_nsobject<BrowserViewController>
1367 strongSelf([weakSelf retain]);
1368 [strongSelf setDismissingModal:NO];
1369 [strongSelf setPresenting:NO];
1370 if (completion)
1371 completion();
1372 [[strongSelf dialogPresenter] tryToPresent];
1373 }];
1374}
1375
1376- (void)presentViewController:(UIViewController*)viewControllerToPresent
1377 animated:(BOOL)flag
1378 completion:(void (^)())completion {
1379 base::mac::ScopedBlock<ProceduralBlock> finalCompletionHandler(
1380 [completion copy]);
1381 // TODO(crbug.com/580098) This is an interim fix for the flicker between the
1382 // launch screen and the FRE Animation. The fix is, if the FRE is about to be
1383 // presented, to show a temporary view of the launch screen and then remove it
1384 // when the controller for the FRE has been presented. This fix should be
1385 // removed when the FRE startup code is rewritten.
1386 BOOL firstRunLaunch = (FirstRun::IsChromeFirstRun() ||
1387 experimental_flags::AlwaysDisplayFirstRun()) &&
1388 !tests_hook::DisableFirstRun();
1389 // These if statements check that |presentViewController| is being called for
1390 // the FRE case.
1391 if (firstRunLaunch &&
1392 [viewControllerToPresent isKindOfClass:[UINavigationController class]]) {
1393 UINavigationController* navController =
1394 base::mac::ObjCCastStrict<UINavigationController>(
1395 viewControllerToPresent);
1396 if ([navController.topViewController
1397 isMemberOfClass:[WelcomeToChromeViewController class]]) {
1398 self.hideStatusBar = YES;
1399
1400 // Load view from Launch Screen and add it to window.
1401 NSBundle* mainBundle = base::mac::FrameworkBundle();
1402 NSArray* topObjects =
1403 [mainBundle loadNibNamed:@"LaunchScreen" owner:self options:nil];
1404 UIViewController* launchScreenController =
1405 base::mac::ObjCCastStrict<UIViewController>([topObjects lastObject]);
1406 // |launchScreenView| is loaded as an autoreleased object, and is retained
1407 // by the |completion| block below.
1408 UIView* launchScreenView = launchScreenController.view;
1409 launchScreenView.userInteractionEnabled = NO;
1410 launchScreenView.frame = self.view.window.bounds;
1411 [self.view.window addSubview:launchScreenView];
1412
1413 // Replace the completion handler sent to the superclass with one which
1414 // removes |launchScreenView| and resets the status bar. If |completion|
1415 // exists, it is called from within the new completion handler.
1416 base::WeakNSObject<BrowserViewController> weakSelf(self);
1417 finalCompletionHandler.reset([^{
1418 [launchScreenView removeFromSuperview];
1419 weakSelf.get().hideStatusBar = NO;
1420 if (completion)
1421 completion();
1422 } copy]);
1423 }
1424 }
1425
1426 self.presenting = YES;
justincohen7e61cd92016-12-24 00:38:171427 if ([_sideSwipeController inSwipe]) {
1428 [_sideSwipeController resetContentView];
1429 }
sdefresnee65fd872016-12-19 13:38:131430
1431 [super presentViewController:viewControllerToPresent
1432 animated:flag
1433 completion:finalCompletionHandler];
1434}
1435
1436#pragma mark - Notification handling
1437
1438- (void)registerForNotifications {
1439 DCHECK(_model);
1440 NSNotificationCenter* defaultCenter = [NSNotificationCenter defaultCenter];
1441 [defaultCenter addObserver:self
1442 selector:@selector(pageLoadStarting:)
1443 name:kTabModelTabWillStartLoadingNotification
1444 object:_model];
1445 [defaultCenter addObserver:self
1446 selector:@selector(pageLoadStarted:)
1447 name:kTabModelTabDidStartLoadingNotification
1448 object:_model];
1449 [defaultCenter addObserver:self
1450 selector:@selector(pageLoadComplete:)
1451 name:kTabModelTabDidFinishLoadingNotification
1452 object:_model];
1453 [defaultCenter addObserver:self
1454 selector:@selector(tabDeselected:)
1455 name:kTabModelTabDeselectedNotification
1456 object:_model];
1457 [defaultCenter addObserver:self
1458 selector:@selector(tabWasAdded:)
1459 name:kTabModelNewTabWillOpenNotification
1460 object:_model];
1461}
1462
1463- (void)pageLoadStarting:(NSNotification*)notify {
1464 Tab* tab = notify.userInfo[kTabModelTabKey];
1465 DCHECK(tab && ([_model indexOfTab:tab] != NSNotFound));
1466 // Hide find bar when navigating to a new page.
1467 [self hideFindBarWithAnimation:NO];
1468 tab.findInPageController.findInPageModel.enabled = NO;
1469 if (tab == [_model currentTab]) {
1470 // TODO(pinkerton): Fill in here about hiding the forward button on
1471 // navigation.
1472 }
1473}
1474
1475- (void)pageLoadStarted:(NSNotification*)notify {
1476 Tab* tab = notify.userInfo[kTabModelTabKey];
1477 DCHECK(tab);
1478 if (tab == [_model currentTab]) {
1479 if (![self isTabNativePage:tab]) {
1480 [_toolbarController currentPageLoadStarted];
1481 }
1482 [self updateVoiceSearchBarVisibilityAnimated:NO];
1483 }
1484}
1485
1486- (void)pageLoadComplete:(NSNotification*)notify {
1487 // Update the UI, but only if the current tab.
1488 Tab* tab = notify.userInfo[kTabModelTabKey];
1489 if (tab == [_model currentTab]) {
1490 // There isn't any need to update the toolbar here. When the page finishes,
1491 // it will have already sent us |-tabModel:didChangeTab:| which will do it.
1492 }
1493
1494 BOOL loadingSucceeded = [notify.userInfo[kTabModelPageLoadSuccess] boolValue];
1495
1496 [self tabLoadComplete:tab withSuccess:loadingSucceeded];
1497}
1498
1499- (void)tabDeselected:(NSNotification*)notify {
1500 DCHECK(notify);
1501 Tab* tab = notify.userInfo[kTabModelTabKey];
1502 DCHECK(tab);
1503 [tab wasHidden];
1504 [_toolbarController dismissTabHistoryPopup];
1505}
1506
1507- (void)tabWasAdded:(NSNotification*)notify {
1508 Tab* tab = notify.userInfo[kTabModelTabKey];
1509 DCHECK(tab);
1510
1511 // Update map if a native controller was vended before the tab was added.
1512 id<CRWNativeContent> nativeController =
1513 [_nativeControllersForTabIDs objectForKey:kNativeControllerTemporaryKey];
1514 if (nativeController) {
1515 [_nativeControllersForTabIDs
1516 removeObjectForKey:kNativeControllerTemporaryKey];
1517 [_nativeControllersForTabIDs setObject:nativeController forKey:tab.tabId];
1518 }
1519
1520 // When adding new tabs, check what kind of reminder infobar should
1521 // be added to the new tab. Try to add only one of them.
1522 // This check is done when a new tab is added either through the Tools Menu
1523 // "New Tab" or through "New Tab" in Stack View Controller. This method
1524 // is called after a new tab has added and finished initial navigation.
1525 // If this is added earlier, the initial navigation may end up clearing
1526 // the infobar(s) that are just added. See http://crbug/340250 for details.
1527 [[UpgradeCenter sharedInstance] addInfoBarToManager:[tab infoBarManager]
1528 forTabId:[tab tabId]];
1529 if (!ReSignInInfoBarDelegate::Create(_browserState, tab)) {
1530 ios_internal::sync::displaySyncErrors(_browserState, tab);
1531 }
1532
1533 // The rest of this function initiates the new tab animation, which is
1534 // phone-specific.
1535 if (IsIPadIdiom())
1536 return;
1537
1538 // Do nothing if browsing is currently suspended. The BVC will set everything
1539 // up correctly when browsing resumes.
1540 if (!self.visible || ![_model webUsageEnabled])
1541 return;
1542
1543 BOOL inBackground = [notify.userInfo[kTabModelOpenInBackgroundKey] boolValue];
1544
1545 // Block that starts voice search at the end of new Tab animation if
1546 // necessary.
1547 ProceduralBlock startVoiceSearchIfNecessaryBlock = ^void() {
1548 if (_startVoiceSearchAfterNewTabAnimation) {
1549 _startVoiceSearchAfterNewTabAnimation = NO;
1550 [self startVoiceSearch];
1551 }
1552 };
1553
1554 _inNewTabAnimation = YES;
1555 if (!inBackground) {
1556 UIView* animationParentView = _contentArea;
1557 // Create the new page image, and load with the new tab page snapshot.
1558 CGFloat newPageOffset = 0;
1559 UIImageView* newPage;
1560 if (tab.url == GURL(kChromeUINewTabURL) && !_isOffTheRecord &&
1561 !IsIPadIdiom()) {
1562 animationParentView = self.view;
1563 newPage = [self pageFullScreenOpenCloseAnimationView];
1564 } else {
1565 newPage = [self pageOpenCloseAnimationView];
1566 }
1567 newPageOffset = newPage.frame.origin.y;
1568
1569 [tab view].frame = _contentArea.bounds;
1570 newPage.image = [tab updateSnapshotWithOverlay:YES visibleFrameOnly:YES];
1571 [animationParentView addSubview:newPage];
1572 CGPoint origin = [self lastTapPoint];
1573 ios_internal::page_animation_util::AnimateInPaperWithAnimationAndCompletion(
1574 newPage, -newPageOffset,
1575 newPage.frame.size.height - newPage.image.size.height, origin,
1576 _isOffTheRecord, NULL, ^{
1577 [newPage removeFromSuperview];
1578 _inNewTabAnimation = NO;
michaeldof49c9b2c2016-12-20 23:07:421579 // Use the model's currentTab here because it is possible that it can
1580 // be reset to a new value before the new Tab animation finished (e.g.
1581 // if another Tab shows a dialog via |dialogPresenter|). However, that
1582 // tab's view hasn't been displayed yet because it was in a new tab
1583 // animation.
1584 Tab* currentTab = [_model currentTab];
1585 if (currentTab) {
1586 [self tabSelected:currentTab];
1587 }
sdefresnee65fd872016-12-19 13:38:131588 startVoiceSearchIfNecessaryBlock();
1589 });
1590 } else {
1591 // -updateSnapshotWithOverlay will force a screen redraw, so take the
1592 // snapshot before adding the views needed for the background animation.
1593 Tab* topTab = [_model currentTab];
1594 UIImage* image = [topTab updateSnapshotWithOverlay:YES
1595 visibleFrameOnly:self.isToolbarOnScreen];
1596 // Add three layers in order on top of the contentArea for the animation:
1597 // 1. The black "background" screen.
1598 base::scoped_nsobject<UIView> background(
1599 [[UIView alloc] initWithFrame:[_contentArea bounds]]);
1600 InstallBackgroundInView(background);
1601 [_contentArea addSubview:background];
1602
1603 // 2. A CardView displaying the data from the current tab.
1604 CardView* topCard = [self addCardViewInFullscreen:!self.isToolbarOnScreen];
1605 NSString* title = [topTab title];
1606 if (![title length])
1607 title = [topTab urlDisplayString];
1608 [topCard setTitle:title];
1609 [topCard setFavicon:[topTab favicon]];
1610 [topCard setImage:image];
1611
1612 // 3. A new, blank CardView to represent the new tab being added.
1613 // Launch the new background tab animation.
1614 ios_internal::page_animation_util::AnimateNewBackgroundPageWithCompletion(
1615 topCard, [_contentArea frame], IsPortrait(), ^{
1616 [background removeFromSuperview];
1617 [topCard removeFromSuperview];
1618 _inNewTabAnimation = NO;
1619 // Resnapshot the top card if it has its own toolbar, as the toolbar
1620 // will be captured in the new tab animation, but isn't desired for
1621 // the stack view snapshots.
1622 id nativeController = [self nativeControllerForTab:topTab];
1623 if ([nativeController conformsToProtocol:@protocol(ToolbarOwner)])
1624 [topTab updateSnapshotWithOverlay:YES visibleFrameOnly:YES];
1625 startVoiceSearchIfNecessaryBlock();
1626 });
1627 }
1628}
1629
1630#pragma mark - UI Configuration and Layout
1631
1632- (void)updateWithTabModel:(TabModel*)model
1633 browserState:(ios::ChromeBrowserState*)browserState {
1634 DCHECK(model);
1635 DCHECK(browserState);
1636 DCHECK(!_model);
1637 DCHECK(!_browserState);
1638 _browserState = browserState;
1639 _isOffTheRecord = browserState->IsOffTheRecord() ? YES : NO;
1640 _model.reset([model retain]);
1641 [_model addObserver:self];
1642
1643 if (!_isOffTheRecord) {
1644 [DefaultIOSWebViewFactory
1645 registerWebViewFactory:[ChromeWebViewFactory class]];
1646 }
1647 NSUInteger count = [_model count];
1648 for (NSUInteger index = 0; index < count; ++index)
1649 [self installDelegatesForTab:[_model tabAtIndex:index]];
1650
1651 [self registerForNotifications];
1652
1653 _imageFetcher->SetRequestContextGetter(_browserState->GetRequestContext());
1654 _dominantColorCache.reset([[NSMutableDictionary alloc] init]);
1655
sdefresnedc432f42017-01-17 14:36:591656 // Register for bookmark changed notification (BookmarkModel may be null
1657 // during testing, so explicitly support this).
sdefresnee65fd872016-12-19 13:38:131658 _bookmarkModel = ios::BookmarkModelFactory::GetForBrowserState(_browserState);
sdefresnedc432f42017-01-17 14:36:591659 if (_bookmarkModel) {
1660 _bookmarkModelBridge.reset(new BrowserBookmarkModelBridge(self));
1661 _bookmarkModel->AddObserver(_bookmarkModelBridge.get());
1662 }
sdefresnee65fd872016-12-19 13:38:131663}
1664
1665- (void)ensureViewCreated {
1666 ignore_result([self view]);
1667}
1668
1669- (void)browserStateDestroyed {
1670 [self setActive:NO];
1671 // Reset the toolbar opacity in case it was changed for contextual search.
1672 [self updateToolbarControlsAlpha:1.0];
1673 [self updateToolbarBackgroundAlpha:1.0];
1674 [_contextualSearchController close];
1675 _contextualSearchController.reset();
1676 [_contextualSearchPanel removeFromSuperview];
1677 [_contextualSearchMask removeFromSuperview];
1678 [_paymentRequestManager close];
1679 _paymentRequestManager.reset();
sdefresnee65fd872016-12-19 13:38:131680 [_toolbarController browserStateDestroyed];
1681 [_model browserStateDestroyed];
michaeldobc2f42e2017-01-12 19:04:471682 [_preloadController browserStateDestroyed];
1683 _preloadController.reset();
sdefresnee65fd872016-12-19 13:38:131684 // The file remover needs the browser state, so needs to be destroyed now.
1685 _externalFileRemover.reset();
1686 _browserState = nullptr;
1687}
1688
1689- (void)installFakeStatusBar {
1690 if (IsIPadIdiom()) {
1691 CGFloat statusBarHeight = StatusBarHeight();
1692 CGRect statusBarFrame =
1693 CGRectMake(0, 0, [[self view] frame].size.width, statusBarHeight);
1694 base::scoped_nsobject<UIView> statusBarView(
1695 [[UIView alloc] initWithFrame:statusBarFrame]);
1696 [statusBarView setBackgroundColor:TabStrip::BackgroundColor()];
1697 [statusBarView setAutoresizingMask:UIViewAutoresizingFlexibleWidth];
1698 [statusBarView layer].zPosition = 99;
1699 [[self view] addSubview:statusBarView];
1700 }
1701
1702 // Add a white bar on phone so that the status bar on the NTP is white.
1703 if (!IsIPadIdiom()) {
1704 CGFloat statusBarHeight = StatusBarHeight();
1705 CGRect statusBarFrame =
1706 CGRectMake(0, 0, [[self view] frame].size.width, statusBarHeight);
1707 base::scoped_nsobject<UIView> statusBarView(
1708 [[UIView alloc] initWithFrame:statusBarFrame]);
1709 [statusBarView setBackgroundColor:[UIColor whiteColor]];
1710 [statusBarView setAutoresizingMask:UIViewAutoresizingFlexibleWidth];
1711 [self.view insertSubview:statusBarView atIndex:0];
1712 }
1713}
1714
1715// Create the UI elements. May or may not have valid browser state & tab model.
1716- (void)buildToolbarAndTabStrip {
1717 DCHECK([self isViewLoaded]);
1718 DCHECK(!_toolbarModelDelegate);
1719
1720 // Create the preload controller before the toolbar controller.
1721 if (!_preloadController) {
1722 _preloadController.reset([_dependencyFactory newPreloadController]);
1723 [_preloadController setDelegate:self];
1724 }
1725
1726 // Create the toolbar model and controller.
1727 _toolbarModelDelegate.reset(new ToolbarModelDelegateIOS(_model));
1728 _toolbarModelIOS.reset([_dependencyFactory
1729 newToolbarModelIOSWithDelegate:_toolbarModelDelegate.get()]);
1730 _toolbarController.reset([_dependencyFactory
1731 newWebToolbarControllerWithDelegate:self
1732 urlLoader:self
1733 preloadProvider:_preloadController.get()]);
1734 [_toolbarController setTabCount:[_model count]];
1735 if (_voiceSearchController.get())
1736 _voiceSearchController->SetDelegate(_toolbarController);
1737
1738 // If needed, create the tabstrip.
1739 if (IsIPadIdiom()) {
1740 // Determine if it's incognito. Whether or not the toggle button is
1741 // visible is consolidated in logic elsewhere so it doesn't need to be set
1742 // here.
1743 _tabStripController.reset(
1744 [_dependencyFactory newTabStripControllerWithTabModel:_model]);
1745 _tabStripController.get().fullscreenDelegate = self;
1746 [_tabStripController setHasTabSwitcherToggleSwitch:
1747 experimental_flags::IsTabSwitcherEnabled()];
1748
1749 // If set before the views are loaded, pass the mode toggle to the
1750 // toolbar controller (only needed if YES, defaults to NO).
1751 if (_modeToggleNeedsSetting) {
1752 _tabStripController.get().hasModeToggleSwitch = _modeToggleNeedsSetting;
1753 }
1754 }
1755
1756 // Create infobar container.
1757 if (!_infoBarContainerDelegate) {
1758 _infoBarContainerDelegate.reset(new InfoBarContainerDelegateIOS(self));
1759 _infoBarContainer.reset(
1760 new InfoBarContainerIOS(_infoBarContainerDelegate.get()));
1761 }
1762}
1763
1764// Enable functionality that only makes sense if the views are loaded and
1765// both browser state and tab model are valid.
1766- (void)addUIFunctionalityForModelAndBrowserState {
1767 DCHECK(_browserState);
1768 DCHECK(_model);
1769 DCHECK([self isViewLoaded]);
1770
1771 [self.sideSwipeController addHorizontalGesturesToView:self.view];
1772
1773 infobars::InfoBarManager* infoBarManager =
1774 [[_model currentTab] infoBarManager];
1775 _infoBarContainer->ChangeInfoBarManager(infoBarManager);
1776
1777 // Create contextual search views and controller.
1778 if ([TouchToSearchPermissionsMediator isTouchToSearchAvailableOnDevice] &&
1779 !_browserState->IsOffTheRecord()) {
1780 _contextualSearchMask =
1781 [[[ContextualSearchMaskView alloc] init] autorelease];
1782 [self.view insertSubview:_contextualSearchMask
1783 belowSubview:[_toolbarController view]];
1784 _contextualSearchPanel = [self createPanelView];
1785 [self.view insertSubview:_contextualSearchPanel
1786 aboveSubview:[_toolbarController view]];
1787 _contextualSearchController.reset([[ContextualSearchController alloc]
1788 initWithBrowserState:_browserState
1789 delegate:self]);
1790 [_contextualSearchController setPanel:_contextualSearchPanel];
1791 [_contextualSearchController setTab:[_model currentTab]];
1792 }
1793
1794 if (experimental_flags::IsPaymentRequestEnabled()) {
1795 _paymentRequestManager.reset([[PaymentRequestManager alloc]
1796 initWithBaseViewController:self
1797 browserState:_browserState]);
1798 [_paymentRequestManager setWebState:[_model currentTab].webState];
1799 }
1800}
1801
1802// Set the frame for the various views. View must be loaded.
1803- (void)setUpViewLayout {
1804 DCHECK([self isViewLoaded]);
1805
1806 CGFloat widthOfView = CGRectGetWidth([self view].bounds);
1807
1808 CGFloat minY = [self headerOffset];
1809
1810 // If needed, position the tabstrip.
1811 if (IsIPadIdiom()) {
1812 [self layoutTabStripForWidth:widthOfView];
1813 [[self view] addSubview:[_tabStripController view]];
1814 minY += CGRectGetHeight([[_tabStripController view] frame]);
1815 }
1816
1817 // Position the toolbar next, either at the top of the browser view or
1818 // directly under the tabstrip.
1819 CGRect toolbarFrame = [[_toolbarController view] frame];
1820 toolbarFrame.origin = CGPointMake(0, minY);
1821 toolbarFrame.size.width = widthOfView;
1822 [[_toolbarController view] setFrame:toolbarFrame];
1823
1824 // Place the infobar container above the content area.
1825 InfoBarContainerView* infoBarContainerView = _infoBarContainer->view();
1826 [self.view insertSubview:infoBarContainerView aboveSubview:_contentArea];
1827
1828 // Place the toolbar controller above the infobar container.
1829 [[self view] insertSubview:[_toolbarController view]
1830 aboveSubview:infoBarContainerView];
1831 minY += CGRectGetHeight(toolbarFrame);
1832
1833 // Account for the toolbar's drop shadow. The toolbar overlaps with the web
1834 // content slightly.
1835 minY -= [ToolbarController toolbarDropShadowHeight];
1836
1837 // Adjust the content area to be under the toolbar, for fullscreen or below
1838 // the toolbar is not fullscreen.
1839 CGRect contentFrame = [_contentArea frame];
1840 CGFloat marginWithHeader = StatusBarHeight();
1841 CGFloat overlap = [self headerHeight] != 0 ? marginWithHeader : minY;
1842 contentFrame.size.height = CGRectGetMaxY(contentFrame) - overlap;
1843 contentFrame.origin.y = overlap;
1844 [_contentArea setFrame:contentFrame];
1845
1846 // Adjust the infobar container to be either at the bottom of the screen
1847 // (iPhone) or on the lower toolbar edge (iPad).
1848 CGRect infoBarFrame = contentFrame;
1849 infoBarFrame.origin.y = CGRectGetMaxY(contentFrame);
1850 infoBarFrame.size.height = 0;
1851 [infoBarContainerView setFrame:infoBarFrame];
1852
1853 // Attach the typing shield to the content area but have it hidden.
1854 [_typingShield setFrame:[_contentArea frame]];
1855 [[self view] insertSubview:_typingShield aboveSubview:_contentArea];
1856 [_typingShield setHidden:YES];
1857 _typingShield.accessibilityIdentifier = @"Typing Shield";
1858 _typingShield.accessibilityLabel = l10n_util::GetNSString(IDS_CANCEL);
1859}
1860
1861- (void)layoutTabStripForWidth:(CGFloat)maxWidth {
1862 UIView* tabStripView = [_tabStripController view];
1863 CGRect tabStripFrame = [tabStripView frame];
1864 tabStripFrame.origin = CGPointZero;
1865 // TODO(crbug.com/256655): Move the origin.y below to -setUpViewLayout.
1866 // because the CGPointZero above will break reset the offset, but it's not
1867 // clear what removing that will do.
1868 tabStripFrame.origin.y = [self headerOffset];
1869 tabStripFrame.size.width = maxWidth;
1870 [tabStripView setFrame:tabStripFrame];
1871}
1872
1873- (void)displayTab:(Tab*)tab isNewSelection:(BOOL)newSelection {
1874 DCHECK(tab);
1875 // Ensure that self.view is loaded to avoid errors that can otherwise occur
1876 // when accessing |_contentArea| below.
1877 if (!_contentArea)
1878 [self ensureViewCreated];
1879
1880 DCHECK(_contentArea);
1881 if (!_inNewTabAnimation) {
1882 // Hide findbar. |updateToolbar| will restore the findbar later.
1883 [self hideFindBarWithAnimation:NO];
1884
1885 // Make new content visible, resizing it first as the orientation may
1886 // have changed from the last time it was displayed.
1887 [[tab view] setFrame:_contentArea.bounds];
1888 [_contentArea displayContentView:[tab view]];
1889 }
1890 [self updateToolbar];
1891
1892 if (newSelection)
1893 [_toolbarController selectedTabChanged];
1894
1895 // Notify the Tab that it was displayed.
1896 [tab wasShown];
1897}
1898
1899- (void)initializeBookmarkInteractionController {
1900 if (_bookmarkInteractionController)
1901 return;
1902 _bookmarkInteractionController.reset([[BookmarkInteractionController alloc]
1903 initWithBrowserState:_browserState
1904 loader:self
1905 parentController:self]);
1906}
1907
1908// Update the state of back and forward buttons, hiding the forward button if
1909// there is nowhere to go. Assumes the model's current tab is up to date.
1910- (void)updateToolbar {
1911 // If the BVC has been partially torn down for low memory, wait for the
1912 // view rebuild to handle toolbar updates.
1913 if (!(_toolbarModelIOS && _browserState))
1914 return;
1915
1916 Tab* tab = [_model currentTab];
1917 if (![tab navigationManager])
1918 return;
1919 [_toolbarController updateToolbarState];
1920 [_toolbarController setShareButtonEnabled:self.canShowShareMenu];
1921
1922 if (tab.isPrerenderTab && !_toolbarModelIOS->IsLoading())
1923 [_toolbarController showPrerenderingAnimation];
1924
1925 // Also update the loading state for the tools menu (that is really an
1926 // extension of the toolbar on the iPhone).
1927 if (!IsIPadIdiom())
1928 [[_toolbarController toolsPopupController]
1929 setIsTabLoading:_toolbarModelIOS->IsLoading()];
1930
1931 if (tab.findInPageController.findInPageModel.enabled)
1932 [self showFindBarWithAnimation:NO
1933 selectText:YES
1934 shouldFocus:[_findBarController isFocused]];
1935
1936 // Hide the toolbar if displaying phone NTP.
1937 if (!IsIPadIdiom()) {
1938 CRWSessionEntry* entry =
1939 [[tab navigationManager]->GetSessionController() currentEntry];
1940 BOOL hideToolbar = NO;
1941 if (entry) {
1942 GURL url = [entry navigationItem]->GetURL();
1943 BOOL isNTP = url.GetOrigin() == GURL(kChromeUINewTabURL);
1944 hideToolbar = isNTP && !_isOffTheRecord &&
1945 ![_toolbarController isOmniboxFirstResponder] &&
1946 ![_toolbarController showingOmniboxPopup];
1947 }
1948 [[_toolbarController view] setHidden:hideToolbar];
1949 }
1950}
1951
1952- (void)updateDialogPresenterActiveState {
1953 self.dialogPresenter.active = self.active && self.viewVisible;
1954}
1955
1956- (void)dismissPopups {
1957 if (_noTabsController.get())
1958 [_noTabsController dismissToolsMenuPopup];
1959 else
1960 [_toolbarController dismissToolsMenuPopup];
1961 [self hidePageInfoPopupForView:nil];
1962 [_toolbarController dismissTabHistoryPopup];
1963 [[_model currentTab].webController recordStateInHistory];
1964}
1965
1966#pragma mark - Tap handling
1967
1968- (void)setLastTapPoint:(id)sender {
1969 CGPoint center;
1970 UIView* parentView = nil;
1971 if ([sender isKindOfClass:[UIView class]]) {
1972 center = [sender center];
1973 parentView = [sender superview];
1974 }
1975 if ([sender isKindOfClass:[ToolsMenuViewItem class]]) {
1976 parentView = [[sender tableViewCell] superview];
1977 center = [[sender tableViewCell] center];
1978 }
1979
1980 if (parentView) {
1981 _lastTapPoint = [parentView convertPoint:center toView:self.view];
1982 _lastTapTime = CACurrentMediaTime();
1983 }
1984}
1985
1986- (CGPoint)lastTapPoint {
1987 if (CACurrentMediaTime() - _lastTapTime < 1) {
1988 return _lastTapPoint;
1989 }
1990 return CGPointZero;
1991}
1992
1993- (void)saveContentAreaTapLocation:(UIGestureRecognizer*)gestureRecognizer {
1994 UIView* view = gestureRecognizer.view;
1995 CGPoint viewCoordinate = [gestureRecognizer locationInView:view];
1996 _lastTapPoint =
1997 [[view superview] convertPoint:viewCoordinate toView:self.view];
1998 _lastTapTime = CACurrentMediaTime();
1999}
2000
2001- (BOOL)addTabIfNoTabWithNormalBrowserState {
2002 if (![_model count]) {
2003 if (!_isOffTheRecord) {
2004 [self addSelectedTabWithURL:GURL(kChromeUINewTabURL)
2005 transition:ui::PAGE_TRANSITION_TYPED];
2006 return YES;
2007 }
2008 }
2009 return NO;
2010}
2011
2012#pragma mark - Tab creation and selection
2013
2014// Called when either a tab finishes loading or when a tab with finished content
2015// is added directly to the model via pre-rendering.
2016- (void)tabLoadComplete:(Tab*)tab withSuccess:(BOOL)success {
2017 DCHECK(tab && ([_model indexOfTab:tab] != NSNotFound));
2018
2019 // Persist the session on a delay.
2020 [_model saveSessionImmediately:NO];
2021}
2022
2023- (Tab*)addSelectedTabWithURL:(const GURL&)url
2024 postData:(TemplateURLRef::PostContent*)postData
2025 transition:(ui::PageTransition)transition {
2026 return [self addSelectedTabWithURL:url
2027 postData:postData
2028 atIndex:[_model count]
2029 transition:transition];
2030}
2031
2032- (Tab*)addSelectedTabWithURL:(const GURL&)url
2033 transition:(ui::PageTransition)transition {
2034 return [self addSelectedTabWithURL:url
2035 atIndex:[_model count]
2036 transition:transition];
2037}
2038
2039- (Tab*)addSelectedTabWithURL:(const GURL&)url
2040 atIndex:(NSUInteger)position
2041 transition:(ui::PageTransition)transition {
2042 return [self addSelectedTabWithURL:url
2043 postData:NULL
2044 atIndex:position
2045 transition:transition];
2046}
2047
2048- (Tab*)addSelectedTabWithURL:(const GURL&)URL
2049 postData:(TemplateURLRef::PostContent*)postData
2050 atIndex:(NSUInteger)position
2051 transition:(ui::PageTransition)transition {
2052 if (position == NSNotFound)
2053 position = [_model count];
2054 DCHECK(position <= [_model count]);
2055
2056 web::NavigationManager::WebLoadParams params(URL);
2057 params.transition_type = transition;
2058 if (postData) {
2059 // Extract the content type and post params from |postData| and add them
2060 // to the load params.
2061 NSString* contentType = base::SysUTF8ToNSString(postData->first);
2062 NSData* data = [NSData dataWithBytes:(void*)postData->second.data()
2063 length:postData->second.length()];
2064 params.post_data.reset([data retain]);
2065 params.extra_headers.reset([@{ @"Content-Type" : contentType } retain]);
2066 }
2067 Tab* tab = [_model insertOrUpdateTabWithLoadParams:params
2068 windowName:nil
2069 opener:nil
2070 openedByDOM:NO
2071 atIndex:position
2072 inBackground:NO];
2073 return tab;
2074}
2075
2076// Whether the given tab's url begins with the chrome prefix.
2077- (BOOL)isTabNativePage:(Tab*)tab {
2078 return tab && tab.url.SchemeIs(kChromeUIScheme);
2079}
2080
2081- (void)expectNewForegroundTab {
2082 _expectingForegroundTab = YES;
2083}
2084
2085- (UIImageView*)pageFullScreenOpenCloseAnimationView {
2086 CGRect viewBounds, remainder;
2087 CGRectDivide(self.view.bounds, &remainder, &viewBounds, StatusBarHeight(),
2088 CGRectMinYEdge);
2089 return [[[UIImageView alloc] initWithFrame:viewBounds] autorelease];
2090}
2091
2092- (UIImageView*)pageOpenCloseAnimationView {
2093 CGRect frame = [_contentArea bounds];
2094
2095 frame.size.height = frame.size.height - [self headerHeight];
2096 frame.origin.y = [self headerHeight];
2097
2098 UIImageView* pageView =
2099 [[[UIImageView alloc] initWithFrame:frame] autorelease];
2100 CGPoint center = CGPointMake(CGRectGetMidX(frame), CGRectGetMidY(frame));
2101 pageView.center = center;
2102
2103 pageView.backgroundColor = [UIColor whiteColor];
2104 return pageView;
2105}
2106
2107- (void)installDelegatesForTab:(Tab*)tab {
2108 // We don't unregister any of this delegation.
2109 // TODO(crbug.com/375577): Unregister these delegates correctly on BVC
2110 // deallocation.
2111 tab.dialogDelegate = self;
2112 tab.snapshotOverlayProvider = self;
2113 tab.storeKitLauncher = self;
2114 tab.passKitDialogProvider = self;
2115 tab.fullScreenControllerDelegate = self;
2116 if (!IsIPadIdiom()) {
2117 tab.overscrollActionsControllerDelegate = self;
2118 }
olivierrobin9ce77b82017-01-12 17:29:192119 tab.tabHeadersDelegate = self;
sdefresnee65fd872016-12-19 13:38:132120 tab.tabSnapshottingDelegate = self;
2121 // Install the proper CRWWebController delegates.
2122 tab.webController.nativeProvider = self;
2123 tab.webController.swipeRecognizerProvider = self.sideSwipeController;
2124 // Delegate will remove itself on destruction.
2125 tab.webState->SetDelegate(_webStateDelegate.get());
2126}
2127
2128// Called when a tab is selected in the model. Make any required view changes.
2129// The notification will not be sent when the tab is already the selected tab.
2130- (void)tabSelected:(Tab*)tab {
2131 DCHECK(tab);
2132
2133 // Ignore changes while the tab stack view is visible (or while suspended).
2134 // The display will be refreshed when this view becomes active again.
2135 if (!self.visible || ![_model webUsageEnabled])
2136 return;
2137
2138 [self displayTab:tab isNewSelection:YES];
2139
2140 if (_expectingForegroundTab && !_inNewTabAnimation) {
2141 // Now that the new tab has been displayed, return to normal. Rather than
2142 // keep a reference to the previous tab, just turn off preview mode for all
2143 // tabs (since doing so is a no-op for the tabs that don't have it set).
2144 _expectingForegroundTab = NO;
2145 for (Tab* tab in _model.get()) {
2146 [tab.webController setOverlayPreviewMode:NO];
2147 }
2148 }
2149}
2150
2151#pragma mark - External files
2152
2153- (NSSet*)referencedExternalFiles {
2154 NSSet* filesReferencedByTabs = [_model currentlyReferencedExternalFiles];
2155
2156 // TODO(noyau): this is incorrect, the caller should know that the model is
2157 // not loaded yet.
sdefresnedc432f42017-01-17 14:36:592158 if (!_bookmarkModel || !_bookmarkModel->loaded())
sdefresnee65fd872016-12-19 13:38:132159 return filesReferencedByTabs;
2160
2161 std::vector<bookmarks::BookmarkModel::URLAndTitle> bookmarks;
2162 _bookmarkModel->GetBookmarks(&bookmarks);
2163 NSMutableSet* bookmarkedFiles = [NSMutableSet set];
2164 for (const auto& bookmark : bookmarks) {
2165 GURL bookmarkUrl = bookmark.url;
2166 if (UrlIsExternalFileReference(bookmarkUrl)) {
2167 [bookmarkedFiles
2168 addObject:base::SysUTF8ToNSString(bookmarkUrl.ExtractFileName())];
2169 }
2170 }
2171 return [filesReferencedByTabs setByAddingObjectsFromSet:bookmarkedFiles];
2172}
2173
2174- (void)removeExternalFilesImmediately:(BOOL)immediately
2175 completionHandler:(ProceduralBlock)completionHandler {
2176 DCHECK_CURRENTLY_ON(web::WebThread::UI);
2177 DCHECK(!_isOffTheRecord);
2178 _externalFileRemover.reset(new ExternalFileRemover(self));
2179 // Delay the cleanup of the unreferenced files received from other apps
2180 // to not impact startup performance.
2181 int delay = immediately ? 0 : kExternalFilesCleanupDelaySeconds;
2182 _externalFileRemover->RemoveAfterDelay(
2183 base::TimeDelta::FromSeconds(delay),
2184 base::BindBlock(completionHandler ? completionHandler
2185 : ^{
2186 }));
2187}
2188
2189#pragma mark - SnapshotOverlayProvider methods
2190
2191- (NSArray*)snapshotOverlaysForTab:(Tab*)tab {
2192 NSMutableArray* overlays = [NSMutableArray array];
2193 if (![_model webUsageEnabled]) {
2194 return overlays;
2195 }
2196 UIView* voiceSearchView = [self voiceSearchOverlayViewForTab:tab];
2197 if (voiceSearchView) {
2198 CGFloat voiceSearchYOffset = [self voiceSearchOverlayYOffsetForTab:tab];
2199 base::scoped_nsobject<SnapshotOverlay> voiceSearchOverlay(
2200 [[SnapshotOverlay alloc] initWithView:voiceSearchView
2201 yOffset:voiceSearchYOffset]);
2202 [overlays addObject:voiceSearchOverlay];
2203 }
2204 UIView* infoBarView = [self infoBarOverlayViewForTab:tab];
2205 if (infoBarView) {
2206 CGFloat infoBarYOffset = [self infoBarOverlayYOffsetForTab:tab];
2207 base::scoped_nsobject<SnapshotOverlay> infoBarOverlay(
2208 [[SnapshotOverlay alloc] initWithView:infoBarView
2209 yOffset:infoBarYOffset]);
2210 [overlays addObject:infoBarOverlay];
2211 }
2212 return overlays;
2213}
2214
2215#pragma mark -
2216
2217- (UIView*)infoBarOverlayViewForTab:(Tab*)tab {
2218 if (IsIPadIdiom()) {
2219 // Not using overlays on iPad because the content is pushed down by
2220 // infobar and the transition between snapshot and fresh page can
2221 // cause both snapshot and real infobars to appear at the same time.
2222 return nil;
2223 }
2224 Tab* currentTab = [_model currentTab];
2225 if (tab && tab == currentTab) {
2226 infobars::InfoBarManager* infoBarManager = [currentTab infoBarManager];
2227 if (infoBarManager->infobar_count() > 0) {
2228 DCHECK(_infoBarContainer);
2229 return _infoBarContainer->view();
2230 }
2231 }
2232 return nil;
2233}
2234
2235- (CGFloat)infoBarOverlayYOffsetForTab:(Tab*)tab {
2236 if (tab != [_model currentTab] || !_infoBarContainer.get()) {
2237 // There is no UI representation for non-current tabs or there is
2238 // no _infoBarContainer instantiated yet.
2239 // Return offset outside of tab.
2240 return CGRectGetMaxY(self.view.frame);
2241 } else if (IsIPadIdiom()) {
2242 // The infobars on iPad are display at the top of a tab.
2243 return CGRectGetMinY([[_model currentTab].webController visibleFrame]);
2244 } else {
2245 // The infobars on iPhone are displayed at the bottom of a tab.
2246 CGRect visibleFrame = [[_model currentTab].webController visibleFrame];
2247 return CGRectGetMaxY(visibleFrame) -
2248 CGRectGetHeight(_infoBarContainer->view().frame);
2249 }
2250}
2251
2252- (UIView*)voiceSearchOverlayViewForTab:(Tab*)tab {
2253 Tab* currentTab = [_model currentTab];
2254 if (tab && tab == currentTab && tab.isVoiceSearchResultsTab &&
2255 _voiceSearchBar && ![_voiceSearchBar isHidden]) {
2256 return _voiceSearchBar;
2257 }
2258 return nil;
2259}
2260
2261- (CGFloat)voiceSearchOverlayYOffsetForTab:(Tab*)tab {
2262 if (tab != [_model currentTab] || [_voiceSearchBar isHidden]) {
2263 // There is no UI representation for non-current tabs or there is
2264 // no visible voice search. Return offset outside of tab.
2265 return CGRectGetMaxY(self.view.frame);
2266 } else {
2267 // The voice search bar on iPhone is displayed at the bottom of a tab.
2268 CGRect visibleFrame = [[_model currentTab].webController visibleFrame];
2269 return CGRectGetMaxY(visibleFrame) - kVoiceSearchBarHeight;
2270 }
2271}
2272
2273- (void)ensureVoiceSearchControllerCreated {
2274 if (!_voiceSearchController.get()) {
2275 VoiceSearchProvider* provider =
2276 ios::GetChromeBrowserProvider()->GetVoiceSearchProvider();
2277 if (provider) {
2278 _voiceSearchController =
2279 provider->CreateVoiceSearchController(_browserState);
2280 _voiceSearchController->SetDelegate(_toolbarController);
2281 }
2282 }
2283}
2284
2285- (void)ensureVoiceSearchBarCreated {
2286 if (_voiceSearchBar)
2287 return;
2288
2289 CGFloat width = CGRectGetWidth([[self view] bounds]);
2290 CGFloat y = CGRectGetHeight([[self view] bounds]) - kVoiceSearchBarHeight;
2291 CGRect frame = CGRectMake(0.0, y, width, kVoiceSearchBarHeight);
kkhorimotoc2cdf6f42017-01-24 21:37:372292 _voiceSearchBar.reset([ios::GetChromeBrowserProvider()
2293 ->GetVoiceSearchProvider()
2294 ->BuildVoiceSearchBar(frame) retain]);
sdefresnee65fd872016-12-19 13:38:132295 [_voiceSearchBar setVoiceSearchBarDelegate:self];
2296 [_voiceSearchBar setHidden:YES];
2297 [_voiceSearchBar setAutoresizingMask:UIViewAutoresizingFlexibleTopMargin |
2298 UIViewAutoresizingFlexibleWidth];
2299 [self.view insertSubview:_voiceSearchBar
2300 belowSubview:_infoBarContainer->view()];
2301}
2302
2303- (void)updateVoiceSearchBarVisibilityAnimated:(BOOL)animated {
2304 // Voice search bar exists and is shown/hidden.
2305 BOOL show = self.shouldShowVoiceSearchBar;
2306 if (_voiceSearchBar && _voiceSearchBar.get().hidden != show)
2307 return;
2308
2309 // Voice search bar doesn't exist and thus is not visible.
2310 if (!_voiceSearchBar && !show)
2311 return;
2312
2313 if (animated)
2314 [_voiceSearchBar.get() animateToBecomeVisible:show];
2315 else
2316 _voiceSearchBar.get().hidden = !show;
2317}
2318
2319- (id<LogoAnimationControllerOwner>)currentLogoAnimationControllerOwner {
2320 Protocol* ownerProtocol = @protocol(LogoAnimationControllerOwner);
2321 if ([_voiceSearchBar conformsToProtocol:ownerProtocol] &&
2322 self.shouldShowVoiceSearchBar) {
2323 // Use |_voiceSearchBar| for VoiceSearch results tab and dismissal
2324 // animations.
2325 return static_cast<id<LogoAnimationControllerOwner>>(_voiceSearchBar.get());
2326 }
2327 id currentNativeController =
2328 [self nativeControllerForTab:self.tabModel.currentTab];
2329 Protocol* possibleOwnerProtocol =
2330 @protocol(LogoAnimationControllerOwnerOwner);
2331 if ([currentNativeController conformsToProtocol:possibleOwnerProtocol] &&
2332 [currentNativeController logoAnimationControllerOwner]) {
2333 // If the current native controller is showing a GLIF view (e.g. the NTP
2334 // when there is no doodle), use that GLIFControllerOwner.
2335 return [currentNativeController logoAnimationControllerOwner];
2336 }
2337 return nil;
2338}
2339
2340#pragma mark - PassKitDialogProvider methods
2341
2342- (void)presentPassKitDialog:(NSData*)data {
2343 NSError* error = nil;
2344 base::scoped_nsobject<PKPass> pass;
2345 if (data)
2346 pass.reset([[PKPass alloc] initWithData:data error:&error]);
2347 if (error || !data) {
2348 if ([_model currentTab]) {
2349 infobars::InfoBarManager* infoBarManager =
2350 [[_model currentTab] infoBarManager];
2351 // TODO(crbug.com/227994): Infobar cleanup (infoBarManager should never be
2352 // NULL, replace if with DCHECK).
2353 if (infoBarManager)
2354 [_dependencyFactory showPassKitErrorInfoBarForManager:infoBarManager];
2355 }
2356 } else {
2357 PKAddPassesViewController* passKitViewController =
2358 [_dependencyFactory newPassKitViewControllerForPass:pass];
2359 if (passKitViewController) {
2360 [self presentViewController:passKitViewController
2361 animated:YES
2362 completion:^{
2363 }];
2364 }
2365 }
2366}
2367
2368- (UIStatusBarStyle)preferredStatusBarStyle {
2369 return (IsIPadIdiom() || _isOffTheRecord) ? UIStatusBarStyleLightContent
2370 : UIStatusBarStyleDefault;
2371}
2372
2373#pragma mark - CRWWebStateDelegate methods.
2374
eugenebut75a06fa72017-01-09 17:09:552375- (web::WebState*)webState:(web::WebState*)webState
2376 openURLWithParams:(const web::WebState::OpenURLParams&)params {
2377 switch (params.disposition) {
2378 case WindowOpenDisposition::NEW_FOREGROUND_TAB:
2379 case WindowOpenDisposition::NEW_BACKGROUND_TAB: {
2380 Tab* tab = [[self tabModel]
2381 insertOrUpdateTabWithURL:params.url
2382 referrer:params.referrer
2383 transition:params.transition
2384 windowName:nil
2385 opener:[self tabForWebState:webState]
2386 openedByDOM:NO
2387 atIndex:TabModelConstants::kTabPositionAutomatically
2388 inBackground:(params.disposition ==
2389 WindowOpenDisposition::NEW_BACKGROUND_TAB)];
2390 return tab.webState;
2391 }
2392 case WindowOpenDisposition::CURRENT_TAB: {
2393 web::NavigationManager::WebLoadParams loadParams(params.url);
2394 loadParams.referrer = params.referrer;
2395 loadParams.transition_type = params.transition;
2396 loadParams.is_renderer_initiated = params.is_renderer_initiated;
2397 webState->GetNavigationManager()->LoadURLWithParams(loadParams);
2398 return webState;
2399 }
2400 default:
2401 NOTIMPLEMENTED();
2402 return nullptr;
2403 };
2404}
2405
sdefresnee65fd872016-12-19 13:38:132406- (void)webState:(web::WebState*)webState didChangeProgress:(double)progress {
2407 if (webState == [_model currentTab].webState) {
2408 // TODO(crbug.com/546406): It is probably possible to do something smarter,
2409 // but the fact that this is not always sent will have to be taken into
2410 // account.
2411 [self updateToolbar];
2412 }
2413}
2414
2415- (BOOL)webState:(web::WebState*)webState
2416 handleContextMenu:(const web::ContextMenuParams&)params {
2417 // Prevent context menu from displaying for a tab which is no longer the
2418 // current one.
2419 if (webState != [_model currentTab].webState) {
2420 return NO;
2421 }
2422
2423 // No custom context menu if no valid url is available in |params|.
2424 if (!params.link_url.is_valid() && !params.src_url.is_valid()) {
2425 return NO;
2426 }
2427
2428 DCHECK(_browserState);
2429 DCHECK([_model currentTab]);
2430
2431 _contextMenuCoordinator.reset([[ContextMenuCoordinator alloc]
2432 initWithBaseViewController:self
2433 params:params]);
2434
2435 NSString* title = nil;
2436 ProceduralBlock action = nil;
2437
2438 base::WeakNSObject<BrowserViewController> weakSelf(self);
2439 GURL link = params.link_url;
2440 bool isLink = link.is_valid();
2441 GURL imageUrl = params.src_url;
2442 bool isImage = imageUrl.is_valid();
2443
2444 if (isLink) {
2445 if (link.SchemeIs(url::kJavaScriptScheme)) {
2446 // Open
2447 title = l10n_util::GetNSStringWithFixup(IDS_IOS_CONTENT_CONTEXT_OPEN);
2448 action = ^{
2449 Record(ACTION_OPEN_JAVASCRIPT, isImage, isLink);
2450 [weakSelf openJavascript:base::SysUTF8ToNSString(link.GetContent())];
2451 };
2452 [_contextMenuCoordinator addItemWithTitle:title action:action];
2453 }
2454
2455 if (web::UrlHasWebScheme(link)) {
2456 web::Referrer referrer([_model currentTab].url, params.referrer_policy);
2457
sdefresnee65fd872016-12-19 13:38:132458 // Open in New Tab.
2459 title = l10n_util::GetNSStringWithFixup(
2460 IDS_IOS_CONTENT_CONTEXT_OPENLINKNEWTAB);
2461 action = ^{
2462 Record(ACTION_OPEN_IN_NEW_TAB, isImage, isLink);
2463 [weakSelf webPageOrderedOpen:link
2464 referrer:referrer
2465 windowName:nil
2466 inBackground:YES
2467 appendTo:kCurrentTab];
2468 };
2469 [_contextMenuCoordinator addItemWithTitle:title action:action];
2470 if (!_isOffTheRecord) {
2471 // Open in Incognito Tab.
2472 title = l10n_util::GetNSStringWithFixup(
2473 IDS_IOS_CONTENT_CONTEXT_OPENLINKNEWINCOGNITOTAB);
2474 action = ^{
2475 Record(ACTION_OPEN_IN_INCOGNITO_TAB, isImage, isLink);
2476 [weakSelf webPageOrderedOpen:link
2477 referrer:referrer
2478 windowName:nil
2479 inIncognito:YES
2480 inBackground:NO
2481 appendTo:kCurrentTab];
2482 };
2483 [_contextMenuCoordinator addItemWithTitle:title action:action];
2484 }
olivierrobin51d4cf42017-01-17 13:32:352485 }
2486 if (link.SchemeIsHTTPOrHTTPS() &&
2487 reading_list::switches::IsReadingListEnabled()) {
2488 NSString* innerText = params.link_text;
2489 if ([innerText length] > 0) {
2490 // Add to reading list.
2491 title = l10n_util::GetNSStringWithFixup(
2492 IDS_IOS_CONTENT_CONTEXT_ADDTOREADINGLIST);
2493 action = ^{
2494 Record(ACTION_READ_LATER, isImage, isLink);
2495 [weakSelf addToReadingListURL:link title:innerText];
2496 };
2497 [_contextMenuCoordinator addItemWithTitle:title action:action];
gambard5fd403492017-01-17 09:17:532498 }
sdefresnee65fd872016-12-19 13:38:132499 }
2500 // Copy Link.
2501 title = l10n_util::GetNSStringWithFixup(IDS_IOS_CONTENT_CONTEXT_COPY);
2502 action = ^{
2503 Record(ACTION_COPY_LINK_ADDRESS, isImage, isLink);
2504 NSURL* url = net::NSURLWithGURL(link);
2505 NSDictionary* item = @{
2506 (NSString*)kUTTypeURL : url,
2507 (NSString*)kUTTypeUTF8PlainText :
2508 [[url absoluteString] dataUsingEncoding:NSUTF8StringEncoding],
2509 };
2510 [[UIPasteboard generalPasteboard] setItems:@[ item ]];
2511 };
2512 [_contextMenuCoordinator addItemWithTitle:title action:action];
2513 }
2514 if (isImage) {
2515 web::Referrer referrer([_model currentTab].url, params.referrer_policy);
2516 // Save Image.
2517 if (experimental_flags::IsDownloadRenamingEnabled()) {
2518 title = l10n_util::GetNSStringWithFixup(
2519 IDS_IOS_CONTENT_CONTEXT_DOWNLOADIMAGE);
2520 } else {
2521 title =
2522 l10n_util::GetNSStringWithFixup(IDS_IOS_CONTENT_CONTEXT_SAVEIMAGE);
2523 }
2524 action = ^{
2525 Record(ACTION_SAVE_IMAGE, isImage, isLink);
2526 [weakSelf saveImageAtURL:imageUrl referrer:referrer];
2527 };
2528 [_contextMenuCoordinator addItemWithTitle:title action:action];
2529 // Open Image.
2530 title = l10n_util::GetNSStringWithFixup(IDS_IOS_CONTENT_CONTEXT_OPENIMAGE);
2531 action = ^{
2532 Record(ACTION_OPEN_IMAGE, isImage, isLink);
2533 [weakSelf loadURL:imageUrl
2534 referrer:referrer
2535 transition:ui::PAGE_TRANSITION_LINK
2536 rendererInitiated:YES];
2537 };
2538 [_contextMenuCoordinator addItemWithTitle:title action:action];
2539 // Open Image In New Tab.
2540 title = l10n_util::GetNSStringWithFixup(
2541 IDS_IOS_CONTENT_CONTEXT_OPENIMAGENEWTAB);
2542 action = ^{
2543 Record(ACTION_OPEN_IMAGE_IN_NEW_TAB, isImage, isLink);
2544 [weakSelf webPageOrderedOpen:imageUrl
2545 referrer:referrer
2546 windowName:nil
2547 inBackground:true
2548 appendTo:kCurrentTab];
2549 };
2550 [_contextMenuCoordinator addItemWithTitle:title action:action];
2551
2552 TemplateURLService* service =
2553 ios::TemplateURLServiceFactory::GetForBrowserState(_browserState);
2554 TemplateURL* defaultURL = service->GetDefaultSearchProvider();
2555 if (defaultURL && !defaultURL->image_url().empty() &&
2556 defaultURL->image_url_ref().IsValid(service->search_terms_data())) {
2557 title = l10n_util::GetNSStringF(IDS_IOS_CONTEXT_MENU_SEARCHWEBFORIMAGE,
2558 defaultURL->short_name());
2559 action = ^{
2560 Record(ACTION_SEARCH_BY_IMAGE, isImage, isLink);
2561 [weakSelf searchByImageAtURL:imageUrl referrer:referrer];
2562 };
2563 [_contextMenuCoordinator addItemWithTitle:title action:action];
2564 }
2565 }
2566
2567 [_contextMenuCoordinator start];
2568 return YES;
2569}
2570
2571- (web::JavaScriptDialogPresenter*)javaScriptDialogPresenterForWebState:
2572 (web::WebState*)webState {
2573 return _javaScriptDialogPresenter.get();
2574}
2575
eugenebut63232102017-01-19 16:19:402576- (void)webState:(web::WebState*)webState
2577 didRequestHTTPAuthForProtectionSpace:(NSURLProtectionSpace*)protectionSpace
2578 proposedCredential:(NSURLCredential*)proposedCredential
2579 completionHandler:(void (^)(NSString* username,
2580 NSString* password))handler {
2581 [self.dialogPresenter runAuthDialogForProtectionSpace:protectionSpace
2582 proposedCredential:proposedCredential
2583 webState:webState
2584 completionHandler:handler];
2585}
2586
sdefresnee65fd872016-12-19 13:38:132587#pragma mark - FullScreenControllerDelegate methods
2588
2589- (CGFloat)headerOffset {
2590 if (IsIPadIdiom())
2591 return StatusBarHeight();
2592 return 0.0;
2593}
2594
2595- (const std::vector<HeaderDefinition>)headerViews {
2596 std::vector<HeaderDefinition> results;
2597 if (![self isViewLoaded])
2598 return results;
2599
2600 if (!IsIPadIdiom()) {
2601 if ([_toolbarController view]) {
2602 HeaderDefinition header = {
2603 base::scoped_nsobject<UIView>([[_toolbarController view] retain]),
2604 Hideable, [ToolbarController toolbarDropShadowHeight], 0.0,
2605 };
2606 results.push_back(header);
2607 }
2608 } else {
2609 if ([_tabStripController view]) {
2610 HeaderDefinition header = {
2611 base::scoped_nsobject<UIView>([[_tabStripController view] retain]),
2612 Hideable, 0.0, 0.0,
2613 };
2614 results.push_back(header);
2615 }
2616 if ([_toolbarController view]) {
2617 HeaderDefinition header = {
2618 base::scoped_nsobject<UIView>([[_toolbarController view] retain]),
2619 Hideable, [ToolbarController toolbarDropShadowHeight], 0.0,
2620 };
2621 results.push_back(header);
2622 }
2623 if ([_findBarController view]) {
2624 HeaderDefinition header = {
2625 base::scoped_nsobject<UIView>([[_findBarController view] retain]),
2626 Overlap, 0.0, kIPadFindBarOverlap,
2627 };
2628 results.push_back(header);
2629 }
2630 }
2631 return results;
2632}
2633
2634- (UIView*)footerView {
2635 return _voiceSearchBar;
2636}
2637
2638- (CGFloat)headerHeight {
2639 return [self headerHeightForTab:[_model currentTab]];
2640}
2641
2642- (CGFloat)headerHeightForTab:(Tab*)tab {
2643 id nativeController = [self nativeControllerForTab:tab];
2644 if ([nativeController conformsToProtocol:@protocol(ToolbarOwner)] &&
2645 [nativeController respondsToSelector:@selector(toolbarHeight)] &&
2646 [nativeController toolbarHeight] > 0.0 && !IsIPadIdiom()) {
2647 // On iPhone, don't add any header height for ToolbarOwner native
2648 // controllers when they're displaying their own toolbar.
2649 return 0;
2650 }
2651
2652 const std::vector<HeaderDefinition> views = [self headerViews];
2653
2654 CGFloat height = [self headerOffset];
2655 for (const auto& header : views) {
2656 if (header.view && header.behaviour == Hideable) {
2657 height += CGRectGetHeight([header.view frame]) -
2658 header.heightAdjustement - header.inset;
2659 }
2660 }
2661
2662 return height - StatusBarHeight();
2663}
2664
2665- (BOOL)isTabWithIDCurrent:(NSString*)sessionID {
sdefresneb7309482017-01-23 17:14:192666 return self.visible && [sessionID isEqualToString:[_model currentTab].tabId];
sdefresnee65fd872016-12-19 13:38:132667}
2668
2669- (CGFloat)currentHeaderOffset {
2670 const std::vector<HeaderDefinition> headers = [self headerViews];
2671 if (!headers.size())
2672 return 0.0;
2673
2674 // Prerender tab does not have a toolbar, return |headerHeight| as promised by
2675 // API documentation.
2676 if ([[[self tabModel] currentTab] isPrerenderTab])
2677 return [self headerHeight];
2678
2679 UIView* topHeader = headers[0].view;
2680 return -(topHeader.frame.origin.y - [self headerOffset]);
2681}
2682
2683- (CGFloat)footerYForHeaderOffset:(CGFloat)headerOffset {
2684 UIView* footer = [self footerView];
2685 CGFloat headerHeight = [self headerHeight];
2686 if (!footer || headerHeight == 0)
2687 return 0.0;
2688
2689 CGFloat footerHeight = CGRectGetHeight(footer.frame);
2690 CGFloat offset = headerOffset * footerHeight / headerHeight;
2691 return std::ceil(CGRectGetHeight(self.view.bounds) - footerHeight + offset);
2692}
2693
2694- (void)fullScreenController:(FullScreenController*)controller
2695 headerAnimationCompleted:(BOOL)completed
2696 offset:(CGFloat)offset {
2697 if (completed)
justincohen04c27772016-12-21 20:16:592698 [controller setToolbarInsetsForHeaderOffset:offset];
sdefresnee65fd872016-12-19 13:38:132699}
2700
2701- (void)setFramesForHeaders:(const std::vector<HeaderDefinition>)headers
2702 atOffset:(CGFloat)headerOffset {
2703 CGFloat height = [self headerOffset];
2704 for (const auto& header : headers) {
2705 CGRect frame = [header.view frame];
2706 frame.origin.y = height - headerOffset - header.inset;
2707 [header.view setFrame:frame];
2708 if (header.behaviour != Overlap)
2709 height += CGRectGetHeight(frame);
2710 }
2711}
2712
2713- (void)fullScreenController:(FullScreenController*)fullScreenController
2714 drawHeaderViewFromOffset:(CGFloat)headerOffset
2715 animate:(BOOL)animate {
2716 if ([_sideSwipeController inSwipe])
2717 return;
2718
2719 CGRect footerFrame = CGRectZero;
2720 UIView* footer = nil;
2721 // Only animate the voice search bar if the tab is a voice search results tab.
2722 if ([_model currentTab].isVoiceSearchResultsTab) {
2723 footer = [self footerView];
2724 footerFrame = footer.frame;
2725 footerFrame.origin.y = [self footerYForHeaderOffset:headerOffset];
2726 }
2727
2728 const std::vector<HeaderDefinition> headers = [self headerViews];
2729 void (^block)(void) = ^{
2730 [self setFramesForHeaders:headers atOffset:headerOffset];
2731 footer.frame = footerFrame;
2732 };
2733 void (^completion)(BOOL) = ^(BOOL finished) {
2734 [self fullScreenController:fullScreenController
2735 headerAnimationCompleted:finished
2736 offset:headerOffset];
2737 };
2738 if (animate) {
2739 [UIView animateWithDuration:ios_internal::kToolbarAnimationDuration
2740 delay:0.0
2741 options:UIViewAnimationOptionBeginFromCurrentState
2742 animations:block
2743 completion:completion];
2744 } else {
2745 block();
2746 completion(YES);
2747 }
2748}
2749
2750- (void)fullScreenController:(FullScreenController*)fullScreenController
2751 drawHeaderViewFromOffset:(CGFloat)headerOffset
2752 onWebViewProxy:(id<CRWWebViewProxy>)webViewProxy
2753 changeTopContentPadding:(BOOL)changeTopContentPadding
2754 scrollingToOffset:(CGFloat)contentOffset {
2755 DCHECK(webViewProxy);
2756 if ([_sideSwipeController inSwipe])
2757 return;
2758
2759 CGRect footerFrame;
2760 UIView* footer = nil;
2761 // Only animate the voice search bar if the tab is a voice search results tab.
2762 if ([_model currentTab].isVoiceSearchResultsTab) {
2763 footer = [self footerView];
2764 footerFrame = footer.frame;
2765 footerFrame.origin.y = [self footerYForHeaderOffset:headerOffset];
2766 }
2767
2768 const std::vector<HeaderDefinition> headers = [self headerViews];
2769 void (^block)(void) = ^{
2770 [self setFramesForHeaders:headers atOffset:headerOffset];
2771 footer.frame = footerFrame;
2772 webViewProxy.scrollViewProxy.contentOffset = CGPointMake(
2773 webViewProxy.scrollViewProxy.contentOffset.x, contentOffset);
2774 if (changeTopContentPadding)
2775 webViewProxy.topContentPadding = contentOffset;
2776 };
2777 void (^completion)(BOOL) = ^(BOOL finished) {
2778 [self fullScreenController:fullScreenController
2779 headerAnimationCompleted:finished
2780 offset:headerOffset];
2781 };
2782
2783 [UIView animateWithDuration:ios_internal::kToolbarAnimationDuration
2784 delay:0.0
2785 options:UIViewAnimationOptionBeginFromCurrentState
2786 animations:block
2787 completion:completion];
2788}
2789
2790#pragma mark - VoiceSearchBarOwner
2791
2792- (id<VoiceSearchBar>)voiceSearchBar {
2793 return _voiceSearchBar;
2794}
2795
2796#pragma mark - Install OverScrollActionController method.
2797- (void)setOverScrollActionControllerToStaticNativeContent:
2798 (StaticHtmlNativeContent*)nativeContent {
2799 if (!IsIPadIdiom() && !FirstRun::IsChromeFirstRun()) {
2800 OverscrollActionsController* controller =
2801 [[[OverscrollActionsController alloc]
2802 initWithScrollView:[nativeContent scrollView]] autorelease];
sdefresnee65fd872016-12-19 13:38:132803 [controller setDelegate:self];
rohitrao922b7111c2017-01-03 14:31:052804 OverscrollStyle style = _isOffTheRecord
2805 ? OverscrollStyle::REGULAR_PAGE_INCOGNITO
2806 : OverscrollStyle::REGULAR_PAGE_NON_INCOGNITO;
sdefresnee65fd872016-12-19 13:38:132807 controller.style = style;
2808 nativeContent.overscrollActionsController = controller;
2809 }
2810}
2811
2812#pragma mark - OverscrollActionsControllerDelegate methods.
2813
2814- (void)overscrollActionsController:(OverscrollActionsController*)controller
rohitrao922b7111c2017-01-03 14:31:052815 didTriggerAction:(OverscrollAction)action {
sdefresnee65fd872016-12-19 13:38:132816 switch (action) {
rohitrao922b7111c2017-01-03 14:31:052817 case OverscrollAction::NEW_TAB:
sdefresnee65fd872016-12-19 13:38:132818 [self newTab:nil];
2819 break;
rohitrao922b7111c2017-01-03 14:31:052820 case OverscrollAction::CLOSE_TAB:
sdefresnee65fd872016-12-19 13:38:132821 [self closeCurrentTab];
2822 break;
rohitrao922b7111c2017-01-03 14:31:052823 case OverscrollAction::REFRESH:
sdefresnee65fd872016-12-19 13:38:132824 if ([[[_model currentTab] webController] loadPhase] ==
2825 web::PAGE_LOADING) {
2826 [[_model currentTab] stopLoading];
2827 }
2828 [[_model currentTab] reload];
2829 break;
rohitrao922b7111c2017-01-03 14:31:052830 case OverscrollAction::NONE:
sdefresnee65fd872016-12-19 13:38:132831 NOTREACHED();
2832 break;
2833 }
2834}
2835
2836- (BOOL)shouldAllowOverscrollActions {
2837 return YES;
2838}
2839
2840- (UIView*)headerView {
2841 return [_toolbarController view];
2842}
2843
2844- (UIView*)toolbarSnapshotView {
2845 return [[_toolbarController view] snapshotViewAfterScreenUpdates:NO];
2846}
2847
2848- (CGFloat)overscrollActionsControllerHeaderInset:
2849 (OverscrollActionsController*)controller {
2850 if (controller == [[[self tabModel] currentTab] overscrollActionsController])
2851 return [self headerHeight];
2852 else
2853 return 0;
2854}
2855
2856- (CGFloat)overscrollHeaderHeight {
2857 return [self headerHeight] + StatusBarHeight();
2858}
2859
2860#pragma mark - TabSnapshottingDelegate methods.
2861
2862- (CGRect)snapshotContentAreaForTab:(Tab*)tab {
2863 CGRect pageContentArea = _contentArea.bounds;
2864 if ([_model webUsageEnabled])
2865 pageContentArea = tab.view.bounds;
2866 CGFloat headerHeight = [self headerHeightForTab:tab];
2867 id nativeController = [self nativeControllerForTab:tab];
2868 if ([nativeController respondsToSelector:@selector(toolbarHeight)])
2869 headerHeight += [nativeController toolbarHeight];
2870 UIEdgeInsets contentInsets = UIEdgeInsetsMake(headerHeight, 0.0, 0.0, 0.0);
2871 return UIEdgeInsetsInsetRect(pageContentArea, contentInsets);
2872}
2873
2874#pragma mark - NewTabPageObserver methods.
2875
2876- (void)selectedPanelDidChange {
2877 [self updateToolbar];
2878}
2879
2880#pragma mark - CRWNativeContentProvider methods
2881
2882- (id<CRWNativeContent>)controllerForURL:(const GURL&)url
2883 withError:(NSError*)error
2884 isPost:(BOOL)isPost {
2885 ErrorPageContent* errorPageContent =
2886 [[[ErrorPageContent alloc] initWithLoader:self
2887 browserState:self.browserState
2888 url:url
2889 error:error
2890 isPost:isPost
2891 isIncognito:_isOffTheRecord] autorelease];
2892 [self setOverScrollActionControllerToStaticNativeContent:errorPageContent];
2893 return errorPageContent;
2894}
2895
2896- (BOOL)hasControllerForURL:(const GURL&)url {
2897 std::string host(url.host());
2898
2899 return host == kChromeUINewTabHost || host == kChromeUIBookmarksHost ||
2900 host == kChromeUITermsHost || host == kChromeUIOfflineHost;
2901}
2902
2903- (id<CRWNativeContent>)controllerForURL:(const GURL&)url {
2904 DCHECK(url.SchemeIs(kChromeUIScheme));
2905
2906 id<CRWNativeContent> nativeController = nil;
2907 std::string url_host = url.host();
2908 if (url_host == kChromeUINewTabHost || url_host == kChromeUIBookmarksHost) {
2909 NewTabPageController* pageController =
2910 [[[NewTabPageController alloc] initWithUrl:url
2911 loader:self
2912 focuser:_toolbarController
2913 ntpObserver:self
2914 browserState:_browserState
2915 colorCache:_dominantColorCache
2916 webToolbarDelegate:self
2917 tabModel:_model] autorelease];
2918 pageController.swipeRecognizerProvider = self.sideSwipeController;
2919
2920 // Panel is always NTP for iPhone.
2921 NewTabPage::PanelIdentifier panelType = NewTabPage::kMostVisitedPanel;
2922
2923 if (IsIPadIdiom()) {
2924 // New Tab Page can have multiple panels. Each panel is addressable
2925 // by a #fragment, e.g. chrome://newtab/#most_visited takes user to
2926 // the Most Visited page, chrome://newtab/#bookmarks takes user to
2927 // the Bookmark Manager, etc.
2928 // The utility functions NewTabPage::IdentifierFromFragment() and
2929 // FragmentFromIdentifier() map an identifier to/from a #fragment.
2930 // If the URL is chrome://bookmarks, pre-select the #bookmarks panel
2931 // without changing the URL since the URL may be chrome://bookmarks/#123.
2932 // If the URL is chrome://newtab/, pre-select the panel based on the
2933 // #fragment.
2934 panelType = url_host == kChromeUIBookmarksHost
2935 ? NewTabPage::kBookmarksPanel
2936 : NewTabPage::IdentifierFromFragment(url.ref());
2937 }
2938 [pageController selectPanel:panelType];
2939 nativeController = pageController;
2940 } else if (url_host == kChromeUITermsHost) {
2941 const std::string& filename = GetTermsOfServicePath();
2942
2943 StaticHtmlNativeContent* staticNativeController =
2944 [[[StaticHtmlNativeContent alloc]
2945 initWithResourcePathResource:base::SysUTF8ToNSString(filename)
2946 loader:self
2947 browserState:_browserState
2948 url:GURL(kChromeUITermsURL)] autorelease];
2949 [self setOverScrollActionControllerToStaticNativeContent:
2950 staticNativeController];
2951 nativeController = staticNativeController;
2952 } else if (url_host == kChromeUIOfflineHost) {
2953 StaticHtmlNativeContent* staticNativeController =
2954 [[[OfflinePageNativeContent alloc] initWithLoader:self
2955 browserState:_browserState
2956 webState:[self currentWebState]
2957 URL:url] autorelease];
2958 [self setOverScrollActionControllerToStaticNativeContent:
2959 staticNativeController];
2960 nativeController = staticNativeController;
2961 } else if (url_host == kChromeUIExternalFileHost) {
2962 // Return an instance of the |ExternalFileController| only if the file is
2963 // still in the sandbox.
2964 NSString* filePath = [ExternalFileController pathForExternalFileURL:url];
2965 if ([[NSFileManager defaultManager] fileExistsAtPath:filePath]) {
2966 nativeController = [[[ExternalFileController alloc]
2967 initWithURL:url
2968 browserState:_browserState] autorelease];
2969 }
2970 } else {
2971 DCHECK(![self hasControllerForURL:url]);
2972 // In any other case the PageNotAvailableController is returned.
2973 nativeController =
2974 [[[PageNotAvailableController alloc] initWithUrl:url] autorelease];
2975 if (url_host == kChromeUIHistoryFrameHost) {
2976 base::mac::ObjCCastStrict<PageNotAvailableController>(nativeController)
2977 .descriptionText = l10n_util::GetNSStringFWithFixup(
2978 IDS_IOS_HISTORY_URL_NOT_AVAILABLE,
2979 base::UTF8ToUTF16(kChromeUIHistoryURL),
2980 l10n_util::GetStringUTF16(IDS_HISTORY_SHOW_HISTORY));
2981 }
2982 }
2983 // If a native controller is vended before its tab is added to the tab model,
2984 // use the temporary key and add it under the new tab's tabId in the
2985 // TabModelObserver callback. This happens:
2986 // - when there is no current tab (occurs when vending the NTP controller for
2987 // the first tab that is opened),
2988 // - when the current tab's url doesn't match |url| (occurs when a native
2989 // controller is opened in a new tab)
2990 // - when the current tab's url matches |url| and there is already a native
2991 // controller of the appropriate type vended to it (occurs when a native
2992 // controller is opened in a new tab from a tab with a matching URL, e.g.
2993 // opening an NTP when an NTP is already displayed in the current tab).
2994 // For normal page loads, history navigations, tab restorations, and crash
2995 // recoveries, the tab will already exist in the tab model and the tabId can
2996 // be used as the native controller key.
2997 // TODO(crbug.com/498568): To reduce complexity here, refactor the flow so
2998 // that native controllers vended here always correspond to the current tab.
2999 Tab* currentTab = [_model currentTab];
3000 NSString* nativeControllerKey = currentTab.tabId;
3001 if (!currentTab || currentTab.url != url ||
3002 [[_nativeControllersForTabIDs objectForKey:nativeControllerKey]
3003 isKindOfClass:[nativeController class]]) {
3004 nativeControllerKey = kNativeControllerTemporaryKey;
3005 }
3006 DCHECK(nativeControllerKey);
3007 [_nativeControllersForTabIDs setObject:nativeController
3008 forKey:nativeControllerKey];
3009 return nativeController;
3010}
3011
3012- (id)nativeControllerForTab:(Tab*)tab {
3013 id nativeController = [_nativeControllersForTabIDs objectForKey:tab.tabId];
3014 if (!nativeController) {
3015 // If there is no controller, check for a native controller stored under
3016 // the temporary key.
3017 nativeController = [_nativeControllersForTabIDs
3018 objectForKey:kNativeControllerTemporaryKey];
3019 }
3020 return nativeController;
3021}
3022
3023#pragma mark - DialogPresenterDelegate methods
3024
3025- (void)dialogPresenter:(DialogPresenter*)presenter
3026 willShowDialogForWebState:(web::WebState*)webState {
3027 for (Tab* iteratedTab in self.tabModel) {
3028 if ([iteratedTab webState] == webState) {
3029 self.tabModel.currentTab = iteratedTab;
3030 DCHECK([[iteratedTab view] isDescendantOfView:self.contentArea]);
3031 break;
3032 }
3033 }
3034}
3035
3036#pragma mark - Context menu methods
3037
3038- (void)searchByImageAtURL:(const GURL&)url
3039 referrer:(const web::Referrer)referrer {
3040 DCHECK(url.is_valid());
3041 base::WeakNSObject<BrowserViewController> weakSelf(self);
3042 web::ImageFetchedCallback callback =
3043 ^(const GURL& original_url, int response_code, NSData* data) {
3044 GURL originalURL(original_url.spec());
3045 DCHECK(data);
3046 dispatch_async(dispatch_get_main_queue(), ^{
3047 [weakSelf searchByImageData:data atURL:originalURL];
3048 });
3049 };
3050 _imageFetcher->StartDownload(
3051 url, callback, web::ReferrerHeaderValueForNavigation(url, referrer),
3052 web::PolicyForNavigation(url, referrer));
3053}
3054
3055- (void)searchByImageData:(NSData*)data atURL:(const GURL&)imageURL {
3056 NSData* imageData = data;
3057 UIImage* image = [UIImage imageWithData:imageData];
3058 // Downsize the image if its area exceeds kSearchByImageMaxImageArea AND
3059 // (either its width exceeds kSearchByImageMaxImageWidth OR its height exceeds
3060 // kSearchByImageMaxImageHeight).
3061 if (image &&
3062 image.size.height * image.size.width > kSearchByImageMaxImageArea &&
3063 (image.size.width > kSearchByImageMaxImageWidth ||
3064 image.size.height > kSearchByImageMaxImageHeight)) {
3065 CGSize newImageSize =
3066 CGSizeMake(kSearchByImageMaxImageWidth, kSearchByImageMaxImageHeight);
3067 image = [image gtm_imageByResizingToSize:newImageSize
3068 preserveAspectRatio:YES
3069 trimToFit:NO];
3070 imageData = UIImageJPEGRepresentation(image, 1.0);
3071 }
3072
3073 char const* bytes = reinterpret_cast<const char*>([imageData bytes]);
3074 std::string byteString(bytes, [imageData length]);
3075
3076 TemplateURLService* templateUrlService =
3077 ios::TemplateURLServiceFactory::GetForBrowserState(_browserState);
3078 TemplateURL* defaultURL = templateUrlService->GetDefaultSearchProvider();
3079 DCHECK(!defaultURL->image_url().empty());
3080 DCHECK(defaultURL->image_url_ref().IsValid(
3081 templateUrlService->search_terms_data()));
3082 TemplateURLRef::SearchTermsArgs search_args(base::ASCIIToUTF16(""));
3083 search_args.image_url = imageURL;
3084 search_args.image_thumbnail_content = byteString;
3085
3086 // Generate the URL and populate |post_content| with the content type and
3087 // HTTP body for the request.
3088 TemplateURLRef::PostContent post_content;
3089 GURL result(defaultURL->image_url_ref().ReplaceSearchTerms(
3090 search_args, templateUrlService->search_terms_data(), &post_content));
3091 [self addSelectedTabWithURL:result
3092 postData:&post_content
3093 transition:ui::PAGE_TRANSITION_TYPED];
3094}
3095
3096- (void)saveImageAtURL:(const GURL&)url
3097 referrer:(const web::Referrer&)referrer {
3098 DCHECK(url.is_valid());
3099
sdefresnee65fd872016-12-19 13:38:133100 web::ImageFetchedCallback callback =
3101 ^(const GURL& original_url, int response_code, NSData* data) {
3102 DCHECK(data);
3103
gambard16cfe1f2016-12-21 13:12:093104 [self managePermissionAndSaveImage:data];
sdefresnee65fd872016-12-19 13:38:133105 };
3106 _imageFetcher->StartDownload(
3107 url, callback, web::ReferrerHeaderValueForNavigation(url, referrer),
3108 web::PolicyForNavigation(url, referrer));
3109}
3110
gambard16cfe1f2016-12-21 13:12:093111- (void)managePermissionAndSaveImage:(NSData*)data {
sdefresnee65fd872016-12-19 13:38:133112 switch ([PHPhotoLibrary authorizationStatus]) {
3113 // User was never asked for permission to access photos.
3114 case PHAuthorizationStatusNotDetermined:
3115 [PHPhotoLibrary requestAuthorization:^(PHAuthorizationStatus status) {
3116 // Call -saveImage again to check if chrome needs to display an error or
3117 // saves the image.
3118 if (status != PHAuthorizationStatusNotDetermined)
gambard16cfe1f2016-12-21 13:12:093119 [self managePermissionAndSaveImage:data];
sdefresnee65fd872016-12-19 13:38:133120 }];
3121 break;
3122
3123 // The application doesn't have permission to access photo and the user
3124 // cannot grant it.
3125 case PHAuthorizationStatusRestricted:
3126 [self displayPrivacyErrorAlertOnMainQueue:
3127 l10n_util::GetNSString(
3128 IDS_IOS_SAVE_IMAGE_RESTRICTED_PRIVACY_ALERT_MESSAGE)];
3129 break;
3130
3131 // The application doesn't have permission to access photo and the user
3132 // can grant it.
3133 case PHAuthorizationStatusDenied:
3134 [self displayImageErrorAlertWithSettingsOnMainQueue];
3135 break;
3136
3137 // The application has permission to access the photos.
3138 default: {
3139 web::WebThread::PostTask(web::WebThread::FILE, FROM_HERE,
3140 base::BindBlock(^{
gambard16cfe1f2016-12-21 13:12:093141 [self saveImage:data];
sdefresnee65fd872016-12-19 13:38:133142 }));
3143 break;
3144 }
3145 }
3146}
3147
gambard16cfe1f2016-12-21 13:12:093148- (void)saveImage:(NSData*)data {
3149 NSString* fileName = [[[NSProcessInfo processInfo] globallyUniqueString]
3150 stringByAppendingString:@".png"];
sdefresnee65fd872016-12-19 13:38:133151 NSURL* fileURL =
3152 [NSURL fileURLWithPath:[NSTemporaryDirectory()
3153 stringByAppendingPathComponent:fileName]];
3154 NSError* error = nil;
3155 [data writeToURL:fileURL options:NSDataWritingAtomic error:&error];
3156
3157 // Error while writing the image to disk.
3158 if (error) {
3159 NSString* errorMessage = [NSString
3160 stringWithFormat:@"%@ (%@ %" PRIdNS ")", [error localizedDescription],
3161 [error domain], [error code]];
3162 [self displayPrivacyErrorAlertOnMainQueue:errorMessage];
3163 return;
3164 }
3165
3166 // Save the image to photos.
3167 [[PHPhotoLibrary sharedPhotoLibrary] performChanges:^{
3168 [PHAssetChangeRequest creationRequestForAssetFromImageAtFileURL:fileURL];
3169 }
3170 completionHandler:^(BOOL success, NSError* error) {
3171 // Callback for the image saving.
3172 [self finishSavingImageWithError:error];
3173
3174 // Cleanup the temporary file.
3175 web::WebThread::PostTask(
3176 web::WebThread::FILE, FROM_HERE, base::BindBlock(^{
3177 NSError* error = nil;
3178 [[NSFileManager defaultManager] removeItemAtURL:fileURL
3179 error:&error];
3180 }));
3181 }];
3182}
3183
3184- (void)displayImageErrorAlertWithSettingsOnMainQueue {
3185 NSURL* settingURL = [NSURL URLWithString:UIApplicationOpenSettingsURLString];
3186 BOOL canGoToSetting =
3187 [[UIApplication sharedApplication] canOpenURL:settingURL];
3188 if (canGoToSetting) {
3189 dispatch_async(dispatch_get_main_queue(), ^{
3190 [self displayImageErrorAlertWithSettings:settingURL];
3191 });
3192 } else {
3193 [self displayPrivacyErrorAlertOnMainQueue:
3194 l10n_util::GetNSString(IDS_IOS_SAVE_IMAGE_PRIVACY_ALERT_MESSAGE)];
3195 }
3196}
3197
3198- (void)displayImageErrorAlertWithSettings:(NSURL*)settingURL {
3199 // Dismiss current alert.
3200 [_alertCoordinator stop];
3201
3202 NSString* title =
3203 l10n_util::GetNSString(IDS_IOS_SAVE_IMAGE_PRIVACY_ALERT_TITLE);
3204 NSString* message = l10n_util::GetNSString(
3205 IDS_IOS_SAVE_IMAGE_PRIVACY_ALERT_MESSAGE_GO_TO_SETTINGS);
3206
3207 _alertCoordinator.reset([[AlertCoordinator alloc]
3208 initWithBaseViewController:self
3209 title:title
3210 message:message]);
3211
3212 [_alertCoordinator addItemWithTitle:l10n_util::GetNSString(IDS_CANCEL)
3213 action:nil
3214 style:UIAlertActionStyleCancel];
3215
3216 [_alertCoordinator
3217 addItemWithTitle:l10n_util::GetNSString(
3218 IDS_IOS_SAVE_IMAGE_PRIVACY_ALERT_GO_TO_SETTINGS)
3219 action:^{
3220 OpenUrlWithCompletionHandler(settingURL, nil);
3221 }
3222 style:UIAlertActionStyleDefault];
3223
3224 [_alertCoordinator start];
3225}
3226
3227- (void)displayPrivacyErrorAlertOnMainQueue:(NSString*)errorContent {
3228 dispatch_async(dispatch_get_main_queue(), ^{
3229 NSString* title =
3230 l10n_util::GetNSString(IDS_IOS_SAVE_IMAGE_PRIVACY_ALERT_TITLE);
3231 [self showErrorAlertWithStringTitle:title message:errorContent];
3232 });
3233}
3234
3235// This callback is triggered when the image is effectively saved onto the photo
3236// album, or if the save failed for some reason.
3237- (void)finishSavingImageWithError:(NSError*)error {
3238 // Was there an error?
3239 if (error) {
3240 // Saving photo failed even though user has granted access to Photos.
3241 // Display the error information from the NSError object for user.
3242 NSString* errorMessage = [NSString
3243 stringWithFormat:@"%@ (%@ %" PRIdNS ")", [error localizedDescription],
3244 [error domain], [error code]];
3245 // This code may be execute outside of the main thread. Make sure to display
3246 // the error on the main thread.
3247 [self displayPrivacyErrorAlertOnMainQueue:errorMessage];
3248 } else {
3249 // TODO(noyau): Ideally I'd like to show an infobar with a link to switch to
3250 // the photo application. The current behaviour is to create the photo there
3251 // but not providing any link to it is suboptimal. That's what Safari is
3252 // doing, and what the PM want, but it doesn't make it right.
3253 }
3254}
3255
sdefresnee65fd872016-12-19 13:38:133256#pragma mark - No-tabs UI methods
3257
3258// Show the No-Tabs UI (hiding normal tab/web ui).
3259- (void)showNoTabsUI {
3260 // The No-Tabs UI is only shown on tablet for non-incognito BVCs. (Incognito
3261 // mode does not have a No-Tabs UI; the user is simply shown the non-incognito
3262 // BVC when the last incognito tab is closed.)
3263 DCHECK(IsIPadIdiom());
3264 DCHECK(!_isOffTheRecord);
3265
3266 // The method showNoTabsUI is called asynchronously when the number of tabs
3267 // reaches zero. Do not show the no tabs UI if a tab was added in the mean
3268 // time.
3269 if ([_model count])
3270 return;
3271
3272 DCHECK([_model currentTab] == nil);
3273 DCHECK([_contentArea subviews].count == 0 ||
3274 experimental_flags::IsTabSwitcherEnabled());
3275 _noTabsController.reset([[NoTabsController alloc] initWithView:self.view]);
3276
3277 // Close the tools popup menu if it is open, as its contents/location were
3278 // specific to being in the tabs UI.
3279 [_toolbarController dismissToolsMenuPopup];
3280
3281 // Immediately hide the web, toolbar, and tabstrip.
3282 [[_toolbarController view] setHidden:YES];
3283 [[_tabStripController view] setHidden:YES];
3284
3285 // Set up the toggle switch animation, if needed.
3286 if ([self hasModeToggleSwitch]) {
3287 UIButton* animationStartButton = [_tabStripController modeToggleButton];
3288 [_noTabsController installAnimationImageForButton:animationStartButton
3289 inView:self.view
3290 show:YES];
3291 }
3292
3293 [_noTabsController prepareForShowAnimation];
3294 [UIView animateWithDuration:kNoTabsAnimationDuration
3295 animations:^{
3296 [_noTabsController showNoTabsUI];
3297 }
3298 completion:^(BOOL finished) {
3299 [_noTabsController showAnimationDidFinish];
3300 [_noTabsController setHasModeToggleSwitch:[self hasModeToggleSwitch]];
3301 }];
3302}
3303
3304// Hide the No-Tabs UI (restoring normal tab/web ui).
3305- (void)dismissNoTabsUI {
3306 // The No-Tabs UI is only shown on tablet for non-incognito BVCs, so there is
3307 // no need to dismiss it for an incognito BVC.
3308 DCHECK(IsIPadIdiom());
3309 if (_isOffTheRecord)
3310 return;
3311
3312 // Set up the toggle switch animation, if needed.
3313 if ([self hasModeToggleSwitch]) {
3314 UIButton* animationEndButton = [_tabStripController modeToggleButton];
3315 [_noTabsController installAnimationImageForButton:animationEndButton
3316 inView:self.view
3317 show:NO];
3318 }
3319
3320 [_noTabsController prepareForDismissAnimation];
3321
3322 // Pull the controller out of the scoped_nsobject so the animation blocks can
3323 // retain it.
3324 NoTabsController* noTabsController = _noTabsController.get();
3325 [UIView animateWithDuration:kNoTabsAnimationDuration
3326 animations:^{
3327 [noTabsController dismissNoTabsUI];
3328 }
3329 completion:^(BOOL finished) {
3330 // When the animation is finished, remove all of the No-Tabs UI and
3331 // reshow the tabstrip, web toolbar, and web.
3332 [noTabsController dismissAnimationDidFinish];
3333 [[_toolbarController view] setHidden:NO];
3334 [[_tabStripController view] setHidden:NO];
3335 }];
3336 // Nullify the instance variable. The controller is retained by the animation.
3337 // Nullifying this variable prevents button press performed during the
3338 // animation to be routed to the noTabController.
3339 _noTabsController.reset();
3340}
3341
3342#pragma mark - Showing popups
3343
3344- (void)showToolsMenuPopup {
3345 DCHECK(_browserState);
3346 DCHECK(self.visible || self.dismissingModal);
3347 DCHECK(!_noTabsController);
3348
3349 // Dismiss the omnibox (if open).
3350 [_toolbarController cancelOmniboxEdit];
3351 // Dismiss the soft keyboard (if open).
3352 [[_model currentTab].webController dismissKeyboard];
3353 // Dismiss Find in Page focus.
3354 [self updateFindBar:NO shouldFocus:NO];
3355
3356 base::scoped_nsobject<ToolsMenuContext> context(
3357 [[ToolsMenuContext alloc] initWithDisplayView:[self view]]);
3358 if ([_model count] == 0)
3359 [context setNoOpenedTabs:YES];
3360 if (_isOffTheRecord)
3361 [context setInIncognito:YES];
3362 if (reading_list::switches::IsReadingListEnabled()) {
3363 if (!_readingListMenuNotifier) {
3364 _readingListMenuNotifier.reset([[ReadingListMenuNotifier alloc]
3365 initWithReadingList:ReadingListModelFactory::GetForBrowserState(
3366 _browserState)]);
3367 }
3368 [context setReadingListMenuNotifier:_readingListMenuNotifier];
3369 }
3370
3371 [_toolbarController showToolsMenuPopupWithContext:context];
3372 ToolsPopupController* toolsPopupController =
3373 [_toolbarController toolsPopupController];
3374 if ([_model currentTab]) {
3375 BOOL isBookmarked = _toolbarModelIOS->IsCurrentTabBookmarked();
3376 [toolsPopupController setIsCurrentPageBookmarked:isBookmarked];
3377 [toolsPopupController setCanShowFindBar:self.canShowFindBar];
3378 [toolsPopupController setCanUseReaderMode:self.canUseReaderMode];
3379 [toolsPopupController
3380 setCanUseDesktopUserAgent:self.canUseDesktopUserAgent];
3381 [toolsPopupController setCanShowShareMenu:self.canShowShareMenu];
3382
3383 if (!IsIPadIdiom())
3384 [toolsPopupController setIsTabLoading:_toolbarModelIOS->IsLoading()];
3385 }
3386}
3387
3388- (void)showPageInfoPopupForView:(UIView*)sourceView {
3389 Tab* tab = [_model currentTab];
3390 DCHECK([tab navigationManager]);
3391 web::NavigationItem* navItem = [tab navigationManager]->GetVisibleItem();
3392
3393 // It is fully expected to have a navItem here, as showPageInfoPopup can only
3394 // be trigerred by a button enabled when a current item matches some
3395 // conditions. However a crash was seen were navItem was NULL hence this
3396 // test after a DCHECK.
3397 DCHECK(navItem);
3398 if (!navItem)
3399 return;
3400
3401 // Don't show if the page is native.
3402 if ([self isTabNativePage:tab])
3403 return;
3404
3405 base::RecordAction(UserMetricsAction("MobileToolbarPageSecurityInfo"));
3406
3407 // Dismiss the omnibox (if open).
3408 [_toolbarController cancelOmniboxEdit];
3409
3410 [[NSNotificationCenter defaultCenter]
3411 postNotificationName:ios_internal::kPageInfoWillShowNotification
3412 object:nil];
3413
3414 // TODO(rohitrao): Get rid of PageInfoModel completely.
3415 PageInfoModelBubbleBridge* bridge = new PageInfoModelBubbleBridge();
3416 PageInfoModel* pageInfoModel = new PageInfoModel(
3417 _browserState, navItem->GetURL(), navItem->GetSSL(), bridge);
3418
3419 UIView* view = [self view];
3420 _pageInfoController.reset([[PageInfoViewController alloc]
3421 initWithModel:pageInfoModel
3422 bridge:bridge
3423 sourceFrame:[sourceView convertRect:[sourceView bounds] toView:view]
3424 parentView:view]);
3425 bridge->set_controller(_pageInfoController.get());
3426}
3427
3428- (void)hidePageInfoPopupForView:(UIView*)sourceView {
3429 [_pageInfoController dismiss];
3430 _pageInfoController.reset();
3431}
3432
3433- (void)showSecurityHelpPage {
3434 [self webPageOrderedOpen:GURL(kPageInfoHelpCenterURL)
3435 referrer:web::Referrer()
3436 windowName:nil
3437 inBackground:NO
3438 appendTo:kCurrentTab];
3439 [self hidePageInfoPopupForView:nil];
3440}
3441
3442- (void)showTabHistoryPopupForBackwardHistory {
3443 DCHECK(self.visible || self.dismissingModal);
3444 DCHECK(!_noTabsController);
3445
3446 // Dismiss the omnibox (if open).
3447 [_toolbarController cancelOmniboxEdit];
3448 // Dismiss the soft keyboard (if open).
3449 Tab* tab = [_model currentTab];
3450 [tab.webController dismissKeyboard];
3451
3452 DCHECK([tab navigationManager]);
3453 CRWSessionController* sc = [tab navigationManager]->GetSessionController();
3454 [_toolbarController showTabHistoryPopupInView:[self view]
3455 withSessionEntries:[sc backwardEntries]
3456 forBackHistory:YES];
3457}
3458
3459- (void)showTabHistoryPopupForForwardHistory {
3460 DCHECK(self.visible || self.dismissingModal);
3461 DCHECK(!_noTabsController);
3462
3463 // Dismiss the omnibox (if open).
3464 [_toolbarController cancelOmniboxEdit];
3465 // Dismiss the soft keyboard (if open).
3466 Tab* tab = [_model currentTab];
3467 [tab.webController dismissKeyboard];
3468
3469 DCHECK([tab navigationManager]);
3470 CRWSessionController* sc = [tab navigationManager]->GetSessionController();
3471 [_toolbarController showTabHistoryPopupInView:[self view]
3472 withSessionEntries:[sc forwardEntries]
3473 forBackHistory:NO];
3474}
3475
3476- (void)navigateToSelectedEntry:(id)sender {
3477 DCHECK([sender isKindOfClass:[TabHistoryCell class]]);
3478 TabHistoryCell* selectedCell = (TabHistoryCell*)sender;
3479 [[_model currentTab] goToEntry:selectedCell.entry];
3480 [_toolbarController dismissTabHistoryPopup];
3481}
3482
3483- (void)print {
3484 Tab* currentTab = [_model currentTab];
3485 // The UI should prevent users from printing non-printable pages. However, a
3486 // redirection to an un-printable page can happen before it is reflected in
3487 // the UI.
3488 if (![currentTab viewForPrinting]) {
3489 [self showSnackbar:l10n_util::GetNSString(IDS_IOS_CANNOT_PRINT_PAGE_ERROR)];
3490 return;
3491 }
3492 DCHECK(_browserState);
3493 if (!_printController.get()) {
3494 _printController.reset([[PrintController alloc]
3495 initWithContextGetter:_browserState->GetRequestContext()]);
3496 }
3497 [_printController printView:[currentTab viewForPrinting]
3498 withTitle:[currentTab title]
3499 viewController:self];
3500}
3501
3502- (void)addToReadingListURL:(const GURL&)URL title:(NSString*)title {
3503 if (!reading_list::switches::IsReadingListEnabled()) {
3504 return;
3505 }
3506 base::RecordAction(UserMetricsAction("MobileReadingListAdd"));
3507
3508 ReadingListModel* readingModel =
3509 ReadingListModelFactory::GetForBrowserState(_browserState);
jife0e60112017-01-16 13:20:013510 readingModel->AddEntry(URL, base::SysNSStringToUTF8(title),
3511 reading_list::ADDED_VIA_CURRENT_APP);
sdefresnee65fd872016-12-19 13:38:133512
gambarde31ad3ba2017-01-19 14:40:033513 [self showSnackbar:l10n_util::GetNSString(
3514 IDS_IOS_READING_LIST_SNACKBAR_MESSAGE)];
sdefresnee65fd872016-12-19 13:38:133515}
3516
3517#pragma mark - Keyboard commands management
3518
3519- (BOOL)shouldRegisterKeyboardCommands {
3520 if ([self presentedViewController])
3521 return NO;
3522
3523 if (_voiceSearchController && _voiceSearchController->IsVisible())
3524 return NO;
3525
3526 // If there is no first responder, try to make the webview the first
3527 // responder.
3528 if (!GetFirstResponder()) {
3529 [_model.get().currentTab.webController.webViewProxy becomeFirstResponder];
3530 }
3531
3532 return YES;
3533}
3534
3535- (KeyCommandsProvider*)keyCommandsProvider {
3536 if (!_keyCommandsProvider) {
3537 _keyCommandsProvider.reset([_dependencyFactory newKeyCommandsProvider]);
3538 }
3539 return _keyCommandsProvider.get();
3540}
3541
3542#pragma mark - KeyCommandsPlumbing
3543
3544- (BOOL)isOffTheRecord {
3545 return _isOffTheRecord;
3546}
3547
3548- (NSUInteger)tabsCount {
3549 return [_model count];
3550}
3551
lpromero47ea8862017-01-13 17:51:063552- (BOOL)canGoBack {
3553 return [_model currentTab].canGoBack;
3554}
3555
3556- (BOOL)canGoForward {
3557 return [_model currentTab].canGoForward;
3558}
3559
sdefresnee65fd872016-12-19 13:38:133560- (void)focusTabAtIndex:(NSUInteger)index {
3561 if ([_model count] > index) {
3562 [_model setCurrentTab:[_model tabAtIndex:index]];
3563 }
3564}
3565
3566- (void)focusNextTab {
3567 NSInteger currentTabIndex = [_model indexOfTab:[_model currentTab]];
3568 NSInteger modelCount = [_model count];
3569 if (currentTabIndex < modelCount - 1) {
3570 Tab* nextTab = [_model tabAtIndex:currentTabIndex + 1];
3571 [_model setCurrentTab:nextTab];
3572 } else {
3573 [_model setCurrentTab:[_model tabAtIndex:0]];
3574 }
3575}
3576
3577- (void)focusPreviousTab {
3578 NSInteger currentTabIndex = [_model indexOfTab:[_model currentTab]];
3579 if (currentTabIndex > 0) {
3580 Tab* previousTab = [_model tabAtIndex:currentTabIndex - 1];
3581 [_model setCurrentTab:previousTab];
3582 } else {
3583 Tab* lastTab = [_model tabAtIndex:[_model count] - 1];
3584 [_model setCurrentTab:lastTab];
3585 }
3586}
3587
3588- (void)reopenClosedTab {
3589 sessions::TabRestoreService* const tabRestoreService =
3590 IOSChromeTabRestoreServiceFactory::GetForBrowserState(_browserState);
3591 if (!tabRestoreService || tabRestoreService->entries().empty())
3592 return;
3593
3594 const std::unique_ptr<sessions::TabRestoreService::Entry>& entry =
3595 tabRestoreService->entries().front();
3596 // Only handle the TAB type.
3597 if (entry->type != sessions::TabRestoreService::TAB)
3598 return;
3599
3600 [self chromeExecuteCommand:[GenericChromeCommand commandWithTag:IDC_NEW_TAB]];
3601 TabRestoreServiceDelegateImplIOS* const delegate =
3602 TabRestoreServiceDelegateImplIOSFactory::GetForBrowserState(
3603 _browserState);
3604 tabRestoreService->RestoreEntryById(delegate, entry->id,
3605 WindowOpenDisposition::CURRENT_TAB);
3606}
3607
3608- (void)focusOmnibox {
3609 [_toolbarController focusOmnibox];
3610}
3611
3612#pragma mark - UIResponder
3613
3614- (NSArray*)keyCommands {
3615 if (![self shouldRegisterKeyboardCommands]) {
3616 return nil;
3617 }
3618 return [self.keyCommandsProvider
3619 keyCommandsForConsumer:self
3620 editingText:![self isFirstResponder]];
3621}
3622
3623#pragma mark -
3624
3625// Induce an intentional crash in the browser process.
3626- (void)induceBrowserCrash {
3627 CHECK(false);
3628 // Call another function, so that the above CHECK can't be tail-call
3629 // optimized. This ensures that this method's name will show up in the stack
3630 // for easier identification.
3631 CHECK(true);
3632}
3633
3634- (void)loadURL:(const GURL&)url
3635 referrer:(const web::Referrer&)referrer
3636 transition:(ui::PageTransition)transition
3637 rendererInitiated:(BOOL)rendererInitiated {
3638 [[OmniboxGeolocationController sharedInstance]
3639 locationBarDidSubmitURL:url
3640 transition:transition
3641 browserState:_browserState];
3642
3643 [_bookmarkInteractionController dismissBookmarkModalControllerAnimated:YES];
3644 if (transition & ui::PAGE_TRANSITION_FROM_ADDRESS_BAR) {
3645 new_tab_page_uma::RecordActionFromOmnibox(_browserState, url, transition);
3646 }
3647
3648 // NOTE: This check for the Crash Host URL is here to avoid the URL from
3649 // ending up in the history causign the app to crash at every subsequent
3650 // restart.
3651 if (url.host() == kChromeUIBrowserCrashHost) {
3652 [self induceBrowserCrash];
3653 // In debug the app can continue working even after the CHECK. Adding a
3654 // return avoids the crash url to be added to the history.
3655 return;
3656 }
3657
3658 if (url == [_preloadController prerenderedURL]) {
3659 Tab* oldTab = [_model currentTab];
3660 Tab* newTab = [_preloadController releasePrerenderContents];
3661 DCHECK(oldTab);
3662 DCHECK(newTab);
3663 if (oldTab && newTab) {
3664 [oldTab recordStateInHistory];
3665 DCHECK([newTab navigationManager]);
3666 CRWSessionController* newHistory =
3667 [newTab navigationManager]->GetSessionController();
3668 DCHECK([oldTab navigationManager]);
3669 CRWSessionController* oldHistory =
3670 [oldTab navigationManager]->GetSessionController();
3671 [newHistory insertStateFromSessionController:oldHistory];
3672 [[newTab nativeAppNavigationController]
3673 copyStateFrom:[oldTab nativeAppNavigationController]];
3674 [_model replaceTab:oldTab withTab:newTab keepOldTabOpen:NO];
3675
3676 // Set isPrerenderTab to NO after replacing the tab. This will allow the
3677 // BrowserViewController to detect that a pre-rendered tab is switched in,
3678 // and show the prerendering animation.
3679 newTab.isPrerenderTab = NO;
3680
3681 BOOL loadingFinished =
3682 [newTab.webController loadPhase] == web::PAGE_LOADED;
3683 [self tabLoadComplete:newTab withSuccess:loadingFinished];
3684
3685 return;
3686 }
3687 }
3688
3689 GURL urlToLoad = url;
3690 if ([_preloadController hasPrefetchedURL:url]) {
3691 // Prefetched URLs have modified URLs, so load the prefetched version of
3692 // |url| instead of the original |url|.
3693 urlToLoad = [_preloadController prefetchedURL];
3694 }
3695
3696 [_preloadController cancelPrerender];
3697
3698 // Some URLs are not allowed while in incognito. If we are in incognito and
3699 // load a disallowed URL, instead create a new tab not in the incognito state.
3700 if (_isOffTheRecord && !IsURLAllowedInIncognito(url)) {
3701 [self webPageOrderedOpen:url
3702 referrer:web::Referrer()
3703 windowName:nil
3704 inIncognito:NO
3705 inBackground:NO
3706 appendTo:kCurrentTab];
3707 return;
3708 }
3709
3710 web::NavigationManager::WebLoadParams params(urlToLoad);
3711 params.referrer = referrer;
3712 params.transition_type = transition;
3713 params.is_renderer_initiated = rendererInitiated;
3714 DCHECK([_model currentTab]);
3715 [[[_model currentTab] webController] loadWithParams:params];
3716}
3717
3718- (void)loadJavaScriptFromLocationBar:(NSString*)script {
3719 [_preloadController cancelPrerender];
3720 DCHECK([_model currentTab]);
3721 [[_model currentTab].webController executeUserJavaScript:script
3722 completionHandler:nil];
3723}
3724
3725- (web::WebState*)currentWebState {
3726 return [[_model currentTab] webState];
3727}
3728
eugenebut75a06fa72017-01-09 17:09:553729- (Tab*)tabForWebState:(web::WebState*)webState {
3730 for (Tab* tab in _model.get()) {
3731 if (tab.webState == webState)
3732 return tab;
3733 }
3734 return nil;
3735}
3736
sdefresnee65fd872016-12-19 13:38:133737// This is called from within an animation block.
3738- (void)toolbarHeightChanged {
3739 if ([self headerHeight] != 0) {
3740 // Ensure full screen height is updated.
3741 Tab* currentTab = [_model currentTab];
3742 BOOL visible = self.isToolbarOnScreen;
3743 [currentTab updateFullscreenWithToolbarVisible:visible];
3744 }
3745}
3746
3747// Load a new URL on a new page/tab.
3748- (void)webPageOrderedOpen:(const GURL&)URL
3749 referrer:(const web::Referrer&)referrer
3750 windowName:(NSString*)windowName
3751 inBackground:(BOOL)inBackground
3752 appendTo:(OpenPosition)appendTo {
3753 Tab* adjacentTab = nil;
3754 if (appendTo == kCurrentTab)
3755 adjacentTab = [_model currentTab];
3756 [_model insertOrUpdateTabWithURL:URL
3757 referrer:referrer
3758 transition:ui::PAGE_TRANSITION_LINK
3759 windowName:windowName
3760 opener:adjacentTab
3761 openedByDOM:NO
3762 atIndex:TabModelConstants::kTabPositionAutomatically
3763 inBackground:inBackground];
3764}
3765
3766- (void)webPageOrderedOpen:(const GURL&)url
3767 referrer:(const web::Referrer&)referrer
3768 windowName:(NSString*)windowName
3769 inIncognito:(BOOL)inIncognito
3770 inBackground:(BOOL)inBackground
3771 appendTo:(OpenPosition)appendTo {
3772 if (inIncognito == _isOffTheRecord) {
3773 [self webPageOrderedOpen:url
3774 referrer:referrer
3775 windowName:windowName
3776 inBackground:inBackground
3777 appendTo:appendTo];
3778 return;
3779 }
3780 // When sending an open command that switches modes, ensure the tab
3781 // ends up appended to the end of the model, not just next to what is
3782 // currently selected in the other mode. This is done with the |append|
3783 // parameter.
3784 base::scoped_nsobject<OpenUrlCommand> command([[OpenUrlCommand alloc]
3785 initWithURL:url
3786 referrer:web::Referrer() // Strip referrer when switching modes.
3787 windowName:windowName
3788 inIncognito:inIncognito
3789 inBackground:inBackground
3790 appendTo:kLastTab]);
3791 [self chromeExecuteCommand:command];
3792}
3793
3794- (void)loadSessionTab:(const sessions::SessionTab*)sessionTab {
3795 [[_model currentTab] loadSessionTab:sessionTab];
3796}
3797
3798- (void)openJavascript:(NSString*)javascript {
rohitrao746baec2017-01-20 16:20:433799 DCHECK(javascript);
3800 javascript = [javascript stringByRemovingPercentEncoding];
3801 web::WebState* webState = [[_model currentTab] webState];
3802 if (webState) {
3803 webState->ExecuteJavaScript(base::SysNSStringToUTF16(javascript));
3804 }
sdefresnee65fd872016-12-19 13:38:133805}
3806
3807#pragma mark - WebToolbarDelegate methods
3808
3809- (IBAction)locationBarDidBecomeFirstResponder:(id)sender {
3810 if (_locationBarHasFocus)
3811 return; // TODO(crbug.com/244366): This should not be necessary.
3812 _locationBarHasFocus = YES;
3813 [[NSNotificationCenter defaultCenter]
3814 postNotificationName:ios_internal::
3815 kLocationBarBecomesFirstResponderNotification
3816 object:nil];
3817 [_sideSwipeController setEnabled:NO];
3818 if ([[_model currentTab].webController wantsKeyboardShield]) {
3819 [[self view] insertSubview:_typingShield aboveSubview:_contentArea];
3820 [_typingShield setAlpha:0.0];
3821 [_typingShield setHidden:NO];
3822 [UIView animateWithDuration:0.3
3823 animations:^{
3824 [_typingShield setAlpha:1.0];
3825 }];
3826 }
3827 [[OmniboxGeolocationController sharedInstance]
3828 locationBarDidBecomeFirstResponder:_browserState];
3829}
3830
3831- (IBAction)locationBarDidResignFirstResponder:(id)sender {
3832 if (!_locationBarHasFocus)
3833 return; // TODO(crbug.com/244366): This should not be necessary.
3834 _locationBarHasFocus = NO;
3835 [_sideSwipeController setEnabled:YES];
3836 [[NSNotificationCenter defaultCenter]
3837 postNotificationName:ios_internal::
3838 kLocationBarResignsFirstResponderNotification
3839 object:nil];
3840 [UIView animateWithDuration:0.3
3841 animations:^{
3842 [_typingShield setAlpha:0.0];
3843 }
3844 completion:^(BOOL finished) {
3845 // This can happen if one quickly resigns the omnibox and then taps
3846 // on the omnibox again during this animation. If the animation is
3847 // interrupted and the toolbar controller is first responder, it's safe
3848 // to assume the |_typingShield| shouldn't be hidden here.
3849 if (!finished && [_toolbarController isOmniboxFirstResponder])
3850 return;
3851 [_typingShield setHidden:YES];
3852 }];
3853 [[OmniboxGeolocationController sharedInstance]
3854 locationBarDidResignFirstResponder:_browserState];
3855
3856 // If a load was cancelled by an omnibox edit, but nothing is loading when
3857 // editing ends (i.e., editing was cancelled), restart the cancelled load.
3858 if (_locationBarEditCancelledLoad) {
3859 _locationBarEditCancelledLoad = NO;
3860 if (!_toolbarModelIOS->IsLoading()) {
3861 [[_model currentTab] reload];
3862 }
3863 }
3864}
3865
3866- (IBAction)locationBarBeganEdit:(id)sender {
3867 // On handsets, if a page is currently loading it should be stopped.
3868 if (!IsIPadIdiom() && _toolbarModelIOS->IsLoading()) {
3869 base::scoped_nsobject<GenericChromeCommand> command(
3870 [[GenericChromeCommand alloc] initWithTag:IDC_STOP]);
3871 [self chromeExecuteCommand:command];
3872 _locationBarEditCancelledLoad = YES;
3873 }
3874}
3875
3876- (IBAction)prepareToEnterTabSwitcher:(id)sender {
3877 [[_model currentTab] updateSnapshotWithOverlay:YES visibleFrameOnly:YES];
3878}
3879
3880- (ToolbarModelIOS*)toolbarModelIOS {
3881 return _toolbarModelIOS.get();
3882}
3883
3884- (void)updateToolbarBackgroundAlpha:(CGFloat)alpha {
3885 [_toolbarController setBackgroundAlpha:alpha];
3886}
3887
3888- (void)updateToolbarControlsAlpha:(CGFloat)alpha {
3889 [_toolbarController setControlsAlpha:alpha];
3890}
3891
3892- (void)willUpdateToolbarSnapshot {
3893 [[_model currentTab].overscrollActionsController clear];
3894}
3895
3896- (CardView*)addCardViewInFullscreen:(BOOL)fullScreen {
3897 CGRect frame = [_contentArea frame];
3898 if (!fullScreen) {
3899 // Changing the origin here is unnecessary, it's set in page_animation_util.
3900 frame.size.height -= [self headerHeight];
3901 }
3902
3903 CGFloat shortAxis = frame.size.width;
3904 CGFloat shortInset = kCardImageInsets.left + kCardImageInsets.right;
3905 shortAxis -= shortInset + 2 * ios_internal::page_animation_util::kCardMargin;
3906 CGFloat aspectRatio = frame.size.height / frame.size.width;
3907 CGFloat longAxis = std::floor(aspectRatio * shortAxis);
3908 CGFloat longInset = kCardImageInsets.top + kCardImageInsets.bottom;
3909 CGSize cardSize = CGSizeMake(shortAxis + shortInset, longAxis + longInset);
3910 CGRect cardFrame = {frame.origin, cardSize};
3911
3912 CardView* card =
3913 [[[CardView alloc] initWithFrame:cardFrame isIncognito:_isOffTheRecord]
3914 autorelease];
3915 card.closeButtonSide = IsPortrait() ? CardCloseButtonSide::TRAILING
3916 : CardCloseButtonSide::LEADING;
3917 [_contentArea addSubview:card];
3918 return card;
3919}
3920
3921#pragma mark - Command Handling
3922
3923- (IBAction)chromeExecuteCommand:(id)sender {
3924 NSInteger command = [sender tag];
3925
3926 if (!_model || !_browserState)
3927 return;
3928
3929 switch (command) {
3930 case IDC_BACK:
lpromero47ea8862017-01-13 17:51:063931 [[_model currentTab] goBack];
sdefresnee65fd872016-12-19 13:38:133932 break;
3933 case IDC_BOOKMARK_PAGE:
3934 [self initializeBookmarkInteractionController];
3935 [_bookmarkInteractionController
3936 presentBookmarkForTab:[_model currentTab]
3937 currentlyBookmarked:_toolbarModelIOS->IsCurrentTabBookmarkedByUser()
3938 inView:[_toolbarController bookmarkButtonView]
3939 originRect:[_toolbarController bookmarkButtonAnchorRect]];
3940 break;
3941 case IDC_CLOSE_TAB:
3942 [self closeCurrentTab];
3943 break;
3944 case IDC_FIND:
3945 [self initFindBarForTab];
3946 break;
3947 case IDC_FIND_NEXT:
3948 // TODO(crbug.com/603524): Reshow find bar if necessary.
3949 [[_model currentTab].findInPageController
3950 findNextStringInPageWithCompletionHandler:^{
3951 FindInPageModel* model =
3952 [_model currentTab].findInPageController.findInPageModel;
3953 [_findBarController updateResultsCount:model];
3954 }];
3955 break;
3956 case IDC_FIND_PREVIOUS:
3957 // TODO(crbug.com/603524): Reshow find bar if necessary.
3958 [[_model currentTab].findInPageController
3959 findPreviousStringInPageWithCompletionHandler:^{
3960 FindInPageModel* model =
3961 [_model currentTab].findInPageController.findInPageModel;
3962 [_findBarController updateResultsCount:model];
3963 }];
3964 break;
3965 case IDC_FIND_CLOSE:
3966 [self closeFindInPage];
3967 break;
3968 case IDC_FIND_UPDATE:
3969 [self searchFindInPage];
3970 break;
3971 case IDC_FORWARD:
lpromero47ea8862017-01-13 17:51:063972 [[_model currentTab] goForward];
sdefresnee65fd872016-12-19 13:38:133973 break;
3974 case IDC_FULLSCREEN:
3975 NOTIMPLEMENTED();
3976 break;
3977 case IDC_HELP_PAGE_VIA_MENU:
3978 [self showHelpPage];
3979 break;
3980 case IDC_NEW_TAB:
3981 if (_isOffTheRecord) {
3982 // Not for this browser state, send it on its way.
3983 [super chromeExecuteCommand:sender];
3984 } else {
3985 [self newTab:sender];
3986 }
3987 break;
3988 case IDC_PRELOAD_VOICE_SEARCH:
3989 // Preload VoiceSearchController and views and view controllers needed
3990 // for voice search.
3991 [self ensureVoiceSearchControllerCreated];
3992 _voiceSearchController->PrepareToAppear();
3993 break;
3994 case IDC_NEW_INCOGNITO_TAB:
3995 if (_isOffTheRecord) {
3996 [self newTab:sender];
3997 } else {
3998 // Not for this browser state, send it on its way.
3999 [super chromeExecuteCommand:sender];
4000 }
4001 break;
4002 case IDC_RELOAD:
4003 [[_model currentTab] reload];
4004 break;
4005 case IDC_SHARE_PAGE:
4006 [self sharePage];
4007 break;
4008 case IDC_SHOW_MAIL_COMPOSER:
4009 [self showMailComposer:sender];
4010 break;
4011 case IDC_READER_MODE:
4012 [[_model currentTab] switchToReaderMode];
4013 break;
4014 case IDC_REQUEST_DESKTOP_SITE:
4015 [self enableDesktopUserAgent];
4016 break;
4017 case IDC_SHOW_TOOLS_MENU: {
4018 // TODO(blundell): Change this if/else to
4019 // |DCHECK(!_noTabsController)| if/when the no tabs controller
4020 // becomes part of the responder chain.
4021 // The no tabs controller's toolbar should open the menu when in the
4022 // no-tabs UI.
4023 if (_noTabsController.get())
4024 [_noTabsController showToolsMenuPopup];
4025 else
4026 [self showToolsMenuPopup];
4027 break;
4028 }
4029 case IDC_SHOW_BOOKMARK_MANAGER: {
4030 if (IsIPadIdiom()) {
4031 [self showAllBookmarks];
4032 } else {
4033 [self initializeBookmarkInteractionController];
4034 [_bookmarkInteractionController presentBookmarks];
4035 }
4036 break;
4037 }
4038 case IDC_SHOW_OTHER_DEVICES: {
4039 if (IsIPadIdiom()) {
4040 [self showNTPPanel:NewTabPage::kOpenTabsPanel];
4041 } else {
4042 UIViewController* controller = [RecentTabsPanelViewController
4043 controllerToPresentForBrowserState:_browserState
4044 loader:self];
4045 controller.modalPresentationStyle = UIModalPresentationFormSheet;
4046 controller.modalPresentationCapturesStatusBarAppearance = YES;
4047 [self presentViewController:controller animated:YES completion:nil];
4048 }
4049 break;
4050 }
4051 case IDC_STOP:
4052 [[_model currentTab] stopLoading];
4053 break;
4054#if !defined(NDEBUG)
4055 case IDC_VIEW_SOURCE:
4056 [self viewSource];
4057 break;
4058#endif
4059 case IDC_SHOW_PAGE_INFO:
4060 DCHECK([sender isKindOfClass:[UIButton class]]);
4061 [self showPageInfoPopupForView:sender];
4062 break;
4063 case IDC_HIDE_PAGE_INFO:
4064 [[NSNotificationCenter defaultCenter]
4065 postNotificationName:ios_internal::kPageInfoWillHideNotification
4066 object:nil];
4067 [self hidePageInfoPopupForView:sender];
4068 break;
4069 case IDC_SHOW_SECURITY_HELP:
4070 [self showSecurityHelpPage];
4071 break;
4072 case IDC_SHOW_BACK_HISTORY:
4073 [self showTabHistoryPopupForBackwardHistory];
4074 break;
4075 case IDC_SHOW_FORWARD_HISTORY:
4076 [self showTabHistoryPopupForForwardHistory];
4077 break;
4078 case IDC_BACK_FORWARD_IN_TAB_HISTORY:
4079 DCHECK([sender isKindOfClass:[TabHistoryCell class]]);
4080 [self navigateToSelectedEntry:sender];
4081 break;
4082 case IDC_PRINT:
4083 [self print];
4084 break;
4085 case IDC_ADD_READING_LIST: {
4086 DCHECK(reading_list::switches::IsReadingListEnabled());
4087 ReadingListAddCommand* command =
4088 base::mac::ObjCCastStrict<ReadingListAddCommand>(sender);
4089 [self addToReadingListURL:[command URL] title:[command title]];
4090 break;
4091 }
4092 case IDC_RATE_THIS_APP:
4093 [self showRateThisAppDialog];
4094 break;
4095 case IDC_SHOW_READING_LIST:
4096 DCHECK(reading_list::switches::IsReadingListEnabled());
4097 [self showReadingList];
4098 break;
4099 case IDC_VOICE_SEARCH:
4100 // If the voice search command is coming from a UIView sender, store it
4101 // before sending the command up the responder chain.
4102 if ([sender isKindOfClass:[UIView class]])
4103 _voiceSearchButton.reset(sender);
4104 [super chromeExecuteCommand:sender];
4105 break;
4106 case IDC_SHOW_QR_SCANNER:
4107 if (experimental_flags::IsQRCodeReaderEnabled()) {
4108 [self showQRScanner];
4109 }
4110 break;
4111 default:
4112 // Unknown commands get sent up the responder chain.
4113 [super chromeExecuteCommand:sender];
4114 break;
4115 }
4116}
4117
4118- (void)closeCurrentTab {
4119 Tab* currentTab = [_model currentTab];
4120 NSUInteger tabIndex = [_model indexOfTab:currentTab];
4121 if (tabIndex == NSNotFound)
4122 return;
4123
4124 // Take snapshot on iPad only if Tab switcher is enabled, if not just close
4125 // the tab.
4126 if (IsIPadIdiom() && !experimental_flags::IsTabSwitcherEnabled()) {
4127 [_model closeTabAtIndex:tabIndex];
4128 return;
4129 }
4130
4131 // Create image of tab for close animation.
4132 UIImageView* exitingPage = [self pageOpenCloseAnimationView];
4133 exitingPage.image =
4134 [currentTab updateSnapshotWithOverlay:YES visibleFrameOnly:YES];
4135
4136 // Close the actual tab, and add its image as a subview.
4137 [_model closeTabAtIndex:tabIndex];
4138
4139 // Do not animate close in iPad.
4140 if (!IsIPadIdiom()) {
4141 [_contentArea addSubview:exitingPage];
4142 ios_internal::page_animation_util::AnimateOutWithCompletion(
4143 exitingPage, 0, YES, IsPortrait(), ^{
4144 [exitingPage removeFromSuperview];
4145 });
4146 }
4147}
4148
4149- (ShareToData*)shareToDataForTab:(Tab*)tab {
4150 // For crash documented in crbug.com/503955, tab.url which is being passed
4151 // as a reference parameter caused a crash due to invalid address which
4152 // which suggests that |tab| may be deallocated along the way. Check that
4153 // tab is still valid by checking webState which would be deallocated if
4154 // tab is being closed.
4155 if (!tab.webState)
4156 return nil;
4157 DCHECK(tab);
4158 // If the original page title exists, it is expected to match the tab title.
4159 // If this ever changes, then a decision has to be made on which one should
4160 // be used for sharing.
4161 DCHECK(!tab.originalTitle || [tab.originalTitle isEqualToString:tab.title]);
4162 BOOL isPagePrintable = [tab viewForPrinting] != nil;
4163 return [[[ShareToData alloc] initWithURL:tab.url
4164 title:tab.title
4165 isOriginalTitle:(tab.originalTitle != nil)
4166 isPagePrintable:isPagePrintable] autorelease];
4167}
4168
4169- (void)sharePage {
4170 ShareToData* data = [self shareToDataForTab:[_model currentTab]];
4171 if (data)
4172 [self sharePageWithData:data];
4173}
4174
4175- (void)sharePageWithData:(ShareToData*)data {
4176 id<ShareProtocol> controller = [_dependencyFactory shareControllerInstance];
4177 if ([controller isActive])
4178 return;
4179 CGRect fromRect = [_toolbarController shareButtonAnchorRect];
4180 UIView* inView = [_toolbarController shareButtonView];
4181 [controller shareWithData:data
4182 controller:self
4183 browserState:_browserState
4184 shareToDelegate:self
4185 fromRect:fromRect
4186 inView:inView];
4187}
4188
4189- (void)clearPresentedStateWithCompletion:(ProceduralBlock)completion {
4190 [[_dependencyFactory shareControllerInstance] cancelShareAnimated:NO];
4191 [_bookmarkInteractionController dismissBookmarkModalControllerAnimated:NO];
4192 [_bookmarkInteractionController dismissSnackbar];
4193 [_toolbarController cancelOmniboxEdit];
4194 [_dialogPresenter cancelAllDialogs];
4195 [self hidePageInfoPopupForView:nil];
4196 if (_voiceSearchController)
4197 _voiceSearchController->DismissMicPermissionsHelp();
4198 [[_model currentTab] dismissModals];
4199 [[_model currentTab].findInPageController
4200 disableFindInPageWithCompletionHandler:^{
4201 [self updateFindBar:NO shouldFocus:NO];
4202 }];
4203 [_contextualSearchController movePanelOffscreen];
4204
4205 [_paymentRequestManager cancelRequest];
4206
4207 [_printController dismissAnimated:YES];
4208 _printController.reset();
4209 if (_noTabsController.get())
4210 [_noTabsController dismissToolsMenuPopup];
4211 else
4212 [_toolbarController dismissToolsMenuPopup];
4213 [_contextMenuCoordinator stop];
4214 [self dismissRateThisAppDialog];
4215
4216 if (self.presentedViewController) {
4217 // Dismisses any other modal controllers that may be present, e.g. Recent
4218 // Tabs.
4219 // Note that currently, some controllers like the bookmark ones were already
4220 // dismissed (in this example in -dismissBookmarkModalControllerAnimated:),
4221 // but are still reported as the presentedViewController. The result is that
4222 // this will call -dismissViewControllerAnimated:completion: a second time
4223 // on it. It is not per se an issue, as it is a no-op. The problem is that
4224 // in such a case, the completion block is not called.
4225 // To ensure the completion is called, nil is passed here, and the
4226 // completion is called below.
4227 [self dismissViewControllerAnimated:NO completion:nil];
4228 // Dismissed controllers will be so after a delay. Queue the completion
4229 // callback after that.
4230 if (completion) {
4231 dispatch_after(
4232 dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.4 * NSEC_PER_SEC)),
4233 dispatch_get_main_queue(), ^{
4234 completion();
4235 });
4236 }
4237 } else if (completion) {
4238 // If no view controllers are presented, we should be ok with dispatching
4239 // the completion block directly.
4240 dispatch_async(dispatch_get_main_queue(), completion);
4241 }
4242}
4243
4244- (void)showHelpPage {
4245 GURL helpUrl(l10n_util::GetStringUTF16(IDS_IOS_TOOLS_MENU_HELP_URL));
4246 [self webPageOrderedOpen:helpUrl
4247 referrer:web::Referrer()
4248 windowName:nil
4249 inBackground:NO
4250 appendTo:kCurrentTab];
4251}
4252
4253- (void)enableDesktopUserAgent {
4254 [[_model currentTab] enableDesktopUserAgent];
4255 [[_model currentTab] reloadForDesktopUserAgent];
4256}
4257
4258- (void)resetAllWebViews {
4259 [_model resetAllWebViews];
4260}
4261
4262#pragma mark - Find Bar
4263
4264- (void)hideFindBarWithAnimation:(BOOL)animate {
4265 [_findBarController hideFindBarView:animate];
4266}
4267
4268- (void)showFindBarWithAnimation:(BOOL)animate
4269 selectText:(BOOL)selectText
4270 shouldFocus:(BOOL)shouldFocus {
4271 DCHECK(_findBarController);
4272 Tab* tab = [_model currentTab];
4273 DCHECK(tab);
4274 CRWWebController* webController = tab.webController;
4275
4276 CGRect referenceFrame = CGRectZero;
4277 if (IsIPadIdiom()) {
4278 referenceFrame = webController.visibleFrame;
4279 referenceFrame.origin.y -= kIPadFindBarOverlap;
4280 } else {
4281 referenceFrame = _contentArea.frame;
4282 }
4283
4284 CGRect omniboxFrame = [_toolbarController visibleOmniboxFrame];
4285 [_findBarController addFindBarView:animate
4286 intoView:self.view
4287 withFrame:referenceFrame
4288 alignWithFrame:omniboxFrame
4289 selectText:selectText];
4290 [self updateFindBar:YES shouldFocus:shouldFocus];
4291}
4292
4293// Create find bar controller and pass it to the web controller.
4294- (void)initFindBarForTab {
4295 if (!self.canShowFindBar)
4296 return;
4297
4298 if (!_findBarController)
4299 _findBarController.reset(
4300 [[FindBarControllerIOS alloc] initWithIncognito:_isOffTheRecord]);
4301
4302 Tab* tab = [_model currentTab];
4303 DCHECK(!tab.findInPageController.findInPageModel.enabled);
4304 tab.findInPageController.findInPageModel.enabled = YES;
4305 [self showFindBarWithAnimation:YES selectText:YES shouldFocus:YES];
4306}
4307
4308- (void)searchFindInPage {
4309 FindInPageController* findInPageController =
4310 [[_model currentTab] findInPageController];
4311 base::WeakNSObject<BrowserViewController> weakSelf(self);
4312 [findInPageController
4313 findStringInPage:[_findBarController searchTerm]
4314 completionHandler:^{
4315 FindInPageModel* model =
4316 [_model currentTab].findInPageController.findInPageModel;
4317 [_findBarController updateResultsCount:model];
4318 }];
4319 if (!_isOffTheRecord)
4320 [findInPageController saveSearchTerm];
4321}
4322
4323- (void)closeFindInPage {
4324 base::WeakNSObject<BrowserViewController> weakSelf(self);
4325 [[_model currentTab].findInPageController
4326 disableFindInPageWithCompletionHandler:^{
4327 [weakSelf updateFindBar:NO shouldFocus:NO];
4328 }];
4329}
4330
4331- (void)updateFindBar:(BOOL)initialUpdate shouldFocus:(BOOL)shouldFocus {
4332 FindInPageModel* model =
4333 [_model currentTab].findInPageController.findInPageModel;
4334 if (model.enabled) {
4335 if (initialUpdate && !_isOffTheRecord) {
4336 [[_model currentTab].findInPageController restoreSearchTerm];
4337 }
4338
4339 [self setFramesForHeaders:[self headerViews]
4340 atOffset:[self currentHeaderOffset]];
4341 [_findBarController updateView:model
4342 initialUpdate:initialUpdate
4343 focusTextfield:shouldFocus];
4344 } else {
4345 [self hideFindBarWithAnimation:YES];
4346 }
4347}
4348
4349- (void)showAllBookmarks {
4350 DCHECK(self.visible || self.dismissingModal);
4351 GURL URL(kChromeUIBookmarksURL);
4352 Tab* tab = [_model currentTab];
4353 web::NavigationManager::WebLoadParams params(URL);
4354 params.transition_type = ui::PAGE_TRANSITION_AUTO_BOOKMARK;
4355 [[tab webController] loadWithParams:params];
4356}
4357
4358- (void)showReadingList {
4359 DCHECK(reading_list::switches::IsReadingListEnabled());
4360 UIViewController* vc = [ReadingListViewControllerBuilder
4361 readingListViewControllerInBrowserState:self.browserState
4362 tabModel:_model];
4363 [self presentViewController:vc animated:YES completion:nil];
4364}
4365
4366- (void)showQRScanner {
4367 _qrScannerViewController.reset(
4368 [[QRScannerViewController alloc] initWithDelegate:_toolbarController]);
4369 [self presentViewController:[_qrScannerViewController
4370 getViewControllerToPresent]
4371 animated:YES
4372 completion:nil];
4373}
4374
4375- (void)showNTPPanel:(NewTabPage::PanelIdentifier)panel {
4376 DCHECK(self.visible || self.dismissingModal);
4377 GURL url(kChromeUINewTabURL);
4378 std::string fragment(NewTabPage::FragmentFromIdentifier(panel));
4379 if (fragment != "") {
4380 GURL::Replacements replacement;
4381 replacement.SetRefStr(fragment);
4382 url = url.ReplaceComponents(replacement);
4383 }
4384 Tab* tab = [_model currentTab];
4385 web::NavigationManager::WebLoadParams params(url);
4386 params.transition_type = ui::PAGE_TRANSITION_AUTO_BOOKMARK;
4387 [[tab webController] loadWithParams:params];
4388}
4389
4390- (void)showRateThisAppDialog {
4391 DCHECK(!_rateThisAppDialog.get());
4392
4393 // Store the current timestamp whenever this dialog is shown.
4394 _browserState->GetPrefs()->SetInt64(prefs::kRateThisAppDialogLastShownTime,
4395 base::Time::Now().ToInternalValue());
4396
4397 // Some versions of iOS7 do not support linking directly to the "Ratings and
4398 // Reviews" appstore page. For iOS7 fall back to an alternative URL that
4399 // links to the main appstore page for the Chrome app.
4400 NSURL* storeURL =
4401 [NSURL URLWithString:(@"itms-apps://itunes.apple.com/WebObjects/"
4402 @"MZStore.woa/wa/"
4403 @"viewContentsUserReviews?type=Purple+Software&id="
4404 @"535886823&pt=9008&ct=rating")];
4405
4406 base::RecordAction(base::UserMetricsAction("IOSRateThisAppDialogShown"));
4407 [self clearPresentedStateWithCompletion:nil];
4408
4409 _rateThisAppDialog.reset(
4410 ios::GetChromeBrowserProvider()->CreateAppRatingPrompt());
4411 [_rateThisAppDialog setAppStoreURL:storeURL];
4412 [_rateThisAppDialog setDelegate:self];
4413 [_rateThisAppDialog show];
4414}
4415
4416- (void)dismissRateThisAppDialog {
4417 if (_rateThisAppDialog.get()) {
4418 base::RecordAction(base::UserMetricsAction(
4419 "IOSRateThisAppDialogDismissedProgramatically"));
4420 [_rateThisAppDialog dismiss];
4421 _rateThisAppDialog.reset();
4422 }
4423}
4424
4425#if !defined(NDEBUG)
4426- (void)viewSource {
4427 Tab* tab = [_model currentTab];
4428 DCHECK(tab);
4429 CRWWebController* webController = tab.webController;
4430 NSString* script = @"document.documentElement.outerHTML;";
4431 base::WeakNSObject<Tab> weakTab(tab);
4432 web::JavaScriptResultBlock completionHandlerBlock = ^(id result, NSError*) {
4433 base::scoped_nsobject<Tab> strongTab([weakTab retain]);
4434 if (!strongTab)
4435 return;
4436 if (![result isKindOfClass:[NSString class]])
4437 result = @"Not an HTML page";
4438 std::string base64HTML;
4439 base::Base64Encode(base::SysNSStringToUTF8(result), &base64HTML);
4440 GURL URL(std::string("data:text/plain;charset=utf-8;base64,") + base64HTML);
4441 web::Referrer referrer([strongTab url], web::ReferrerPolicyDefault);
4442 [strongTab webPageOrderedOpen:URL
4443 referrer:referrer
4444 windowName:nil
4445 inBackground:NO];
4446 };
4447 [webController executeJavaScript:script
4448 completionHandler:completionHandlerBlock];
4449}
4450#endif // !defined(NDEBUG)
4451
4452- (void)startVoiceSearch {
4453 // Delay Voice Search until new tab animations have finished.
4454 if (_inNewTabAnimation) {
4455 _startVoiceSearchAfterNewTabAnimation = YES;
4456 return;
4457 }
4458
4459 // Keyboard shouldn't overlay the ecoutez window, so dismiss find in page and
4460 // dismiss the keyboard.
4461 [self closeFindInPage];
4462 [[_model currentTab].webController dismissKeyboard];
4463
4464 // Ensure that voice search objects are created.
4465 [self ensureVoiceSearchControllerCreated];
4466 [self ensureVoiceSearchBarCreated];
4467
4468 // Present voice search.
4469 [_voiceSearchBar prepareToPresentVoiceSearch];
4470 _voiceSearchController->StartRecognition(self, [_model currentTab]);
4471 [_toolbarController cancelOmniboxEdit];
4472}
4473
4474#pragma mark - ToolbarOwner
4475
4476- (ToolbarController*)relinquishedToolbarController {
4477 if (_isToolbarControllerRelinquished)
4478 return nil;
4479
4480 ToolbarController* relinquishedToolbarController = nil;
4481 if ([_toolbarController view].hidden) {
4482 Tab* currentTab = [_model currentTab];
4483 if (currentTab && UrlHasChromeScheme(currentTab.url)) {
4484 // Use the native content controller's toolbar when the BVC's is hidden.
4485 id nativeController = [self nativeControllerForTab:currentTab];
4486 if ([nativeController conformsToProtocol:@protocol(ToolbarOwner)]) {
4487 relinquishedToolbarController =
4488 [nativeController relinquishedToolbarController];
4489 _relinquishedToolbarOwner.reset(nativeController);
4490 }
4491 }
4492 } else {
4493 relinquishedToolbarController = _toolbarController.get();
4494 }
4495 _isToolbarControllerRelinquished = (relinquishedToolbarController != nil);
4496 return relinquishedToolbarController;
4497}
4498
4499- (void)reparentToolbarController {
4500 if (_isToolbarControllerRelinquished) {
4501 if ([[_toolbarController view] isDescendantOfView:self.view]) {
4502 // A native content controller's toolbar has been relinquished.
4503 [_relinquishedToolbarOwner reparentToolbarController];
4504 _relinquishedToolbarOwner.reset();
4505 } else if ([_findBarController isFindInPageShown]) {
4506 [self.view insertSubview:[_toolbarController view]
4507 belowSubview:[_findBarController view]];
4508 } else {
4509 [self.view addSubview:[_toolbarController view]];
4510 }
4511 if (_contextualSearchPanel) {
4512 // Move panel back into its correct place.
4513 [self.view insertSubview:_contextualSearchPanel
4514 aboveSubview:[_toolbarController view]];
4515 }
4516 _isToolbarControllerRelinquished = NO;
4517 }
4518}
4519
4520#pragma mark - TabModelObserver methods
4521
4522// Observer method, tab inserted.
4523- (void)tabModel:(TabModel*)model
4524 didInsertTab:(Tab*)tab
4525 atIndex:(NSUInteger)modelIndex
4526 inForeground:(BOOL)fg {
4527 DCHECK(tab);
4528 [self installDelegatesForTab:tab];
4529
4530 if (fg) {
4531 [_contextualSearchController setTab:tab];
4532 [_paymentRequestManager setWebState:tab.webState];
4533 }
4534}
4535
4536// Observer method, active tab changed.
4537- (void)tabModel:(TabModel*)model
4538 didChangeActiveTab:(Tab*)newTab
4539 previousTab:(Tab*)previousTab
4540 atIndex:(NSUInteger)index {
4541 // TODO(rohitrao): tabSelected expects to always be called with a non-nil tab.
4542 // Currently this observer method is always called with a non-nil |newTab|,
4543 // but that may change in the future. Remove this DCHECK when it does.
4544 DCHECK(newTab);
4545 if (_infoBarContainer.get()) {
4546 infobars::InfoBarManager* infoBarManager = [newTab infoBarManager];
4547 _infoBarContainer->ChangeInfoBarManager(infoBarManager);
4548 }
4549 [self updateVoiceSearchBarVisibilityAnimated:NO];
4550
4551 [_contextualSearchController setTab:newTab];
4552 [_paymentRequestManager setWebState:newTab.webState];
4553
4554 [self tabSelected:newTab];
4555 DCHECK_EQ(newTab, [model currentTab]);
4556 [self installDelegatesForTab:newTab];
4557}
4558
4559// Observer method, tab changed.
4560- (void)tabModel:(TabModel*)model didChangeTab:(Tab*)tab {
4561 DCHECK(tab && ([_model indexOfTab:tab] != NSNotFound));
4562 if (tab == [_model currentTab]) {
4563 [self updateToolbar];
4564 // Disable contextual search when |tab| is a voice search result tab.
4565 BOOL enableContextualSearch = self.active && !tab.isVoiceSearchResultsTab;
4566 [_contextualSearchController enableContextualSearch:enableContextualSearch];
4567 }
4568}
4569
4570// A tab has been removed, remove its views from display if necessary.
4571- (void)tabModel:(TabModel*)model
4572 didRemoveTab:(Tab*)tab
4573 atIndex:(NSUInteger)index {
4574 // Remove stored native controllers for the tab.
4575 [_nativeControllersForTabIDs removeObjectForKey:tab.tabId];
4576
4577 // Ignore changes while the tab stack view is visible (or while suspended).
4578 // The display will be refreshed when this view becomes active again.
4579 if (!self.visible || !model.webUsageEnabled)
4580 return;
4581
4582 // Remove the find bar for now.
4583 [self hideFindBarWithAnimation:NO];
4584}
4585
4586- (void)tabModel:(TabModel*)model willRemoveTab:(Tab*)tab {
4587 if (tab == [model currentTab]) {
4588 [_contentArea displayContentView:nil];
4589 [_toolbarController selectedTabChanged];
4590 }
4591
4592 [[UpgradeCenter sharedInstance] tabWillClose:tab.tabId];
4593 if ([model count] == 1) { // About to remove the last tab.
4594 [_contextualSearchController setTab:nil];
4595 [_paymentRequestManager setWebState:nil];
4596 }
4597}
4598
4599// Called when the number of tabs changes. Update the toolbar accordingly.
4600- (void)tabModelDidChangeTabCount:(TabModel*)model {
4601 DCHECK(model == _model);
4602 if ([_model count] == 1 && _noTabsController.get()) {
4603 [self dismissNoTabsUI];
4604 }
4605
4606 [_toolbarController setTabCount:[_model count]];
4607 // If the iPad tab switcher feature is enabled, the tab switcher is shown
4608 // instead of the no tabs UI. Showing the tab switcher in that case is
4609 // done from the main controller.
4610 // If the iPad tab switcher feature is disabled and the number of tabs is
4611 // zero, ensure the no-tabs UI is visible (such as if the last tab was closed
4612 // programmatically from JS). This is done on a delay because the
4613 // notification happens while the tab is going away and trying to trigger a
4614 // change during teardown causes problems.
4615 BOOL showNoTabsUIOnIPad =
4616 IsIPadIdiom() && !experimental_flags::IsTabSwitcherEnabled();
4617 if (![_model count] && showNoTabsUIOnIPad && !_isOffTheRecord) {
4618 [self performSelector:@selector(showNoTabsUI)
4619 withObject:nil
4620 afterDelay:0.01];
4621 }
4622}
4623
4624#pragma mark - Upgrade Detection
4625
4626- (void)showUpgrade:(UpgradeCenter*)center {
4627 // Add an infobar on all the open tabs.
4628 for (Tab* tab in _model.get()) {
4629 NSString* tabId = tab.tabId;
4630 DCHECK([tab infoBarManager]);
4631 [center addInfoBarToManager:[tab infoBarManager] forTabId:tabId];
4632 }
4633}
4634
4635#pragma mark - ContextualSearchControllerDelegate
4636
4637- (void)createTabFromContextualSearchController:(const GURL&)url {
4638 Tab* currentTab = [_model currentTab];
4639 DCHECK(currentTab);
4640 NSUInteger index = [_model indexOfTab:currentTab];
4641 [self addSelectedTabWithURL:url
4642 atIndex:index + 1
4643 transition:ui::PAGE_TRANSITION_LINK];
4644}
4645
4646- (void)promotePanelToTabProvidedBy:(id<ContextualSearchTabProvider>)tabProvider
4647 focusInput:(BOOL)focusInput {
4648 // Tell the panel it will be promoted.
4649 ContextualSearchPanelView* promotingPanel = _contextualSearchPanel;
4650 [promotingPanel prepareForPromotion];
4651
4652 // Make a new panel and tell the controller about it.
4653 _contextualSearchPanel = [self createPanelView];
4654 [self.view insertSubview:_contextualSearchPanel belowSubview:promotingPanel];
4655 [_contextualSearchController setPanel:_contextualSearchPanel];
4656
4657 // Figure out vertical offset.
4658 CGFloat offset = StatusBarHeight();
4659 if (IsIPadIdiom()) {
4660 offset = MAX(offset, CGRectGetMaxY([_tabStripController view].frame));
4661 }
4662
4663 // Transition steps: Animate the panel position, fade in the toolbar and
4664 // tab strip.
4665 ProceduralBlock transition = ^{
4666 [promotingPanel promoteToMatchSuperviewWithVerticalOffset:offset];
4667 [self updateToolbarControlsAlpha:1.0];
4668 [self updateToolbarBackgroundAlpha:1.0];
4669 [_tabStripController view].alpha = 1.0;
4670 };
4671
4672 // After the transition animation completes, add the tab to the tab model
4673 // (on iPad this triggers the tab strip animation too), then fade out the
4674 // transitioning panel and remove it.
4675 void (^completion)(BOOL) = ^(BOOL finished) {
4676 _contextualSearchMask.alpha = 0;
4677 Tab* newTab = [tabProvider releaseTab];
4678 DCHECK(newTab);
4679 DCHECK([newTab navigationManager]);
4680 // Add the new tab to the tab model.
4681 [newTab setParentTabModel:_model];
4682 // Insert the new tab one after the current tab.
4683 Tab* currentTab = [_model currentTab];
4684 NSUInteger index = [_model indexOfTab:currentTab];
4685 [_model insertTab:newTab atIndex:index + 1];
4686
4687 // Set isPrerenderTab to NO after inserting the tab. This will allow the
4688 // BrowserViewController to detect that a pre-rendered tab is switched in,
4689 // and show the prerendering animation. This needs to happen before the
4690 // tab is made the current tab.
4691 // This also enables contextual search (if otherwise applicable) on
4692 // |newTab|.
4693 newTab.isPrerenderTab = NO;
4694 [_model setCurrentTab:newTab];
4695
4696 BOOL loadingFinished = [newTab.webController loadPhase] == web::PAGE_LOADED;
4697 if (loadingFinished)
4698 [self tabLoadComplete:newTab withSuccess:YES];
4699
4700 if (focusInput) {
4701 [_toolbarController focusOmnibox];
4702 }
4703 _infoBarContainer->RestoreInfobars();
4704
4705 [UIView animateWithDuration:ios::material::kDuration2
4706 animations:^{
4707 promotingPanel.alpha = 0;
4708 }
4709 completion:^(BOOL finished) {
4710 [promotingPanel removeFromSuperview];
4711 }];
4712 };
4713
4714 [UIView animateWithDuration:ios::material::kDuration3
4715 animations:transition
4716 completion:completion];
4717}
4718
4719- (ContextualSearchPanelView*)createPanelView {
4720 PanelConfiguration* config;
4721 CGSize panelContainerSize = self.view.bounds.size;
4722 if (IsIPadIdiom()) {
4723 config = [PadPanelConfiguration
4724 configurationForContainerSize:panelContainerSize
4725 horizontalSizeClass:self.traitCollection.horizontalSizeClass];
4726 } else {
4727 config = [PhonePanelConfiguration
4728 configurationForContainerSize:panelContainerSize
4729 horizontalSizeClass:self.traitCollection.horizontalSizeClass];
4730 }
4731 ContextualSearchPanelView* newPanel = [[[ContextualSearchPanelView alloc]
4732 initWithConfiguration:config] autorelease];
4733 [newPanel addMotionObserver:self];
4734 [newPanel addMotionObserver:_contextualSearchMask];
4735 return newPanel;
4736}
4737
4738#pragma mark - ContextualSearchPanelMotionObserver
4739
4740- (void)panel:(ContextualSearchPanelView*)panel
4741 didMoveWithMotion:(ContextualSearch::PanelMotion)motion {
4742 // If the header is offset, it's offscreen (or moving offscreen) and the
4743 // toolbar shouldn't be opacity-adjusted by the contextual search panel.
4744 if ([self currentHeaderOffset] != 0)
4745 return;
4746
4747 CGFloat toolbarAlpha;
4748
4749 if (motion.state == ContextualSearch::PREVIEWING) {
4750 // As the panel moves past the previewing position, the toolbar should
4751 // become more transparent.
4752 toolbarAlpha = 1 - motion.gradation;
4753 } else if (motion.state == ContextualSearch::COVERING) {
4754 // The toolbar should be totally transparent when the panel is covering.
4755 toolbarAlpha = 0.0;
4756 } else {
4757 return;
4758 }
4759
4760 // On iPad, the toolbar doesn't go fully transparent, so map |toolbarAlpha|'s
4761 // [0-1.0] range to [0.5-1.0].
4762 if (IsIPadIdiom()) {
4763 toolbarAlpha = 0.5 + (toolbarAlpha * 0.5);
4764 [_tabStripController view].alpha = toolbarAlpha;
4765 }
4766
4767 [self updateToolbarControlsAlpha:toolbarAlpha];
4768 [self updateToolbarBackgroundAlpha:toolbarAlpha];
4769}
4770
4771- (void)panel:(ContextualSearchPanelView*)panel
4772 didChangeToState:(ContextualSearch::PanelState)toState
4773 fromState:(ContextualSearch::PanelState)fromState {
4774 if (toState == ContextualSearch::DISMISSED) {
4775 // Panel has become hidden.
4776 _infoBarContainer->RestoreInfobars();
4777 [self updateToolbarControlsAlpha:1.0];
4778 [self updateToolbarBackgroundAlpha:1.0];
4779 [_tabStripController view].alpha = 1.0;
4780 } else if (fromState == ContextualSearch::DISMISSED) {
4781 // Panel has become visible.
4782 _infoBarContainer->SuspendInfobars();
4783 }
4784}
4785
4786- (void)panelWillPromote:(ContextualSearchPanelView*)panel {
4787 [panel removeMotionObserver:self];
4788}
4789
4790- (CGFloat)currentHeaderHeight {
4791 return [self headerHeight] - [self currentHeaderOffset];
4792}
4793
4794#pragma mark - InfoBarControllerDelegate
4795
4796- (void)infoBarContainerStateChanged:(bool)isAnimating {
4797 InfoBarContainerView* infoBarContainerView = _infoBarContainer->view();
4798 DCHECK(infoBarContainerView);
4799 CGRect containerFrame = infoBarContainerView.frame;
4800 CGFloat height = [infoBarContainerView topmostVisibleInfoBarHeight];
4801 containerFrame.origin.y = CGRectGetMaxY(_contentArea.frame) - height;
4802 containerFrame.size.height = height;
4803 BOOL isViewVisible = self.visible;
4804 [UIView animateWithDuration:0.1
4805 animations:^{
4806 [infoBarContainerView setFrame:containerFrame];
4807 }
4808 completion:^(BOOL finished) {
4809 if (!isViewVisible)
4810 return;
4811 UIAccessibilityPostNotification(
4812 UIAccessibilityLayoutChangedNotification, infoBarContainerView);
4813 }];
4814}
4815
4816- (BOOL)shouldAutorotate {
4817 if (_voiceSearchController && _voiceSearchController->IsVisible()) {
4818 // Don't rotate if a voice search is being presented or dismissed. Once the
4819 // transition animations finish, only the Voice Search UIViewController's
4820 // |-shouldAutorotate| will be called.
4821 return NO;
4822 } else if (_sideSwipeController && ![_sideSwipeController shouldAutorotate]) {
4823 // Don't auto rotate if side swipe controller view says not to.
4824 return NO;
4825 } else {
4826 return [super shouldAutorotate];
4827 }
4828}
4829
4830// Always return yes, as this tap should work with various recognizers,
4831// including UITextTapRecognizer, UILongPressGestureRecognizer,
4832// UIScrollViewPanGestureRecognizer and others.
4833- (BOOL)gestureRecognizer:(UIGestureRecognizer*)gestureRecognizer
4834 shouldRecognizeSimultaneouslyWithGestureRecognizer:
4835 (UIGestureRecognizer*)otherGestureRecognizer {
4836 return YES;
4837}
4838
4839// Tap gestures should only be recognized within |_contentArea|.
4840- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer*)gesture {
4841 CGPoint location = [gesture locationInView:self.view];
4842
4843 // Only allow touches on descendant views of |_contentArea|.
4844 UIView* hitView = [self.view hitTest:location withEvent:nil];
4845 return (![hitView isDescendantOfView:_contentArea]) ? NO : YES;
4846}
4847
4848#pragma mark - SideSwipeController Delegate Methods
4849
4850- (void)sideSwipeViewDismissAnimationDidEnd:(UIView*)sideSwipeView {
4851 DCHECK(!IsIPadIdiom());
4852 // Update frame incase orientation changed while |_contentArea| was out of
4853 // the view hierarchy.
4854 [_contentArea setFrame:[sideSwipeView frame]];
4855
4856 [self.view insertSubview:_contentArea atIndex:0];
4857 [self updateVoiceSearchBarVisibilityAnimated:NO];
4858 [self updateToolbar];
4859
4860 // Reset horizontal stack view.
4861 [sideSwipeView removeFromSuperview];
4862 [_sideSwipeController setInSwipe:NO];
4863 [_infoBarContainer->view() setHidden:NO];
4864}
4865
4866- (UIView*)contentView {
4867 return _contentArea;
4868}
4869
4870- (TabStripController*)tabStripController {
4871 return _tabStripController;
4872}
4873
4874- (WebToolbarController*)toolbarController {
4875 return _toolbarController;
4876}
4877
4878- (BOOL)preventSideSwipe {
4879 if ([_toolbarController toolsPopupController])
4880 return YES;
4881
4882 if (_voiceSearchController && _voiceSearchController->IsVisible())
4883 return YES;
4884
4885 if ([_contextualSearchPanel state] >= ContextualSearch::PEEKING)
4886 return YES;
4887
4888 if (!self.active)
4889 return YES;
4890
4891 return NO;
4892}
4893
4894- (void)updateAccessoryViewsForSideSwipeWithVisibility:(BOOL)visible {
4895 if (visible) {
4896 [self updateVoiceSearchBarVisibilityAnimated:NO];
4897 [self updateToolbar];
4898 [_infoBarContainer->view() setHidden:NO];
4899 } else {
4900 // Hide UI accessories such as find bar and first visit overlays
4901 // for welcome page.
4902 [self hideFindBarWithAnimation:NO];
4903 [_infoBarContainer->view() setHidden:YES];
4904 [_voiceSearchBar setHidden:YES];
4905 }
4906}
4907
4908- (BOOL)verifyToolbarViewPlacementInView:(UIView*)views {
4909 BOOL seenToolbar = NO;
4910 BOOL seenInfoBarContainer = NO;
4911 BOOL seenContentArea = NO;
4912 for (UIView* view in views.subviews) {
4913 if (view == [_toolbarController view])
4914 seenToolbar = YES;
4915 else if (view == _infoBarContainer->view())
4916 seenInfoBarContainer = YES;
4917 else if (view == _contentArea)
4918 seenContentArea = YES;
4919 if ((seenToolbar && !seenInfoBarContainer) ||
4920 (seenInfoBarContainer && !seenContentArea))
4921 return NO;
4922 }
4923 return YES;
4924}
4925
4926#pragma mark - PreloadControllerDelegate methods
4927
4928- (BOOL)shouldUseDesktopUserAgent {
4929 return [_model currentTab].useDesktopUserAgent;
4930}
4931
4932- (CRWSessionEntry*)currentSessionEntry {
4933 Tab* tab = [_model currentTab];
4934 if (![tab navigationManager])
4935 return nil;
4936 return [[tab navigationManager]->GetSessionController() currentEntry];
4937}
4938
4939#pragma mark - BookmarkBridgeMethods
4940
4941// If an added or removed bookmark is the same as the current url, update the
4942// toolbar so the star highlight is kept in sync.
4943- (void)bookmarkNodeModified:(const BookmarkNode*)node {
4944 if ([_model currentTab] && node->url() == [_model currentTab].url)
4945 [self updateToolbar];
4946}
4947
4948// If all bookmarks are removed, update the toolbar so the star highlight is
4949// kept in sync.
4950- (void)allBookmarksRemoved {
4951 [self updateToolbar];
4952}
4953
4954#pragma mark - ShareToDelegate methods
4955
4956- (void)shareDidComplete:(ShareTo::ShareResult)shareStatus
4957 successMessage:(NSString*)message {
4958 // The shareTo dialog dismisses itself instead of through
4959 // |-dismissViewControllerAnimated:completion:| so we must reset the
4960 // presenting state here.
4961 self.presenting = NO;
4962 [self.dialogPresenter tryToPresent];
4963
4964 switch (shareStatus) {
4965 case ShareTo::SHARE_SUCCESS:
4966 if ([message length])
4967 [self showSnackbar:message];
4968 break;
4969 case ShareTo::SHARE_ERROR:
4970 [self showErrorAlert:IDS_IOS_SHARE_TO_ERROR_ALERT_TITLE
4971 message:IDS_IOS_SHARE_TO_ERROR_ALERT];
4972 break;
4973 case ShareTo::SHARE_NETWORK_FAILURE:
4974 [self showErrorAlert:IDS_IOS_SHARE_TO_NETWORK_ERROR_ALERT_TITLE
4975 message:IDS_IOS_SHARE_TO_NETWORK_ERROR_ALERT];
4976 break;
4977 case ShareTo::SHARE_SIGN_IN_FAILURE:
4978 [self showErrorAlert:IDS_IOS_SHARE_TO_SIGN_IN_ERROR_ALERT_TITLE
4979 message:IDS_IOS_SHARE_TO_SIGN_IN_ERROR_ALERT];
4980 break;
4981 case ShareTo::SHARE_CANCEL:
4982 case ShareTo::SHARE_UNKNOWN_RESULT:
4983 break;
4984 }
4985}
4986
4987- (void)passwordAppExDidFinish:(ShareTo::ShareResult)shareStatus
4988 username:(NSString*)username
4989 password:(NSString*)password
4990 successMessage:(NSString*)message {
4991 switch (shareStatus) {
4992 case ShareTo::SHARE_SUCCESS: {
4993 PasswordController* passwordController =
4994 [[_model currentTab] passwordController];
4995 __block BOOL shown = NO;
4996 [passwordController findAndFillPasswordForms:username
4997 password:password
4998 completionHandler:^(BOOL completed) {
4999 if (shown || !completed || ![message length])
5000 return;
5001 [self showSnackbar:message];
5002 shown = YES;
5003 }];
5004 break;
5005 }
5006 default:
5007 break;
5008 }
5009}
5010
5011- (void)showErrorAlert:(int)titleMessageId message:(int)messageId {
5012 NSString* title = l10n_util::GetNSString(titleMessageId);
5013 NSString* message = l10n_util::GetNSString(messageId);
5014 [self showErrorAlertWithStringTitle:title message:message];
5015}
5016
5017- (void)showErrorAlertWithStringTitle:(NSString*)title
5018 message:(NSString*)message {
5019 // Dismiss current alert.
5020 [_alertCoordinator stop];
5021
5022 _alertCoordinator.reset(
5023 [[_dependencyFactory alertCoordinatorWithTitle:title
5024 message:message
5025 viewController:self] retain]);
5026 [_alertCoordinator start];
5027}
5028
5029- (void)showSnackbar:(NSString*)message {
5030 [_dependencyFactory showSnackbarWithMessage:message];
5031}
5032
5033#pragma mark - Show Mail Composer methods
5034
5035- (void)showMailComposer:(id)sender {
5036 ShowMailComposerCommand* command = (ShowMailComposerCommand*)sender;
5037 if (![MFMailComposeViewController canSendMail]) {
5038 NSString* alertTitle =
5039 l10n_util::GetNSString([command emailNotConfiguredAlertTitleId]);
5040 NSString* alertMessage =
5041 l10n_util::GetNSString([command emailNotConfiguredAlertMessageId]);
5042 [self showErrorAlertWithStringTitle:alertTitle message:alertMessage];
5043 return;
5044 }
5045 base::scoped_nsobject<MFMailComposeViewController> mailViewController(
5046 [[MFMailComposeViewController alloc] init]);
5047 [mailViewController setModalPresentationStyle:UIModalPresentationFormSheet];
5048 [mailViewController setToRecipients:[command toRecipients]];
5049 [mailViewController setSubject:[command subject]];
5050 [mailViewController setMessageBody:[command body] isHTML:NO];
5051
5052 const base::FilePath& textFile = [command textFileToAttach];
5053 if (!textFile.empty()) {
5054 NSString* filename = base::SysUTF8ToNSString(textFile.value());
5055 NSData* data = [NSData dataWithContentsOfFile:filename];
5056 if (data) {
5057 NSString* displayName =
5058 base::SysUTF8ToNSString(textFile.BaseName().value());
5059 [mailViewController addAttachmentData:data
5060 mimeType:@"text/plain"
5061 fileName:displayName];
5062 }
5063 }
5064
5065 [mailViewController setMailComposeDelegate:self];
5066 [self presentViewController:mailViewController animated:YES completion:nil];
5067}
5068
5069#pragma mark - MFMailComposeViewControllerDelegate methods
5070
5071- (void)mailComposeController:(MFMailComposeViewController*)controller
5072 didFinishWithResult:(MFMailComposeResult)result
5073 error:(NSError*)error {
5074 [self dismissViewControllerAnimated:YES completion:nil];
5075}
5076
5077#pragma mark - StoreKitLauncher methods
5078
5079- (void)productViewControllerDidFinish:
5080 (SKStoreProductViewController*)viewController {
5081 [self dismissViewControllerAnimated:YES completion:nil];
5082}
5083
5084- (void)openAppStore:(NSString*)appId {
5085 if (![appId length])
5086 return;
5087 NSDictionary* product =
5088 @{SKStoreProductParameterITunesItemIdentifier : appId};
5089 base::scoped_nsobject<SKStoreProductViewController> storeViewController(
5090 [[SKStoreProductViewController alloc] init]);
5091 [storeViewController setDelegate:self];
5092 [storeViewController loadProductWithParameters:product completionBlock:nil];
5093 [self presentViewController:storeViewController animated:YES completion:nil];
5094}
5095
5096#pragma mark - TabDialogDelegate methods
5097
sdefresnee65fd872016-12-19 13:38:135098- (void)cancelDialogForTab:(Tab*)tab {
5099 [self.dialogPresenter cancelDialogForWebState:tab.webState];
5100}
5101
5102#pragma mark - FKFeedbackPromptDelegate methods
5103
5104- (void)userTappedRateApp:(UIView*)view {
5105 base::RecordAction(base::UserMetricsAction("IOSRateThisAppRateChosen"));
5106 _rateThisAppDialog.reset();
5107}
5108
5109- (void)userTappedSendFeedback:(UIView*)view {
5110 base::RecordAction(base::UserMetricsAction("IOSRateThisAppFeedbackChosen"));
5111 _rateThisAppDialog.reset();
5112 base::scoped_nsobject<GenericChromeCommand> command(
5113 [[GenericChromeCommand alloc] initWithTag:IDC_REPORT_AN_ISSUE]);
5114 [self chromeExecuteCommand:command];
5115}
5116
5117- (void)userTappedDismiss:(UIView*)view {
5118 base::RecordAction(base::UserMetricsAction("IOSRateThisAppDismissChosen"));
5119 _rateThisAppDialog.reset();
5120}
5121
5122#pragma mark - VoiceSearchBarDelegate
5123
5124- (BOOL)isTTSEnabledForVoiceSearchBar:(id<VoiceSearchBar>)voiceSearchBar {
5125 DCHECK_EQ(_voiceSearchBar.get(), voiceSearchBar);
5126 [self ensureVoiceSearchControllerCreated];
5127 return _voiceSearchController->IsTextToSpeechEnabled() &&
5128 _voiceSearchController->IsTextToSpeechSupported();
5129}
5130
5131- (void)voiceSearchBarDidUpdateButtonState:(id<VoiceSearchBar>)voiceSearchBar {
5132 DCHECK_EQ(_voiceSearchBar.get(), voiceSearchBar);
5133 [self.tabModel.currentTab updateSnapshotWithOverlay:YES visibleFrameOnly:YES];
5134}
5135
5136#pragma mark - VoiceSearchPresenter
5137
5138- (UIView*)voiceSearchButton {
5139 return _voiceSearchButton;
5140}
5141
5142- (id<LogoAnimationControllerOwner>)logoAnimationControllerOwner {
5143 return [self currentLogoAnimationControllerOwner];
5144}
5145
5146@end