blob: aa4cd9a93b6ded6d85a11f2ac00b0feecfecdb49 [file] [log] [blame]
// 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