| // Copyright 2012 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #import "ios/chrome/browser/tabs/tab_model.h" |
| |
| #include <list> |
| #include <utility> |
| #include <vector> |
| |
| #include "base/bind.h" |
| #import "base/ios/crb_protocol_observers.h" |
| #include "base/logging.h" |
| #import "base/mac/scoped_nsobject.h" |
| #include "base/metrics/histogram.h" |
| #include "base/metrics/user_metrics.h" |
| #include "base/metrics/user_metrics_action.h" |
| #include "base/strings/sys_string_conversions.h" |
| #include "components/sessions/core/serialized_navigation_entry.h" |
| #include "components/sessions/core/session_id.h" |
| #include "components/sessions/core/tab_restore_service.h" |
| #include "components/sessions/ios/ios_live_tab.h" |
| #include "ios/chrome/browser/browser_state/chrome_browser_state.h" |
| #include "ios/chrome/browser/chrome_url_constants.h" |
| #import "ios/chrome/browser/chrome_url_util.h" |
| #import "ios/chrome/browser/metrics/tab_usage_recorder.h" |
| #include "ios/chrome/browser/sessions/ios_chrome_tab_restore_service_factory.h" |
| #import "ios/chrome/browser/sessions/session_service.h" |
| #import "ios/chrome/browser/sessions/session_window.h" |
| #import "ios/chrome/browser/snapshots/snapshot_cache.h" |
| #include "ios/chrome/browser/tab_parenting_global_observer.h" |
| #import "ios/chrome/browser/tabs/tab.h" |
| #import "ios/chrome/browser/tabs/tab_model_list.h" |
| #import "ios/chrome/browser/tabs/tab_model_observer.h" |
| #import "ios/chrome/browser/tabs/tab_model_order_controller.h" |
| #import "ios/chrome/browser/tabs/tab_model_synced_window_delegate.h" |
| #import "ios/chrome/browser/xcallback_parameters.h" |
| #import "ios/web/navigation/crw_session_certificate_policy_manager.h" |
| #import "ios/web/navigation/crw_session_controller.h" |
| #include "ios/web/public/browser_state.h" |
| #include "ios/web/public/certificate_policy_cache.h" |
| #include "ios/web/public/navigation_item.h" |
| #import "ios/web/public/navigation_manager.h" |
| #include "ios/web/public/web_thread.h" |
| #import "ios/web/web_state/ui/crw_web_controller.h" |
| #import "ios/web/web_state/web_state_impl.h" |
| #include "url/gurl.h" |
| |
| NSString* const kTabModelTabWillStartLoadingNotification = |
| @"kTabModelTabWillStartLoadingNotification"; |
| NSString* const kTabModelUserNavigatedNotification = @"kTabModelUserNavigation"; |
| NSString* const kTabModelTabDidStartLoadingNotification = |
| @"kTabModelTabDidStartLoadingNotification"; |
| NSString* const kTabModelTabDidFinishLoadingNotification = |
| @"kTabModelTabDidFinishLoadingNotification"; |
| NSString* const kTabModelAllTabsDidCloseNotification = |
| @"kTabModelAllTabsDidCloseNotification"; |
| NSString* const kTabModelTabDeselectedNotification = |
| @"kTabModelTabDeselectedNotification"; |
| NSString* const kTabModelNewTabWillOpenNotification = |
| @"kTabModelNewTabWillOpenNotification"; |
| NSString* const kTabModelTabKey = @"tab"; |
| NSString* const kTabModelPageLoadSuccess = @"pageLoadSuccess"; |
| NSString* const kTabModelOpenInBackgroundKey = @"shouldOpenInBackground"; |
| |
| namespace { |
| |
| // Updates CRWSessionCertificatePolicyManager's certificate policy cache. |
| void UpdateCertificatePolicyCacheFromWebState(web::WebStateImpl* webState) { |
| DCHECK([NSThread isMainThread]); |
| DCHECK(webState); |
| scoped_refptr<web::CertificatePolicyCache> policy_cache = |
| web::BrowserState::GetCertificatePolicyCache(webState->GetBrowserState()); |
| CRWSessionController* controller = |
| webState->GetNavigationManagerImpl().GetSessionController(); |
| [[controller sessionCertificatePolicyManager] |
| updateCertificatePolicyCache:policy_cache]; |
| } |
| |
| // Populates the certificate policy cache based on the current entries of the |
| // given tabs. |
| void RestoreCertificatePolicyCacheFromTabs(NSArray* tabs) { |
| DCHECK([NSThread isMainThread]); |
| for (Tab* tab in tabs) { |
| UpdateCertificatePolicyCacheFromWebState(tab.webStateImpl); |
| } |
| } |
| |
| // Scrubs the certificate policy cache of all the certificate policies except |
| // those for the current entries of the given tabs. |
| void CleanCertificatePolicyCache( |
| scoped_refptr<web::CertificatePolicyCache> policy_cache, |
| NSArray* tabs) { |
| DCHECK_CURRENTLY_ON(web::WebThread::IO); |
| DCHECK(policy_cache); |
| policy_cache->ClearCertificatePolicies(); |
| web::WebThread::PostTask( |
| web::WebThread::UI, FROM_HERE, |
| base::Bind(&RestoreCertificatePolicyCacheFromTabs, tabs)); |
| } |
| |
| } // anonymous namespace |
| |
| @interface TabModelObservers : CRBProtocolObservers<TabModelObserver> |
| @end |
| @implementation TabModelObservers |
| @end |
| |
| @interface TabModel ()<TabUsageRecorderDelegate> { |
| // Array of |Tab| objects. |
| base::scoped_nsobject<NSMutableArray> _tabs; |
| // Maintains policy for where new tabs go and the selection when a tab |
| // is removed. |
| base::scoped_nsobject<TabModelOrderController> _orderController; |
| // The delegate for sync. |
| std::unique_ptr<TabModelSyncedWindowDelegate> _syncedWindowDelegate; |
| // Currently selected tab. May be nil. |
| base::WeakNSObject<Tab> _currentTab; |
| |
| // Counters for metrics. |
| int _openedTabCount; |
| int _closedTabCount; |
| int _newTabCount; |
| |
| // Backs up property with the same name. |
| std::unique_ptr<TabUsageRecorder> _tabUsageRecorder; |
| // Backs up property with the same name. |
| const SessionID _sessionID; |
| // Saves session's state. |
| base::scoped_nsobject<SessionServiceIOS> _sessionService; |
| // List of TabModelObservers. |
| base::scoped_nsobject<TabModelObservers> _observers; |
| } |
| |
| // Session window for the contents of the tab model. |
| @property(nonatomic, readonly) SessionWindowIOS* windowForSavingSession; |
| |
| // Returns YES if tab URL host indicates that tab is an NTP tab. |
| - (BOOL)isNTPTab:(Tab*)tab; |
| |
| // Opens a tab at the specified URL and registers its JS-supplied window name if |
| // appropriate. For certain transition types, will consult the order controller |
| // and thus may only use |index| as a hint. |parentTab| may be nil if there |
| // is no parent associated with this new tab, as may |windowName| if not |
| // applicable. |openedByDOM| is YES if the page was opened by DOM. |
| // The |index| parameter can be set to |
| // TabModelConstants::kTabPositionAutomatically if the caller doesn't have a |
| // preference for the position of the tab. |
| - (Tab*)insertTabWithLoadParams: |
| (const web::NavigationManager::WebLoadParams&)params |
| windowName:(NSString*)windowName |
| opener:(Tab*)parentTab |
| openedByDOM:(BOOL)openedByDOM |
| atIndex:(NSUInteger)index |
| inBackground:(BOOL)inBackground; |
| // Call to switch the selected tab. Broadcasts about the change in selection. |
| // It's ok for |newTab| to be nil in case the last tab is going away. In that |
| // case, the "tab deselected" notification gets sent, but no corresponding |
| // "tab selected" notification is sent. |persist| indicates whether or not |
| // the tab's state should be persisted in history upon switching. |
| - (void)changeSelectedTabFrom:(Tab*)oldTab |
| to:(Tab*)newTab |
| persistState:(BOOL)persist; |
| // Tells the snapshot cache the adjacent tab session ids. |
| - (void)updateSnapshotCache:(Tab*)tab; |
| // Helper method that posts a notification with the given name with |tab| |
| // in the userInfo dictionary under the kTabModelTabKey. |
| - (void)postNotificationName:(NSString*)notificationName withTab:(Tab*)tab; |
| @end |
| |
| @implementation TabModel |
| |
| @synthesize browserState = _browserState; |
| @synthesize sessionID = _sessionID; |
| @synthesize webUsageEnabled = webUsageEnabled_; |
| |
| #pragma mark - Overriden |
| |
| - (void)dealloc { |
| DCHECK([_observers empty]); |
| // browserStateDestroyed should always have been called before destruction. |
| DCHECK(!_browserState); |
| |
| [[NSNotificationCenter defaultCenter] removeObserver:self]; |
| // Make sure the tabs do clean after themselves. It is important for |
| // removeObserver: to be called first otherwise a lot of unecessary work will |
| // happen on -closeAllTabs. |
| [self closeAllTabs]; |
| |
| [super dealloc]; |
| } |
| |
| #pragma mark - Public methods |
| |
| - (Tab*)currentTab { |
| return _currentTab.get(); |
| } |
| |
| - (void)setCurrentTab:(Tab*)newTab { |
| DCHECK([_tabs containsObject:newTab]); |
| if (_currentTab != newTab) { |
| base::RecordAction(base::UserMetricsAction("MobileTabSwitched")); |
| [self updateSnapshotCache:newTab]; |
| } |
| if (_tabUsageRecorder) { |
| _tabUsageRecorder->RecordTabSwitched(_currentTab, newTab); |
| } |
| [self changeSelectedTabFrom:_currentTab to:newTab persistState:YES]; |
| } |
| |
| - (TabModelSyncedWindowDelegate*)syncedWindowDelegate { |
| return _syncedWindowDelegate.get(); |
| } |
| |
| - (TabUsageRecorder*)tabUsageRecorder { |
| return _tabUsageRecorder.get(); |
| } |
| |
| - (BOOL)isOffTheRecord { |
| return _browserState && _browserState->IsOffTheRecord(); |
| } |
| |
| - (BOOL)isEmpty { |
| return self.count == 0; |
| } |
| |
| - (NSUInteger)count { |
| return [_tabs count]; |
| } |
| |
| - (instancetype)initWithSessionWindow:(SessionWindowIOS*)window |
| sessionService:(SessionServiceIOS*)service |
| browserState:(ios::ChromeBrowserState*)browserState { |
| if ((self = [super init])) { |
| _observers.reset([[TabModelObservers |
| observersWithProtocol:@protocol(TabModelObserver)] retain]); |
| |
| _browserState = browserState; |
| DCHECK(_browserState); |
| |
| // There must be a valid session service defined to consume session windows. |
| DCHECK(service); |
| _sessionService.reset([service retain]); |
| |
| // Normal browser states are the only ones to get tab restore. Tab sync |
| // handles incognito browser states by filtering on profile, so it's |
| // important to the backend code to always have a sync window delegate. |
| if (!_browserState->IsOffTheRecord()) { |
| // Set up the usage recorder before tabs are created. |
| _tabUsageRecorder.reset(new TabUsageRecorder(self)); |
| } |
| _syncedWindowDelegate.reset(new TabModelSyncedWindowDelegate(self)); |
| |
| _tabs.reset([[NSMutableArray alloc] init]); |
| NSNotificationCenter* defaultCenter = [NSNotificationCenter defaultCenter]; |
| if (window) { |
| while (window.unclaimedSessions) { |
| std::unique_ptr<web::WebStateImpl> webState = [window nextSession]; |
| DCHECK_EQ(webState->GetBrowserState(), _browserState); |
| // Restore the CertificatePolicyCache. |
| UpdateCertificatePolicyCacheFromWebState(webState.get()); |
| // Create a new tab for each entry in the window. Don't send delegate |
| // notifications for each restored tab, only when all done. |
| base::scoped_nsobject<Tab> tab( |
| [[Tab alloc] initWithWebState:std::move(webState) model:self]); |
| [tab webController].usePlaceholderOverlay = YES; |
| [tab fetchFavicon]; |
| [_tabs addObject:tab]; |
| |
| TabParentingGlobalObserver::GetInstance()->OnTabParented( |
| [tab webStateImpl]); |
| } |
| if ([_tabs count]) { |
| DCHECK(window.selectedIndex < [_tabs count]); |
| _currentTab.reset([self tabAtIndex:window.selectedIndex]); |
| DCHECK(_currentTab); |
| if (_tabUsageRecorder) |
| _tabUsageRecorder->InitialRestoredTabs(_currentTab, _tabs); |
| // Perform initializations for affiliated objects which update the |
| // session information related to the current tab. |
| [_currentTab updateLastVisitedTimestamp]; |
| [self saveSessionImmediately:NO]; |
| } |
| } |
| |
| _orderController.reset( |
| [[TabModelOrderController alloc] initWithTabModel:self]); |
| // Register for resign active notification. |
| [defaultCenter addObserver:self |
| selector:@selector(willResignActive:) |
| name:UIApplicationWillResignActiveNotification |
| object:nil]; |
| // Register for background notification. |
| [defaultCenter addObserver:self |
| selector:@selector(applicationDidEnterBackground:) |
| name:UIApplicationDidEnterBackgroundNotification |
| object:nil]; |
| // Register for foregrounding notification. |
| [defaultCenter addObserver:self |
| selector:@selector(applicationWillEnterForeground:) |
| name:UIApplicationWillEnterForegroundNotification |
| object:nil]; |
| |
| // Associate with ios::ChromeBrowserState. |
| RegisterTabModelWithChromeBrowserState(_browserState, self); |
| } |
| return self; |
| } |
| |
| - (instancetype)init { |
| NOTREACHED(); |
| return nil; |
| } |
| |
| - (BOOL)restoreSessionWindow:(SessionWindowIOS*)window { |
| DCHECK(_browserState); |
| DCHECK(window); |
| if (!window.unclaimedSessions) |
| return NO; |
| size_t oldCount = [_tabs count]; |
| size_t index = oldCount; |
| while (window.unclaimedSessions) { |
| std::unique_ptr<web::WebStateImpl> webState = [window nextSession]; |
| DCHECK_EQ(webState->GetBrowserState(), _browserState); |
| Tab* tab = [self insertTabWithWebState:std::move(webState) atIndex:index++]; |
| tab.webController.usePlaceholderOverlay = YES; |
| // Restore the CertificatePolicyCache. Note that after calling Pass() |
| // |webState| is invalid, so we need to get the webstate from |tab|. |
| UpdateCertificatePolicyCacheFromWebState(tab.webStateImpl); |
| } |
| DCHECK([_tabs count] > oldCount); |
| // If any tab was restored, the saved selected tab must be selected. |
| if ([_tabs count] > oldCount) { |
| NSUInteger selectedIndex = window.selectedIndex; |
| if (selectedIndex == NSNotFound) |
| selectedIndex = oldCount; |
| else |
| selectedIndex += oldCount; |
| DCHECK(selectedIndex < [_tabs count]); |
| Tab* newTab = [self tabAtIndex:selectedIndex]; |
| DCHECK(newTab); |
| [self changeSelectedTabFrom:_currentTab to:newTab persistState:YES]; |
| |
| // If there was only one tab and it was the new tab page, clobber it. |
| if (oldCount == 1) { |
| Tab* tab = [_tabs objectAtIndex:0]; |
| if (tab.url == GURL(kChromeUINewTabURL)) { |
| [self closeTab:tab]; |
| if (_tabUsageRecorder) |
| _tabUsageRecorder->InitialRestoredTabs(_currentTab, _tabs); |
| return YES; |
| } |
| } |
| if (_tabUsageRecorder) { |
| _tabUsageRecorder->InitialRestoredTabs( |
| _currentTab, |
| [_tabs subarrayWithRange:NSMakeRange(oldCount, |
| [_tabs count] - oldCount)]); |
| } |
| } |
| return NO; |
| } |
| |
| - (void)saveSessionImmediately:(BOOL)immediately { |
| // Do nothing if there are tabs in the model but no selected tab. This is |
| // a transitional state. |
| if ((!_currentTab && [_tabs count]) || !_browserState) |
| return; |
| [_sessionService saveWindow:self.windowForSavingSession |
| forBrowserState:_browserState |
| immediately:immediately]; |
| } |
| |
| - (Tab*)tabAtIndex:(NSUInteger)index { |
| return [_tabs objectAtIndex:index]; |
| } |
| |
| - (NSUInteger)indexOfTab:(Tab*)tab { |
| return [_tabs indexOfObject:tab]; |
| } |
| |
| - (Tab*)tabWithWindowName:(NSString*)windowName { |
| if (!windowName) |
| return nil; |
| for (Tab* tab in _tabs.get()) { |
| if ([windowName isEqualToString:tab.windowName]) { |
| return tab; |
| } |
| } |
| return nil; |
| } |
| |
| - (Tab*)nextTabWithOpener:(Tab*)tab afterTab:(Tab*)afterTab { |
| NSUInteger startIndex = NSNotFound; |
| // Start looking after |afterTab|. If it's not found, start looking after |
| // |tab|. If it's not found either, bail. |
| if (afterTab) |
| startIndex = [self indexOfTab:afterTab]; |
| if (startIndex == NSNotFound) |
| startIndex = [self indexOfTab:tab]; |
| if (startIndex == NSNotFound) |
| return nil; |
| NSString* parentID = [tab currentSessionID]; |
| for (NSUInteger i = startIndex + 1; i < [_tabs count]; ++i) { |
| Tab* current = [_tabs objectAtIndex:i]; |
| DCHECK([current navigationManager]); |
| CRWSessionController* sessionController = |
| [current navigationManager]->GetSessionController(); |
| if ([sessionController.openerId isEqualToString:parentID]) |
| return current; |
| } |
| return nil; |
| } |
| |
| - (Tab*)firstTabWithOpener:(Tab*)tab { |
| if (!tab) |
| return nil; |
| NSUInteger stopIndex = [self indexOfTab:tab]; |
| if (stopIndex == NSNotFound) |
| return nil; |
| NSString* parentID = [tab currentSessionID]; |
| // Match the navigation index as well as the session id, to better match the |
| // state of the tab. I.e. two tabs are opened via a link from tab A, and then |
| // a new url is loaded into tab A, and more tabs opened from that url, the |
| // latter two tabs should not be grouped with the former two. The navigation |
| // index is the simplest way to detect navigation changes. |
| DCHECK([tab navigationManager]); |
| NSInteger parentNavIndex = [tab navigationManager]->GetCurrentItemIndex(); |
| for (NSUInteger i = 0; i < stopIndex; ++i) { |
| Tab* tabToCheck = [_tabs objectAtIndex:i]; |
| DCHECK([tabToCheck navigationManager]); |
| CRWSessionController* sessionController = |
| [tabToCheck navigationManager]->GetSessionController(); |
| if ([sessionController.openerId isEqualToString:parentID] && |
| sessionController.openerNavigationIndex == parentNavIndex) { |
| return tabToCheck; |
| } |
| } |
| return nil; |
| } |
| |
| - (Tab*)lastTabWithOpener:(Tab*)tab { |
| NSUInteger startIndex = [self indexOfTab:tab]; |
| if (startIndex == NSNotFound) |
| return nil; |
| // There is at least one tab in the model, because otherwise the above check |
| // would have returned. |
| NSString* parentID = [tab currentSessionID]; |
| DCHECK([tab navigationManager]); |
| NSInteger parentNavIndex = [tab navigationManager]->GetCurrentItemIndex(); |
| |
| Tab* match = nil; |
| // Find the last tab in the first matching 'group'. A 'group' is a set of |
| // tabs whose opener's id and opener's navigation index match. The navigation |
| // index is used in addition to the session id to detect navigations changes |
| // within the same session. |
| for (NSUInteger i = startIndex + 1; i < [_tabs count]; ++i) { |
| Tab* tabToCheck = [_tabs objectAtIndex:i]; |
| DCHECK([tabToCheck navigationManager]); |
| CRWSessionController* sessionController = |
| [tabToCheck navigationManager]->GetSessionController(); |
| if ([sessionController.openerId isEqualToString:parentID] && |
| sessionController.openerNavigationIndex == parentNavIndex) { |
| match = tabToCheck; |
| } else if (match) { |
| break; |
| } |
| } |
| return match; |
| } |
| |
| - (Tab*)openerOfTab:(Tab*)tab { |
| if (![tab navigationManager]) |
| return nil; |
| NSString* opener = [tab navigationManager]->GetSessionController().openerId; |
| if (!opener.length) // Short-circuit if opener is empty. |
| return nil; |
| for (Tab* iteratedTab in _tabs.get()) { |
| if ([[iteratedTab currentSessionID] isEqualToString:opener]) |
| return iteratedTab; |
| } |
| return nil; |
| } |
| |
| - (Tab*)insertOrUpdateTabWithURL:(const GURL&)URL |
| referrer:(const web::Referrer&)referrer |
| transition:(ui::PageTransition)transition |
| windowName:(NSString*)windowName |
| opener:(Tab*)parentTab |
| openedByDOM:(BOOL)openedByDOM |
| atIndex:(NSUInteger)index |
| inBackground:(BOOL)inBackground { |
| web::NavigationManager::WebLoadParams params(URL); |
| params.referrer = referrer; |
| params.transition_type = transition; |
| return [self insertOrUpdateTabWithLoadParams:params |
| windowName:windowName |
| opener:parentTab |
| openedByDOM:openedByDOM |
| atIndex:index |
| inBackground:inBackground]; |
| } |
| |
| - (Tab*)insertOrUpdateTabWithLoadParams: |
| (const web::NavigationManager::WebLoadParams&)loadParams |
| windowName:(NSString*)windowName |
| opener:(Tab*)parentTab |
| openedByDOM:(BOOL)openedByDOM |
| atIndex:(NSUInteger)index |
| inBackground:(BOOL)inBackground { |
| // Find the tab for the given window name. If found, load with |
| // |originalParams| in it, otherwise create a new tab for it. |
| Tab* tab = [self tabWithWindowName:windowName]; |
| if (tab) { |
| // Updating a tab shouldn't be possible with web usage suspended, since |
| // whatever page would be driving it should also be suspended. |
| DCHECK(webUsageEnabled_); |
| |
| web::NavigationManager::WebLoadParams updatedParams(loadParams); |
| updatedParams.is_renderer_initiated = (parentTab != nil); |
| [tab.webController loadWithParams:updatedParams]; |
| |
| // Force the page to start loading even if it's in the background. |
| [tab.webController triggerPendingLoad]; |
| |
| if (!inBackground) |
| [self setCurrentTab:tab]; |
| } else { |
| tab = [self insertTabWithLoadParams:loadParams |
| windowName:windowName |
| opener:parentTab |
| openedByDOM:openedByDOM |
| atIndex:index |
| inBackground:inBackground]; |
| } |
| |
| return tab; |
| } |
| |
| - (Tab*)insertBlankTabWithTransition:(ui::PageTransition)transition |
| opener:(Tab*)parentTab |
| openedByDOM:(BOOL)openedByDOM |
| atIndex:(NSUInteger)index |
| inBackground:(BOOL)inBackground { |
| GURL emptyURL; |
| web::NavigationManager::WebLoadParams params(emptyURL); |
| params.transition_type = transition; |
| // Tabs open by DOM are always renderer initiated. |
| params.is_renderer_initiated = openedByDOM; |
| return [self insertTabWithLoadParams:params |
| windowName:nil |
| opener:parentTab |
| openedByDOM:openedByDOM |
| atIndex:index |
| inBackground:inBackground]; |
| } |
| |
| - (Tab*)insertTabWithWebState:(std::unique_ptr<web::WebState>)webState |
| atIndex:(NSUInteger)index { |
| DCHECK(_browserState); |
| DCHECK_EQ(webState->GetBrowserState(), _browserState); |
| base::scoped_nsobject<Tab> tab( |
| [[Tab alloc] initWithWebState:std::move(webState) model:self]); |
| [tab webController].webUsageEnabled = webUsageEnabled_; |
| [self insertTab:tab atIndex:index]; |
| return tab; |
| } |
| |
| - (void)insertTab:(Tab*)tab atIndex:(NSUInteger)index { |
| DCHECK(tab); |
| DCHECK(index <= [_tabs count]); |
| [tab fetchFavicon]; |
| [_tabs insertObject:tab atIndex:index]; |
| |
| [_observers tabModel:self didInsertTab:tab atIndex:index inForeground:NO]; |
| [_observers tabModelDidChangeTabCount:self]; |
| |
| base::RecordAction(base::UserMetricsAction("MobileNewTabOpened")); |
| // Persist the session due to a new tab being inserted. If this is a |
| // background tab (will not become active), saving now will capture the |
| // state properly. If it does eventually become active, another save will |
| // be triggered to properly capture the end result. |
| [self saveSessionImmediately:NO]; |
| ++_newTabCount; |
| } |
| |
| - (void)moveTab:(Tab*)tab toIndex:(NSUInteger)toIndex { |
| NSUInteger fromIndex = [self indexOfTab:tab]; |
| DCHECK_NE(NSNotFound, static_cast<NSInteger>(fromIndex)); |
| DCHECK_LT(toIndex, self.count); |
| if (fromIndex == NSNotFound || toIndex >= self.count || |
| fromIndex == toIndex) { |
| return; |
| } |
| |
| base::scoped_nsobject<Tab> tabSaver([tab retain]); |
| [_tabs removeObject:tab]; |
| [_tabs insertObject:tab atIndex:toIndex]; |
| |
| [_observers tabModel:self didMoveTab:tab fromIndex:fromIndex toIndex:toIndex]; |
| } |
| |
| - (void)replaceTab:(Tab*)oldTab |
| withTab:(Tab*)newTab |
| keepOldTabOpen:(BOOL)keepOldTabOpen { |
| NSUInteger index = [self indexOfTab:oldTab]; |
| DCHECK_NE(NSNotFound, static_cast<NSInteger>(index)); |
| |
| base::scoped_nsobject<Tab> tabSaver([oldTab retain]); |
| [newTab fetchFavicon]; |
| [_tabs replaceObjectAtIndex:index withObject:newTab]; |
| [newTab setParentTabModel:self]; |
| |
| [_observers tabModel:self didReplaceTab:oldTab withTab:newTab atIndex:index]; |
| |
| if (self.currentTab == oldTab) |
| [self changeSelectedTabFrom:nil to:newTab persistState:NO]; |
| |
| [oldTab setParentTabModel:nil]; |
| if (!keepOldTabOpen) |
| [oldTab close]; |
| |
| // Record a tab clobber, since swapping tabs bypasses the tab code that would |
| // normally log clobbers. |
| base::RecordAction(base::UserMetricsAction("MobileTabClobbered")); |
| } |
| |
| - (void)closeTabAtIndex:(NSUInteger)index { |
| DCHECK(index < [_tabs count]); |
| [self closeTab:[_tabs objectAtIndex:index]]; |
| } |
| |
| - (void)closeTab:(Tab*)tab { |
| // Ensure the tab stays alive long enough for us to send out the |
| // notice of its destruction to the delegate. |
| [_observers tabModel:self willRemoveTab:tab]; |
| [tab close]; // Note it is not safe to access the tab after 'close'. |
| } |
| |
| - (void)closeAllTabs { |
| // If this changes, _closedTabCount metrics need to be adjusted. |
| for (NSInteger i = self.count - 1; i >= 0; --i) |
| [self closeTabAtIndex:i]; |
| [[NSNotificationCenter defaultCenter] |
| postNotificationName:kTabModelAllTabsDidCloseNotification |
| object:self]; |
| } |
| |
| - (void)haltAllTabs { |
| for (Tab* tab in _tabs.get()) { |
| [tab terminateNetworkActivity]; |
| } |
| } |
| |
| - (void)notifyTabChanged:(Tab*)tab { |
| [_observers tabModel:self didChangeTab:tab]; |
| } |
| |
| - (void)addObserver:(id<TabModelObserver>)observer { |
| [_observers addObserver:observer]; |
| } |
| |
| - (void)removeObserver:(id<TabModelObserver>)observer { |
| [_observers removeObserver:observer]; |
| } |
| |
| - (void)resetSessionMetrics { |
| _closedTabCount = 0; |
| _openedTabCount = 0; |
| _newTabCount = 0; |
| } |
| |
| - (void)recordSessionMetrics { |
| UMA_HISTOGRAM_CUSTOM_COUNTS("Session.ClosedTabCounts", _closedTabCount, 1, |
| 200, 50); |
| UMA_HISTOGRAM_CUSTOM_COUNTS("Session.OpenedTabCounts", _openedTabCount, 1, |
| 200, 50); |
| UMA_HISTOGRAM_CUSTOM_COUNTS("Session.NewTabCounts", _newTabCount, 1, 200, 50); |
| } |
| |
| - (void)notifyTabSnapshotChanged:(Tab*)tab withImage:(UIImage*)image { |
| DCHECK([NSThread isMainThread]); |
| [_observers tabModel:self didChangeTabSnapshot:tab withImage:image]; |
| } |
| |
| - (void)resetAllWebViews { |
| for (Tab* tab in _tabs.get()) { |
| [tab.webController reinitializeWebViewAndReload:(tab == _currentTab)]; |
| } |
| } |
| |
| - (void)setWebUsageEnabled:(BOOL)webUsageEnabled { |
| if (webUsageEnabled_ == webUsageEnabled) |
| return; |
| webUsageEnabled_ = webUsageEnabled; |
| for (Tab* tab in _tabs.get()) { |
| tab.webUsageEnabled = webUsageEnabled; |
| } |
| } |
| |
| - (void)setPrimary:(BOOL)primary { |
| if (_tabUsageRecorder) |
| _tabUsageRecorder->RecordPrimaryTabModelChange(primary, _currentTab); |
| } |
| |
| - (NSSet*)currentlyReferencedExternalFiles { |
| NSMutableSet* referencedFiles = [NSMutableSet set]; |
| if (!_browserState) |
| return referencedFiles; |
| // Check the currently open tabs for external files. |
| for (Tab* tab in _tabs.get()) { |
| if (UrlIsExternalFileReference(tab.url)) { |
| NSString* fileName = base::SysUTF8ToNSString(tab.url.ExtractFileName()); |
| [referencedFiles addObject:fileName]; |
| } |
| } |
| // Do the same for the recently closed tabs. |
| sessions::TabRestoreService* restoreService = |
| IOSChromeTabRestoreServiceFactory::GetForBrowserState(_browserState); |
| DCHECK(restoreService); |
| for (const auto& entry : restoreService->entries()) { |
| sessions::TabRestoreService::Tab* tab = |
| static_cast<sessions::TabRestoreService::Tab*>(entry.get()); |
| int navigationIndex = tab->current_navigation_index; |
| sessions::SerializedNavigationEntry navigation = |
| tab->navigations[navigationIndex]; |
| GURL URL = navigation.virtual_url(); |
| if (UrlIsExternalFileReference(URL)) { |
| NSString* fileName = base::SysUTF8ToNSString(URL.ExtractFileName()); |
| [referencedFiles addObject:fileName]; |
| } |
| } |
| return referencedFiles; |
| } |
| |
| // NOTE: This can be called multiple times, so must be robust against that. |
| - (void)browserStateDestroyed { |
| [[NSNotificationCenter defaultCenter] removeObserver:self]; |
| if (_browserState) { |
| UnregisterTabModelFromChromeBrowserState(_browserState, self); |
| } |
| _browserState = nullptr; |
| } |
| |
| // Called when a tab is closing, but before its CRWWebController is destroyed. |
| // Equivalent to DetachTabContentsAt() in Chrome's TabStripModel. |
| - (void)didCloseTab:(Tab*)closedTab { |
| NSUInteger closedTabIndex = [_tabs indexOfObject:closedTab]; |
| DCHECK(closedTab); |
| DCHECK(closedTabIndex != NSNotFound); |
| // Let the sessions::TabRestoreService know about that new tab. |
| sessions::TabRestoreService* restoreService = |
| _browserState |
| ? IOSChromeTabRestoreServiceFactory::GetForBrowserState(_browserState) |
| : nullptr; |
| web::NavigationManagerImpl* navigationManager = [closedTab navigationManager]; |
| DCHECK(navigationManager); |
| int itemCount = navigationManager->GetItemCount(); |
| if (restoreService && (![self isNTPTab:closedTab] || itemCount > 1)) { |
| restoreService->CreateHistoricalTab( |
| sessions::IOSLiveTab::GetForWebState(closedTab.webStateImpl), |
| static_cast<int>(closedTabIndex)); |
| } |
| // This needs to be called before the tab is removed from the list. |
| Tab* newSelection = |
| [_orderController determineNewSelectedTabFromRemovedTab:closedTab]; |
| base::scoped_nsobject<Tab> kungFuDeathGrip([closedTab retain]); |
| [_tabs removeObject:closedTab]; |
| |
| // If closing the current tab, clear |_currentTab| before sending any |
| // notification. This avoids various parts of the code getting confused |
| // when the current tab isn't in the tab model. |
| Tab* savedCurrentTab = _currentTab; |
| if (closedTab == _currentTab) |
| _currentTab.reset(nil); |
| |
| [_observers tabModel:self didRemoveTab:closedTab atIndex:closedTabIndex]; |
| [_observers tabModelDidChangeTabCount:self]; |
| |
| // Current tab has closed, update the selected tab and swap in its |
| // contents. There is nothing to do if a non-selected tab is closed as |
| // the selection isn't index-based, therefore it hasn't changed. |
| // -changeSelectedTabFrom: will persist the state change, so only do it |
| // if the selection isn't changing. |
| if (closedTab == savedCurrentTab) { |
| [self changeSelectedTabFrom:closedTab to:newSelection persistState:NO]; |
| } else { |
| [self saveSessionImmediately:NO]; |
| } |
| base::RecordAction(base::UserMetricsAction("MobileTabClosed")); |
| ++_closedTabCount; |
| } |
| |
| - (void)navigationCommittedInTab:(Tab*)tab { |
| if (self.offTheRecord) |
| return; |
| if (![tab navigationManager]) |
| return; |
| |
| // See if the navigation was within a page; if so ignore it. |
| web::NavigationItem* previousItem = |
| [tab navigationManager]->GetPreviousItem(); |
| if (previousItem) { |
| GURL previousURL = previousItem->GetURL(); |
| GURL currentURL = [tab navigationManager]->GetVisibleItem()->GetURL(); |
| |
| url::Replacements<char> replacements; |
| replacements.ClearRef(); |
| if (previousURL.ReplaceComponents(replacements) == |
| currentURL.ReplaceComponents(replacements)) { |
| return; |
| } |
| } |
| |
| int tabCount = static_cast<int>(self.count); |
| UMA_HISTOGRAM_CUSTOM_COUNTS("Tabs.TabCountPerLoad", tabCount, 1, 200, 50); |
| } |
| |
| #pragma mark - NSFastEnumeration |
| |
| - (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState*)state |
| objects:(id*)objects |
| count:(NSUInteger)count { |
| return [_tabs countByEnumeratingWithState:state objects:objects count:count]; |
| } |
| |
| #pragma mark - TabUsageRecorderDelegate |
| |
| - (NSUInteger)liveTabsCount { |
| NSUInteger count = 0; |
| NSArray* tabs = _tabs.get(); |
| for (Tab* tab in tabs) { |
| if ([tab.webController isViewAlive]) |
| count++; |
| } |
| return count; |
| } |
| |
| #pragma mark - Private methods |
| |
| - (SessionWindowIOS*)windowForSavingSession { |
| // Background tabs will already have their state preserved, but not the |
| // fg tab. Do it now. |
| [_currentTab recordStateInHistory]; |
| |
| // Build the array of sessions. Copy the session objects as the saving will |
| // be done on a separate thread. |
| // TODO(crbug.com/661986): This could get expensive especially since this |
| // window may never be saved (if another call comes in before the delay). |
| SessionWindowIOS* window = [[[SessionWindowIOS alloc] init] autorelease]; |
| for (Tab* tab in _tabs.get()) { |
| DCHECK(tab.webStateImpl); |
| std::unique_ptr<web::WebStateImpl> webStateCopy( |
| tab.webStateImpl->CopyForSessionWindow()); |
| [window addSession:std::move(webStateCopy)]; |
| } |
| window.selectedIndex = [self indexOfTab:_currentTab]; |
| return window; |
| } |
| |
| - (BOOL)isNTPTab:(Tab*)tab { |
| std::string host = tab.url.host(); |
| return host == kChromeUINewTabHost || host == kChromeUIBookmarksHost; |
| } |
| |
| - (Tab*)insertTabWithLoadParams: |
| (const web::NavigationManager::WebLoadParams&)params |
| windowName:(NSString*)windowName |
| opener:(Tab*)parentTab |
| openedByDOM:(BOOL)openedByDOM |
| atIndex:(NSUInteger)index |
| inBackground:(BOOL)inBackground { |
| DCHECK(_browserState); |
| base::scoped_nsobject<Tab> tab([[Tab alloc] |
| initWithWindowName:windowName |
| opener:parentTab |
| openedByDOM:openedByDOM |
| model:self |
| browserState:_browserState]); |
| [tab webController].webUsageEnabled = webUsageEnabled_; |
| |
| if ((PageTransitionCoreTypeIs(params.transition_type, |
| ui::PAGE_TRANSITION_LINK)) && |
| (index == TabModelConstants::kTabPositionAutomatically)) { |
| DCHECK(!parentTab || [self indexOfTab:parentTab] != NSNotFound); |
| // Assume tabs opened via link clicks are part of the same "task" as their |
| // parent and are grouped together. |
| TabModelOrderConstants::InsertionAdjacency adjacency = |
| inBackground ? TabModelOrderConstants::kAdjacentAfter |
| : TabModelOrderConstants::kAdjacentBefore; |
| index = [_orderController insertionIndexForTab:tab |
| transition:params.transition_type |
| opener:parentTab |
| adjacency:adjacency]; |
| } else { |
| // For all other types, respect what was passed to us, normalizing values |
| // that are too large. |
| if (index >= self.count) |
| index = [_orderController insertionIndexForAppending]; |
| } |
| |
| if (PageTransitionCoreTypeIs(params.transition_type, |
| ui::PAGE_TRANSITION_TYPED) && |
| index == self.count) { |
| // Also, any tab opened at the end of the TabStrip with a "TYPED" |
| // transition inherit group as well. This covers the cases where the user |
| // creates a New Tab (e.g. Ctrl+T, or clicks the New Tab button), or types |
| // in the address bar and presses Alt+Enter. This allows for opening a new |
| // Tab to quickly look up something. When this Tab is closed, the old one |
| // is re-selected, not the next-adjacent. |
| // TODO(crbug.com/661988): Make this work. |
| } |
| |
| [self insertTab:tab atIndex:index]; |
| |
| if (!inBackground && _tabUsageRecorder) |
| _tabUsageRecorder->TabCreatedForSelection(tab); |
| |
| [[tab webController] loadWithParams:params]; |
| // Force the page to start loading even if it's in the background. |
| if (webUsageEnabled_) |
| [[tab webController] triggerPendingLoad]; |
| NSDictionary* userInfo = @{ |
| kTabModelTabKey : tab, |
| kTabModelOpenInBackgroundKey : @(inBackground), |
| }; |
| [[NSNotificationCenter defaultCenter] |
| postNotificationName:kTabModelNewTabWillOpenNotification |
| object:self |
| userInfo:userInfo]; |
| |
| if (!inBackground) |
| [self setCurrentTab:tab]; |
| |
| return tab; |
| } |
| |
| - (void)changeSelectedTabFrom:(Tab*)oldTab |
| to:(Tab*)newTab |
| persistState:(BOOL)persist { |
| if (oldTab) { |
| // Save state, such as scroll position, before switching tabs. |
| if (oldTab != newTab && persist) |
| [oldTab recordStateInHistory]; |
| [self postNotificationName:kTabModelTabDeselectedNotification |
| withTab:oldTab]; |
| } |
| |
| // No Tab to select (e.g. the last Tab has been closed). |
| if ([self indexOfTab:newTab] == NSNotFound) |
| return; |
| |
| _currentTab.reset(newTab); |
| if (newTab) { |
| [_observers tabModel:self |
| didChangeActiveTab:newTab |
| previousTab:oldTab |
| atIndex:[self indexOfTab:newTab]]; |
| [newTab updateLastVisitedTimestamp]; |
| ++_openedTabCount; |
| } |
| BOOL loadingFinished = [newTab.webController loadPhase] == web::PAGE_LOADED; |
| if (loadingFinished) { |
| // Persist the session state. |
| [self saveSessionImmediately:NO]; |
| } |
| } |
| |
| - (void)updateSnapshotCache:(Tab*)tab { |
| NSMutableSet* set = [NSMutableSet set]; |
| NSUInteger index = [self indexOfTab:tab]; |
| if (index > 0) { |
| Tab* previousTab = [self tabAtIndex:(index - 1)]; |
| [set addObject:[previousTab currentSessionID]]; |
| } |
| if (index < self.count - 1) { |
| Tab* nextTab = [self tabAtIndex:(index + 1)]; |
| [set addObject:[nextTab currentSessionID]]; |
| } |
| [SnapshotCache sharedInstance].pinnedIDs = set; |
| } |
| |
| - (void)postNotificationName:(NSString*)notificationName withTab:(Tab*)tab { |
| // A scoped_nsobject is used rather than an NSDictionary with static |
| // initializer dictionaryWithObject, because that approach adds the dictionary |
| // to the autorelease pool, which in turn holds Tab alive longer than |
| // necessary. |
| base::scoped_nsobject<NSDictionary> userInfo( |
| [[NSDictionary alloc] initWithObjectsAndKeys:tab, kTabModelTabKey, nil]); |
| [[NSNotificationCenter defaultCenter] postNotificationName:notificationName |
| object:self |
| userInfo:userInfo]; |
| } |
| |
| #pragma mark - Notification Handlers |
| |
| // Called when UIApplicationWillResignActiveNotification is received. |
| - (void)willResignActive:(NSNotification*)notify { |
| if (webUsageEnabled_ && _currentTab) { |
| [[SnapshotCache sharedInstance] |
| willBeSavedGreyWhenBackgrounding:[_currentTab currentSessionID]]; |
| } |
| } |
| |
| // Called when UIApplicationDidEnterBackgroundNotification is received. |
| - (void)applicationDidEnterBackground:(NSNotification*)notify { |
| if (!_browserState) |
| return; |
| // Evict all the certificate policies except for the current entries of the |
| // active sessions. |
| scoped_refptr<web::CertificatePolicyCache> policy_cache = |
| web::BrowserState::GetCertificatePolicyCache(_browserState); |
| DCHECK(policy_cache); |
| web::WebThread::PostTask( |
| web::WebThread::IO, FROM_HERE, |
| base::Bind(&CleanCertificatePolicyCache, policy_cache, _tabs)); |
| |
| if (_tabUsageRecorder) |
| _tabUsageRecorder->AppDidEnterBackground(); |
| |
| // Normally, the session is saved after some timer expires but since the app |
| // is about to enter the background send YES to save the session immediately. |
| [self saveSessionImmediately:YES]; |
| |
| // Write out a grey version of the current website to disk. |
| if (webUsageEnabled_ && _currentTab) { |
| [[SnapshotCache sharedInstance] |
| saveGreyInBackgroundForSessionID:[_currentTab currentSessionID]]; |
| } |
| } |
| |
| // Called when UIApplicationWillEnterForegroundNotification is received. |
| - (void)applicationWillEnterForeground:(NSNotification*)notify { |
| if (_tabUsageRecorder) { |
| _tabUsageRecorder->AppWillEnterForeground(); |
| } |
| } |
| |
| @end |
| |
| @implementation TabModel (PrivateForTestingOnly) |
| |
| - (Tab*)addTabWithURL:(const GURL&)URL |
| referrer:(const web::Referrer&)referrer |
| windowName:(NSString*)windowName { |
| return [self insertTabWithURL:URL |
| referrer:referrer |
| windowName:windowName |
| opener:nil |
| atIndex:[_orderController insertionIndexForAppending]]; |
| } |
| |
| - (Tab*)insertTabWithURL:(const GURL&)URL |
| referrer:(const web::Referrer&)referrer |
| windowName:(NSString*)windowName |
| opener:(Tab*)parentTab |
| atIndex:(NSUInteger)index { |
| DCHECK(_browserState); |
| base::scoped_nsobject<Tab> tab([[Tab alloc] |
| initWithWindowName:windowName |
| opener:parentTab |
| openedByDOM:NO |
| model:self |
| browserState:_browserState]); |
| web::NavigationManager::WebLoadParams params(URL); |
| params.referrer = referrer; |
| params.transition_type = ui::PAGE_TRANSITION_TYPED; |
| [[tab webController] loadWithParams:params]; |
| [tab webController].webUsageEnabled = webUsageEnabled_; |
| [self insertTab:tab atIndex:index]; |
| return tab; |
| } |
| |
| @end |