Index: ios/chrome/browser/tabs/tab_model.mm |
diff --git a/ios/chrome/browser/tabs/tab_model.mm b/ios/chrome/browser/tabs/tab_model.mm |
new file mode 100644 |
index 0000000000000000000000000000000000000000..cab6b545825a7f585372bba576d3ce02551213c9 |
--- /dev/null |
+++ b/ios/chrome/browser/tabs/tab_model.mm |
@@ -0,0 +1,1099 @@ |
+// 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 "base/supports_user_data.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_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)); |
+} |
+ |
+// Wrapper class to attach a TabModel to a base::SupportsUserData object, such |
+// as an ios::ChromeBrowserState. This wrapper retains the TabModel it wraps, so |
+// any base::SupportsUserData object storing such a wrapper has ownership of the |
+// TabModel. |
+class TabModelHandle : public base::SupportsUserData::Data { |
+ public: |
+ explicit TabModelHandle(TabModel* model) : tab_model_([model retain]) {} |
+ ~TabModelHandle() override {} |
+ TabModel* tab_model() { return tab_model_; } |
+ |
+ private: |
+ base::scoped_nsobject<TabModel> tab_model_; |
+}; |
+ |
+// Key for storing a TabModelHandle in a ChromeBrowserState. |
+const char kTabModelKeyName[] = "tab_model"; |
+ |
+} // 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)tabModelForBrowserState:(ios::ChromeBrowserState*)browserState { |
+ if (!browserState) |
+ return nil; |
+ TabModelHandle* handle = |
+ static_cast<TabModelHandle*>(browserState->GetUserData(kTabModelKeyName)); |
+ return handle ? handle->tab_model() : nil; |
+} |
+ |
+- (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]; |
+ |
+ // Store pointer to |self| in |_browserState|. |
+ _browserState->SetUserData(kTabModelKeyName, new TabModelHandle(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) { |
+ _browserState->RemoveUserData(kTabModelKeyName); |
+ } |
+ _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 |