[go: nahoru, domu]

blob: 5766450f505c04d06ee0424b724419b8b6685b83 [file] [log] [blame]
// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "ios/chrome/browser/ui/recent_tabs/recent_tabs_table_view_controller.h"
#import <objc/runtime.h>
#import "base/apple/foundation_util.h"
#import "base/check_op.h"
#import "base/feature_list.h"
#import "base/metrics/histogram_functions.h"
#import "base/metrics/histogram_macros.h"
#import "base/metrics/user_metrics.h"
#import "base/metrics/user_metrics_action.h"
#import "base/notreached.h"
#import "base/numerics/safe_conversions.h"
#import "base/strings/sys_string_conversions.h"
#import "base/time/time.h"
#import "components/prefs/pref_service.h"
#import "components/search_engines/template_url_service.h"
#import "components/sessions/core/session_id.h"
#import "components/sessions/core/tab_restore_service.h"
#import "components/strings/grit/components_strings.h"
#import "components/sync/base/features.h"
#import "components/sync/base/user_selectable_type.h"
#import "components/sync/service/sync_service.h"
#import "components/sync/service/sync_user_settings.h"
#import "components/sync_sessions/open_tabs_ui_delegate.h"
#import "components/sync_sessions/session_sync_service.h"
#import "ios/chrome/app/tests_hook.h"
#import "ios/chrome/browser/drag_and_drop/drag_item_util.h"
#import "ios/chrome/browser/drag_and_drop/table_view_url_drag_drop_handler.h"
#import "ios/chrome/browser/metrics/new_tab_page_uma.h"
#import "ios/chrome/browser/net/crurl.h"
#import "ios/chrome/browser/search_engines/template_url_service_factory.h"
#import "ios/chrome/browser/sessions/live_tab_context_browser_agent.h"
#import "ios/chrome/browser/sessions/session_util.h"
#import "ios/chrome/browser/settings/sync/utils/sync_presenter.h"
#import "ios/chrome/browser/settings/sync/utils/sync_util.h"
#import "ios/chrome/browser/shared/model/browser/browser.h"
#import "ios/chrome/browser/shared/model/browser_state/chrome_browser_state.h"
#import "ios/chrome/browser/shared/model/web_state_list/web_state_list.h"
#import "ios/chrome/browser/shared/model/web_state_list/web_state_opener.h"
#import "ios/chrome/browser/shared/public/commands/application_commands.h"
#import "ios/chrome/browser/shared/public/commands/open_new_tab_command.h"
#import "ios/chrome/browser/shared/public/commands/show_signin_command.h"
#import "ios/chrome/browser/shared/public/features/features.h"
#import "ios/chrome/browser/shared/ui/symbols/symbols.h"
#import "ios/chrome/browser/shared/ui/table_view/cells/table_view_activity_indicator_header_footer_item.h"
#import "ios/chrome/browser/shared/ui/table_view/cells/table_view_disclosure_header_footer_item.h"
#import "ios/chrome/browser/shared/ui/table_view/cells/table_view_illustrated_item.h"
#import "ios/chrome/browser/shared/ui/table_view/cells/table_view_image_item.h"
#import "ios/chrome/browser/shared/ui/table_view/cells/table_view_tabs_search_suggested_history_item.h"
#import "ios/chrome/browser/shared/ui/table_view/cells/table_view_text_button_item.h"
#import "ios/chrome/browser/shared/ui/table_view/cells/table_view_text_header_footer_item.h"
#import "ios/chrome/browser/shared/ui/table_view/cells/table_view_text_item.h"
#import "ios/chrome/browser/shared/ui/table_view/cells/table_view_url_item.h"
#import "ios/chrome/browser/shared/ui/table_view/chrome_table_view_styler.h"
#import "ios/chrome/browser/shared/ui/table_view/table_view_favicon_data_source.h"
#import "ios/chrome/browser/shared/ui/table_view/table_view_utils.h"
#import "ios/chrome/browser/shared/ui/util/uikit_ui_util.h"
#import "ios/chrome/browser/signin/authentication_service.h"
#import "ios/chrome/browser/signin/authentication_service_factory.h"
#import "ios/chrome/browser/signin/chrome_account_manager_service_factory.h"
#import "ios/chrome/browser/sync/enterprise_utils.h"
#import "ios/chrome/browser/sync/session_sync_service_factory.h"
#import "ios/chrome/browser/sync/sync_observer_bridge.h"
#import "ios/chrome/browser/sync/sync_service_factory.h"
#import "ios/chrome/browser/synced_sessions/distant_session.h"
#import "ios/chrome/browser/synced_sessions/distant_tab.h"
#import "ios/chrome/browser/synced_sessions/synced_sessions.h"
#import "ios/chrome/browser/tabs_search/tabs_search_service.h"
#import "ios/chrome/browser/tabs_search/tabs_search_service_factory.h"
#import "ios/chrome/browser/ui/authentication/cells/signin_promo_view_configurator.h"
#import "ios/chrome/browser/ui/authentication/cells/signin_promo_view_consumer.h"
#import "ios/chrome/browser/ui/authentication/cells/table_view_signin_promo_item.h"
#import "ios/chrome/browser/ui/authentication/enterprise/enterprise_utils.h"
#import "ios/chrome/browser/ui/authentication/signin/signin_utils.h"
#import "ios/chrome/browser/ui/authentication/signin_presenter.h"
#import "ios/chrome/browser/ui/authentication/signin_promo_view_mediator.h"
#import "ios/chrome/browser/ui/keyboard/UIKeyCommand+Chrome.h"
#import "ios/chrome/browser/ui/recent_tabs/recent_tabs_constants.h"
#import "ios/chrome/browser/ui/recent_tabs/recent_tabs_menu_provider.h"
#import "ios/chrome/browser/ui/recent_tabs/recent_tabs_presentation_delegate.h"
#import "ios/chrome/browser/ui/recent_tabs/recent_tabs_table_view_controller_delegate.h"
#import "ios/chrome/browser/ui/recent_tabs/recent_tabs_table_view_controller_ui_delegate.h"
#import "ios/chrome/browser/url_loading/url_loading_browser_agent.h"
#import "ios/chrome/browser/url_loading/url_loading_params.h"
#import "ios/chrome/browser/url_loading/url_loading_util.h"
#import "ios/chrome/common/ui/colors/semantic_color_names.h"
#import "ios/chrome/common/ui/favicon/favicon_attributes.h"
#import "ios/chrome/common/ui/favicon/favicon_view.h"
#import "ios/chrome/common/ui/table_view/table_view_cells_constants.h"
#import "ios/chrome/grit/ios_chromium_strings.h"
#import "ios/chrome/grit/ios_strings.h"
#import "ios/public/provider/chrome/browser/modals/modals_api.h"
#import "ios/web/public/web_state.h"
#import "ui/base/l10n/l10n_util.h"
#import "ui/base/l10n/time_format.h"
namespace {
typedef NS_ENUM(NSInteger, SectionIdentifier) {
SectionIdentifierRecentlyClosedTabs = kSectionIdentifierEnumZero,
SectionIdentifierOtherDevices,
SectionIdentifierSuggestedActions,
// The first SessionsSectionIdentifier index.
kFirstSessionSectionIdentifier,
};
typedef NS_ENUM(NSInteger, ItemType) {
ItemTypeRecentlyClosedHeader = kItemTypeEnumZero,
ItemTypeRecentlyClosed,
ItemTypeOtherDevicesHeader,
ItemTypeOtherDevicesSyncOff,
ItemTypeOtherDevicesNoSessions,
ItemTypeOtherDevicesSignedOut,
ItemTypeOtherDevicesSigninPromo,
ItemTypeOtherDevicesSyncInProgressHeader,
ItemTypeSessionHeader,
ItemTypeSessionTabData,
ItemTypeShowFullHistory,
ItemTypeSigninDisabled,
ItemTypeSuggestedActionsHeader,
ItemTypeSuggestedActionSearchOpenTabs,
ItemTypeSuggestedActionSearchWeb,
ItemTypeSuggestedActionSearchHistory,
};
// Key for saving whether the Other Device section is collapsed.
NSString* const kOtherDeviceCollapsedKey = @"OtherDevicesCollapsed";
// Key for saving whether the Recently Closed section is collapsed.
NSString* const kRecentlyClosedCollapsedKey = @"RecentlyClosedCollapsed";
// Estimated Table Row height.
const CGFloat kEstimatedRowHeight = 56;
// Separation space between sections.
const CGFloat kSeparationSpaceBetweenSections = 9;
// Section index for recently closed tabs.
const int kRecentlyClosedTabsSectionIndex = 0;
// A pair representing a single recently closed item. The `TableViewURLItem` is
// used to display the item and the `SessionID` is used to restore the item if
// selected by the user.
typedef std::pair<SessionID, TableViewURLItem*> RecentlyClosedTableViewItemPair;
} // namespace
@interface ListModelCollapsedSceneSessionMediator : ListModelCollapsedMediator
// Creates a collapsed section mediator that stores data in the session's
// userInfo instead of NSUserDefaults, which allows different states per window.
- (instancetype)initWithSession:(UISceneSession*)session;
@end
@interface RecentTabsTableViewController () <SigninPromoViewConsumer,
SigninPresenter,
SyncObserverModelBridge,
SyncPresenter,
TableViewURLDragDataSource,
UIContextMenuInteractionDelegate,
UIGestureRecognizerDelegate> {
// The displayed recently closed tabs.
std::vector<RecentlyClosedTableViewItemPair> _recentlyClosedItems;
// The instance which owns the DistantTabs to display.
std::unique_ptr<synced_sessions::SyncedSessions> _syncedSessions;
// The displayed sessions and tabs. The sessions and tabs are owned by
// `_syncedSessions`, but `_displayedTabs` allows for filtering to display
// only particular tabs.
std::vector<synced_sessions::DistantTabsSet> _displayedTabs;
std::unique_ptr<SyncObserverBridge> _syncObserver;
}
// The service that manages the recently closed tabs
@property(nonatomic, assign) sessions::TabRestoreService* tabRestoreService;
// The sync state.
@property(nonatomic, assign) SessionsSyncUserState sessionState;
@property(nonatomic, strong) SigninPromoViewMediator* signinPromoViewMediator;
// The browser state used for many operations, derived from the one provided by
// `self.browser`.
@property(nonatomic, readonly) ChromeBrowserState* browserState;
// YES if this ViewController is being presented on incognito mode.
@property(nonatomic, readonly, getter=isIncognito) BOOL incognito;
// Convenience getter for `self.browser`'s WebStateList
@property(nonatomic, readonly) WebStateList* webStateList;
// Handler for URL drag interactions.
@property(nonatomic, strong) TableViewURLDragDropHandler* dragDropHandler;
@end
@implementation RecentTabsTableViewController : ChromeTableViewController
#pragma mark - Public Interface
- (instancetype)init {
UITableViewStyle style = ChromeTableViewStyle();
self = [super initWithStyle:style];
if (self) {
_sessionState = SessionsSyncUserState::USER_SIGNED_OUT;
_syncedSessions.reset(new synced_sessions::SyncedSessions());
_restoredTabDisposition = WindowOpenDisposition::CURRENT_TAB;
_preventUpdates = YES;
}
return self;
}
- (void)viewDidLoad {
[super viewDidLoad];
self.view.accessibilityIdentifier =
kRecentTabsTableViewControllerAccessibilityIdentifier;
[self.tableView setDelegate:self];
self.tableView.cellLayoutMarginsFollowReadableWidth = NO;
self.tableView.estimatedRowHeight = kEstimatedRowHeight;
self.tableView.estimatedSectionHeaderHeight = kEstimatedRowHeight;
self.tableView.rowHeight = UITableViewAutomaticDimension;
self.tableView.sectionFooterHeight = 0.0;
self.title = l10n_util::GetNSString(IDS_IOS_CONTENT_SUGGESTIONS_RECENT_TABS);
self.dragDropHandler = [[TableViewURLDragDropHandler alloc] init];
self.dragDropHandler.origin = WindowActivityRecentTabsOrigin;
self.dragDropHandler.dragDataSource = self;
self.tableView.dragDelegate = self.dragDropHandler;
self.tableView.dragInteractionEnabled = true;
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
if (!self.preventUpdates) {
// The table view might get stale while hidden, so we need to forcibly
// refresh it here.
[self loadModel];
[self.tableView reloadData];
}
}
#pragma mark - Setters & Getters
- (void)setBrowser:(Browser*)browser {
_browser = browser;
if (browser) {
ChromeBrowserState* browserState = browser->GetBrowserState();
// Some RecentTabs services depend on objects not present in the
// OffTheRecord BrowserState, in order to prevent crashes set
// `_browserState` to `browserState->OriginalChromeBrowserState`. While
// doing this check if incognito or not so that pages are loaded
// accordingly.
_browserState = browserState->GetOriginalChromeBrowserState();
_incognito = browserState->IsOffTheRecord();
_syncObserver.reset(new SyncObserverBridge(self, self.syncService));
} else {
_syncObserver.reset();
}
}
- (WebStateList*)webStateList {
return self.browser->GetWebStateList();
}
- (void)setPreventUpdates:(BOOL)preventUpdates {
if (_preventUpdates == preventUpdates)
return;
_preventUpdates = preventUpdates;
if (preventUpdates)
return;
[self loadModel];
[self.tableView reloadData];
}
- (syncer::SyncService*)syncService {
DCHECK(_browserState);
return SyncServiceFactory::GetForBrowserState(_browserState);
}
// Returns YES if the user cannot turn on sync for enterprise policy reasons.
- (BOOL)isSyncDisabledByAdministrator {
DCHECK(self.syncService);
if (self.syncService->HasDisableReason(
syncer::SyncService::DISABLE_REASON_ENTERPRISE_POLICY)) {
// Return YES if the SyncDisabled policy is enabled.
return YES;
}
if (self.syncService->GetUserSettings()->IsTypeManagedByPolicy(
syncer::UserSelectableType::kTabs)) {
// Return YES if the data type is disabled by the SyncTypesListDisabled
// policy.
return YES;
}
DCHECK(self.browserState);
AuthenticationService* authService =
AuthenticationServiceFactory::GetForBrowserState(self.browserState);
DCHECK(authService);
// Return NO is sign-in is disabled by the BrowserSignin policy.
return authService->GetServiceStatus() ==
AuthenticationService::ServiceStatus::SigninDisabledByPolicy;
}
#pragma mark - SyncObserverModelBridge
- (void)onSyncStateChanged {
if (self.preventUpdates ||
![self.tableViewModel
hasSectionForSectionIdentifier:SectionIdentifierOtherDevices]) {
return;
}
[self.tableView
performBatchUpdates:^{
if (self.searchTerms.length)
[self updateSessionSections];
else
[self updateOtherDevicesSectionForState:self.sessionState];
}
completion:nil];
}
#pragma mark - TableViewModel
- (void)loadModel {
[super loadModel];
if (self.session) {
// Replace mediator to store collapsed keys in scene session.
self.tableViewModel.collapsableMediator =
[[ListModelCollapsedSceneSessionMediator alloc]
initWithSession:self.session];
}
[self addRecentlyClosedSection];
if (self.sessionState ==
SessionsSyncUserState::USER_SIGNED_IN_SYNC_ON_WITH_SESSIONS) {
[self addSessionSections];
} else {
[self addOtherDevicesSectionForState:self.sessionState];
}
if (self.searchTerms.length) {
[self addSuggestedActionsSection];
}
}
#pragma mark Recently Closed Section
- (void)addRecentlyClosedSection {
// Hide section during search if empty.
if (![self recentlyClosedTabsSectionExists]) {
return;
}
TableViewModel* model = self.tableViewModel;
// Recently Closed Section.
[model insertSectionWithIdentifier:SectionIdentifierRecentlyClosedTabs
atIndex:kRecentlyClosedTabsSectionIndex];
[model setSectionIdentifier:SectionIdentifierRecentlyClosedTabs
collapsedKey:kRecentlyClosedCollapsedKey];
TableViewDisclosureHeaderFooterItem* header =
[[TableViewDisclosureHeaderFooterItem alloc]
initWithType:ItemTypeRecentlyClosedHeader];
header.text = l10n_util::GetNSString(IDS_IOS_RECENT_TABS_RECENTLY_CLOSED);
if (self.tabRestoreService->entries().empty()) {
header.subtitleText =
l10n_util::GetNSString(IDS_IOS_RECENT_TABS_RECENTLY_CLOSED_EMPTY);
}
[model setHeader:header
forSectionWithIdentifier:SectionIdentifierRecentlyClosedTabs];
header.collapsed = [self.tableViewModel
sectionIsCollapsed:SectionIdentifierRecentlyClosedTabs];
// Add Recently Closed Tabs Cells.
[self addRecentlyClosedTabItems];
if (self.searchTerms.length) {
// Hide the show full history item in the recently closed section while
// searching.
return;
}
// Add show full history item last.
TableViewImageItem* historyItem =
[[TableViewImageItem alloc] initWithType:ItemTypeShowFullHistory];
historyItem.title = l10n_util::GetNSString(IDS_HISTORY_SHOWFULLHISTORY_LINK);
historyItem.image =
DefaultSymbolWithPointSize(kHistorySymbol, kSymbolActionPointSize);
historyItem.textColor = [UIColor colorNamed:kBlueColor];
historyItem.accessibilityIdentifier =
kRecentTabsShowFullHistoryCellAccessibilityIdentifier;
[model addItem:historyItem
toSectionWithIdentifier:SectionIdentifierRecentlyClosedTabs];
}
// Iterates through all the TabRestoreService entries and adds items to the
// recently closed tabs section. This method performs no UITableView operations.
- (void)addRecentlyClosedTabItems {
if (!self.tabRestoreService)
return;
if (!self.searchTerms.length) {
// A manual item refresh is necessary when tab search is disabled or when
// there is no search term.
std::vector<RecentlyClosedTableViewItemPair> recentlyClosedItems;
for (auto iter = self.tabRestoreService->entries().begin();
iter != self.tabRestoreService->entries().end(); ++iter) {
const sessions::TabRestoreService::Entry* entry = iter->get();
DCHECK(entry);
// Only TAB type is handled.
// TODO(crbug.com/1056596) : Support WINDOW restoration under
// multi-window.
DCHECK_EQ(sessions::TabRestoreService::TAB, entry->type);
const sessions::TabRestoreService::Tab* tab =
static_cast<const sessions::TabRestoreService::Tab*>(entry);
const sessions::SerializedNavigationEntry& navigationEntry =
tab->navigations[tab->current_navigation_index];
TableViewURLItem* recentlyClosedTab =
[[TableViewURLItem alloc] initWithType:ItemTypeRecentlyClosed];
recentlyClosedTab.title =
base::SysUTF16ToNSString(navigationEntry.title());
recentlyClosedTab.URL =
[[CrURL alloc] initWithGURL:navigationEntry.virtual_url()];
RecentlyClosedTableViewItemPair item(entry->id, recentlyClosedTab);
recentlyClosedItems.push_back(item);
}
_recentlyClosedItems = recentlyClosedItems;
}
for (const RecentlyClosedTableViewItemPair& item : _recentlyClosedItems) {
[self.tableViewModel addItem:item.second
toSectionWithIdentifier:SectionIdentifierRecentlyClosedTabs];
}
}
// Updates the recently closed tabs section by clobbering and reinserting
// section. Needs to be called inside a performBatchUpdates block.
- (void)updateRecentlyClosedSection {
[self.tableViewModel
removeSectionWithIdentifier:SectionIdentifierRecentlyClosedTabs];
[self addRecentlyClosedSection];
NSUInteger index = [self.tableViewModel
sectionForSectionIdentifier:SectionIdentifierRecentlyClosedTabs];
[self.tableView reloadSections:[NSIndexSet indexSetWithIndex:index]
withRowAnimation:UITableViewRowAnimationNone];
}
#pragma mark Sessions Section
// Cleans up the model in order to update the Session sections. Needs to be
// called inside a performBatchUpdates block.
- (void)updateSessionSections {
SessionsSyncUserState previousState = self.sessionState;
if (previousState !=
SessionsSyncUserState::USER_SIGNED_IN_SYNC_ON_WITH_SESSIONS) {
// The previous state was one of the OtherDevices states, remove it.
[self.tableView deleteSections:[self otherDevicesSectionIndexSet]
withRowAnimation:UITableViewRowAnimationNone];
[self.tableViewModel
removeSectionWithIdentifier:SectionIdentifierOtherDevices];
}
// Clean up any previously added SessionSections.
[self removeSessionSections];
// Re-Add the session sections to `self.tableViewModel` and insert them into
// `self.tableView`.
[self addSessionSections];
[self.tableView insertSections:[self sessionSectionIndexSet]
withRowAnimation:UITableViewRowAnimationNone];
}
// Adds all the Remote Sessions sections with its respective items.
- (void)addSessionSections {
TableViewModel* model = self.tableViewModel;
for (NSUInteger i = 0; i < [self numberOfSessions]; i++) {
synced_sessions::DistantSession const* session =
_syncedSessions->GetSessionWithTag(_displayedTabs[i].session_tag);
NSInteger sessionIdentifier = [self sectionIdentifierForSession:session];
[model addSectionWithIdentifier:sessionIdentifier];
NSString* sessionCollapsedKey = base::SysUTF8ToNSString(session->tag);
[model setSectionIdentifier:sessionIdentifier
collapsedKey:sessionCollapsedKey];
TableViewDisclosureHeaderFooterItem* header =
[[TableViewDisclosureHeaderFooterItem alloc]
initWithType:ItemTypeSessionHeader];
header.text = base::SysUTF8ToNSString(session->name);
header.subtitleText = l10n_util::GetNSStringF(
IDS_IOS_OPEN_TABS_LAST_USED,
base::SysNSStringToUTF16([self lastSyncStringForSesssion:session]));
header.collapsed = [model sectionIsCollapsed:sessionIdentifier];
[model setHeader:header forSectionWithIdentifier:sessionIdentifier];
[self addItemsForSession:session];
}
}
- (void)addItemsForSession:(synced_sessions::DistantSession const*)session {
const synced_sessions::DistantTabsSet* session_tabs_set =
[self distantTabsSetForSessionWithTag:session->tag];
if (!session_tabs_set) {
return;
}
NSInteger sectionIdentifier = [self sectionIdentifierForSession:session];
if (session_tabs_set->filtered_tabs) {
// Only add the items from `filtered_tabs`.
for (synced_sessions::DistantTab* sessionTab :
session_tabs_set->filtered_tabs.value()) {
[self addItemForDistantTab:sessionTab
toSectionWithIdentifier:sectionIdentifier];
}
} else {
// When `filtered_tabs` is null, all tabs in the session are included
// in the set.
for (auto&& sessionTab : session->tabs) {
[self addItemForDistantTab:sessionTab.get()
toSectionWithIdentifier:sectionIdentifier];
}
}
}
- (void)addItemForDistantTab:(synced_sessions::DistantTab*)sessionTab
toSectionWithIdentifier:(NSInteger)sectionIdentifier {
NSString* title = base::SysUTF16ToNSString(sessionTab->title);
TableViewURLItem* sessionTabItem =
[[TableViewURLItem alloc] initWithType:ItemTypeSessionTabData];
sessionTabItem.title = title;
sessionTabItem.URL = [[CrURL alloc] initWithGURL:sessionTab->virtual_url];
[self.tableViewModel addItem:sessionTabItem
toSectionWithIdentifier:sectionIdentifier];
}
// Remove all SessionSections from `self.tableViewModel` and `self.tableView`
// Needs to be called inside a performBatchUpdates block.
- (void)removeSessionSections {
NSMutableIndexSet* indexesToBeDeleted = [NSMutableIndexSet indexSet];
NSMutableIndexSet* sectionIdentifiersToBeDeleted =
[NSMutableIndexSet indexSet];
for (NSInteger index = 0; index < [self.tableViewModel numberOfSections];
index++) {
NSInteger sectionIdentifier =
[self.tableViewModel sectionIdentifierForSectionIndex:index];
if (sectionIdentifier >= kFirstSessionSectionIdentifier) {
[sectionIdentifiersToBeDeleted addIndex:sectionIdentifier];
[indexesToBeDeleted addIndex:index];
}
}
[sectionIdentifiersToBeDeleted
enumerateIndexesUsingBlock:^(NSUInteger sectionIdentifier, BOOL* stop) {
[self.tableViewModel removeSectionWithIdentifier:sectionIdentifier];
}];
[self.tableView deleteSections:indexesToBeDeleted
withRowAnimation:UITableViewRowAnimationNone];
}
#pragma mark Other Devices Section
// Cleans up the model in order to update the Other devices section. Needs to be
// called inside a performBatchUpdates block.
- (void)updateOtherDevicesSectionForState:(SessionsSyncUserState)newState {
DCHECK_NE(newState,
SessionsSyncUserState::USER_SIGNED_IN_SYNC_ON_WITH_SESSIONS);
TableViewModel* model = self.tableViewModel;
SessionsSyncUserState previousState = self.sessionState;
if (previousState ==
SessionsSyncUserState::USER_SIGNED_IN_SYNC_ON_WITH_SESSIONS) {
// There were previously one or more session sections, but now they will be
// removed and replaced with a single OtherDevices section.
[self removeSessionSections];
[self addOtherDevicesSectionForState:newState];
// This is a special situation where the tableview operation is an insert
// rather than reload because the section was deleted.
[self.tableView insertSections:[self otherDevicesSectionIndexSet]
withRowAnimation:UITableViewRowAnimationNone];
return;
}
// For all other previous states, the tableview operation is a reload since
// there is already an OtherDevices section that can be updated.
[model removeSectionWithIdentifier:SectionIdentifierOtherDevices];
[self addOtherDevicesSectionForState:newState];
[self.tableView reloadSections:[self otherDevicesSectionIndexSet]
withRowAnimation:UITableViewRowAnimationNone];
}
// Adds Other Devices Section and its header.
- (void)addOtherDevicesSectionForState:(SessionsSyncUserState)state {
AuthenticationService* authService =
AuthenticationServiceFactory::GetForBrowserState(self.browserState);
const AuthenticationService::ServiceStatus authServiceStatus =
authService->GetServiceStatus();
// If sign-in is disabled through user Settings, do not show Other Devices
// section. However, if sign-in is disabled by policy Chrome will
// continue to show the Other Devices section with a specialized message.
switch (authServiceStatus) {
case AuthenticationService::ServiceStatus::SigninDisabledByUser:
case AuthenticationService::ServiceStatus::SigninDisabledByInternal:
return;
case AuthenticationService::ServiceStatus::SigninDisabledByPolicy:
case AuthenticationService::ServiceStatus::SigninForcedByPolicy:
case AuthenticationService::ServiceStatus::SigninAllowed:
break;
}
TableViewModel* model = self.tableViewModel;
[model addSectionWithIdentifier:SectionIdentifierOtherDevices];
[model setSectionIdentifier:SectionIdentifierOtherDevices
collapsedKey:kOtherDeviceCollapsedKey];
// If user is not signed in, show disclosure view section header so that they
// know they can collapse the signin prompt section
if (state == SessionsSyncUserState::USER_SIGNED_IN_SYNC_IN_PROGRESS) {
TableViewActivityIndicatorHeaderFooterItem* header =
[[TableViewActivityIndicatorHeaderFooterItem alloc]
initWithType:ItemTypeOtherDevicesSyncInProgressHeader];
header.text = l10n_util::GetNSString(IDS_IOS_RECENT_TABS_OTHER_DEVICES);
header.subtitleText =
l10n_util::GetNSString(IDS_IOS_RECENT_TABS_SYNC_IN_PROGRESS);
[model setHeader:header
forSectionWithIdentifier:SectionIdentifierOtherDevices];
return;
} else {
TableViewDisclosureHeaderFooterItem* header =
[[TableViewDisclosureHeaderFooterItem alloc]
initWithType:ItemTypeRecentlyClosedHeader];
header.text = l10n_util::GetNSString(IDS_IOS_RECENT_TABS_OTHER_DEVICES);
if (self.isSyncDisabledByAdministrator) {
header.disabled = YES;
header.subtitleText =
l10n_util::GetNSString(IDS_IOS_RECENT_TABS_DISABLED_BY_ORGANIZATION);
}
[model setHeader:header
forSectionWithIdentifier:SectionIdentifierOtherDevices];
header.collapsed =
[self.tableViewModel sectionIsCollapsed:SectionIdentifierOtherDevices];
}
if (!self.isSyncDisabledByAdministrator &&
authServiceStatus !=
AuthenticationService::ServiceStatus::SigninDisabledByPolicy) {
ItemType itemType;
NSString* itemSubtitle;
NSString* itemButtonText;
switch (state) {
case SessionsSyncUserState::USER_SIGNED_IN_SYNC_ON_WITH_SESSIONS:
case SessionsSyncUserState::USER_SIGNED_IN_SYNC_IN_PROGRESS:
NOTREACHED();
return;
case SessionsSyncUserState::USER_SIGNED_IN_SYNC_OFF:
itemType = ItemTypeOtherDevicesSyncOff;
itemSubtitle =
l10n_util::GetNSString(IDS_IOS_RECENT_TABS_OTHER_DEVICES_LABEL);
itemButtonText = l10n_util::GetNSString(
IDS_IOS_RECENT_TABS_OTHER_DEVICES_TURN_ON_TABS);
if (!base::FeatureList::IsEnabled(
syncer::kReplaceSyncPromosWithSignInPromos)) {
itemSubtitle = l10n_util::GetNSString(
IDS_IOS_RECENT_TABS_OTHER_DEVICES_SYNC_IS_OFF_MESSAGE);
itemButtonText = l10n_util::GetNSString(
IDS_IOS_RECENT_TABS_OTHER_DEVICES_TURN_ON_SYNC);
}
break;
case SessionsSyncUserState::USER_SIGNED_IN_SYNC_ON_NO_SESSIONS:
itemType = ItemTypeOtherDevicesNoSessions;
itemSubtitle = l10n_util::GetNSString(
IDS_IOS_RECENT_TABS_OTHER_DEVICES_EMPTY_MESSAGE);
break;
case SessionsSyncUserState::USER_SIGNED_OUT:
[self addSigninPromoViewItem];
itemType = ItemTypeOtherDevicesSignedOut;
itemSubtitle =
base::FeatureList::IsEnabled(
syncer::kReplaceSyncPromosWithSignInPromos)
? l10n_util::GetNSString(
IDS_IOS_RECENT_TABS_OTHER_DEVICES_LABEL)
: l10n_util::GetNSString(
IDS_IOS_RECENT_TABS_OTHER_DEVICES_SIGNED_OUT_MESSAGE);
break;
}
NSString* title =
l10n_util::GetNSString(IDS_IOS_RECENT_TABS_OTHER_DEVICES_EMPTY_TITLE);
NSString* accessibilityId =
kRecentTabsOtherDevicesIllustratedCellAccessibilityIdentifier;
TableViewIllustratedItem* illustratedItem = [self
createIllustratedItemWithType:itemType
image:[UIImage imageNamed:@"recent_tabs_other_"
@"devices_empty"]
title:title
subtitle:itemSubtitle
buttonText:itemButtonText
accessibilityIdentifier:accessibilityId];
[self.tableViewModel insertItem:illustratedItem
inSectionWithIdentifier:SectionIdentifierOtherDevices
atIndex:0];
}
}
- (TableViewIllustratedItem*)createIllustratedItemWithType:(ItemType)type
image:(UIImage*)image
title:(NSString*)title
subtitle:(NSString*)subtitle
buttonText:(NSString*)buttonText
accessibilityIdentifier:
(NSString*)accessibilityIdentifier {
TableViewIllustratedItem* illustratedItem =
[[TableViewIllustratedItem alloc] initWithType:type];
illustratedItem.image = image;
illustratedItem.title = title;
illustratedItem.subtitle = subtitle;
illustratedItem.buttonText = buttonText;
illustratedItem.accessibilityIdentifier = accessibilityIdentifier;
return illustratedItem;
}
- (void)addSigninPromoViewItem {
// Init|_signinPromoViewMediator` if nil.
if (!self.signinPromoViewMediator && self.browserState) {
self.signinPromoViewMediator = [[SigninPromoViewMediator alloc]
initWithAccountManagerService:ChromeAccountManagerServiceFactory::
GetForBrowserState(self.browserState)
authService:AuthenticationServiceFactory::
GetForBrowserState(self.browserState)
prefService:self.browserState->GetPrefs()
syncService:self.syncService
accessPoint:signin_metrics::AccessPoint::
ACCESS_POINT_RECENT_TABS
presenter:self
baseViewController:self];
if (base::FeatureList::IsEnabled(
syncer::kReplaceSyncPromosWithSignInPromos)) {
ChromeAccountManagerService* accountManagerService =
ChromeAccountManagerServiceFactory::GetForBrowserState(
self.browserState);
self.signinPromoViewMediator.signinPromoAction =
accountManagerService->HasIdentities()
? SigninPromoAction::kSigninSheet
: SigninPromoAction::kInstantSignin;
}
self.signinPromoViewMediator.consumer = self;
}
// Configure and add a TableViewSigninPromoItem to the model.
TableViewSigninPromoItem* signinPromoItem = [[TableViewSigninPromoItem alloc]
initWithType:ItemTypeOtherDevicesSigninPromo];
signinPromoItem.text =
l10n_util::GetNSString(IDS_IOS_SIGNIN_PROMO_RECENT_TABS_WITH_UNITY);
signinPromoItem.delegate = self.signinPromoViewMediator;
signinPromoItem.configurator =
[self.signinPromoViewMediator createConfigurator];
[self.tableViewModel addItem:signinPromoItem
toSectionWithIdentifier:SectionIdentifierOtherDevices];
}
#pragma mark Suggested Actions Section
- (void)addSuggestedActionsSection {
TableViewModel* model = self.tableViewModel;
UIColor* actionsTextColor = [UIColor colorNamed:kBlueColor];
[model addSectionWithIdentifier:SectionIdentifierSuggestedActions];
TableViewTextHeaderFooterItem* header = [[TableViewTextHeaderFooterItem alloc]
initWithType:ItemTypeSuggestedActionsHeader];
header.text = l10n_util::GetNSString(IDS_IOS_TABS_SEARCH_SUGGESTED_ACTIONS);
[model setHeader:header
forSectionWithIdentifier:SectionIdentifierSuggestedActions];
TableViewImageItem* searchWebItem = [[TableViewImageItem alloc]
initWithType:ItemTypeSuggestedActionSearchWeb];
searchWebItem.title =
l10n_util::GetNSString(IDS_IOS_TABS_SEARCH_SUGGESTED_ACTION_SEARCH_WEB);
searchWebItem.textColor = actionsTextColor;
searchWebItem.image = [[UIImage imageNamed:@"suggested_action_web"]
imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
[model addItem:searchWebItem
toSectionWithIdentifier:SectionIdentifierSuggestedActions];
if ([self.presentationDelegate
respondsToSelector:@selector(showRegularTabGridFromRecentTabs)]) {
TableViewImageItem* searchOpenTabsItem = [[TableViewImageItem alloc]
initWithType:ItemTypeSuggestedActionSearchOpenTabs];
searchOpenTabsItem.title = l10n_util::GetNSString(
IDS_IOS_TABS_SEARCH_SUGGESTED_ACTION_SEARCH_OPEN_TABS);
searchOpenTabsItem.textColor = actionsTextColor;
searchOpenTabsItem.image =
[[UIImage imageNamed:@"suggested_action_open_tabs"]
imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
[model addItem:searchOpenTabsItem
toSectionWithIdentifier:SectionIdentifierSuggestedActions];
}
TableViewTabsSearchSuggestedHistoryItem* searchHistoryItem =
[[TableViewTabsSearchSuggestedHistoryItem alloc]
initWithType:ItemTypeSuggestedActionSearchHistory];
searchHistoryItem.textColor = actionsTextColor;
[model addItem:searchHistoryItem
toSectionWithIdentifier:SectionIdentifierSuggestedActions];
}
#pragma mark - TableViewModel Helpers
// Ordered array of all section identifiers.
- (NSArray*)allSessionSectionIdentifiers {
NSMutableArray* allSessionSectionIdentifiers = [[NSMutableArray alloc] init];
for (NSUInteger i = 0; i < [self numberOfSessions]; i++) {
[allSessionSectionIdentifiers
addObject:@(i + kFirstSessionSectionIdentifier)];
}
return allSessionSectionIdentifiers;
}
// Returns the TableViewModel SectionIdentifier for `distantSession`. Returns -1
// if `distantSession` doesn't exists.
- (NSInteger)sectionIdentifierForSession:
(synced_sessions::DistantSession const*)distantSession {
for (NSUInteger i = 0; i < [self numberOfSessions]; i++) {
if (_displayedTabs[i].session_tag == distantSession->tag)
return i + kFirstSessionSectionIdentifier;
}
NOTREACHED();
return -1;
}
// Returns an IndexSet containing the Other Devices Section.
- (NSIndexSet*)otherDevicesSectionIndexSet {
NSUInteger otherDevicesSection = [self.tableViewModel
sectionForSectionIdentifier:SectionIdentifierOtherDevices];
return [NSIndexSet indexSetWithIndex:otherDevicesSection];
}
// Returns an IndexSet containing all the Session Sections.
- (NSIndexSet*)sessionSectionIndexSet {
// Create a range of all Session Sections.
NSRange rangeOfSessionSections =
NSMakeRange([self firstSessionSectionIndex], [self numberOfSessions]);
NSIndexSet* sessionSectionIndexes =
[NSIndexSet indexSetWithIndexesInRange:rangeOfSessionSections];
return sessionSectionIndexes;
}
- (NSInteger)firstSessionSectionIndex {
NSInteger firstSessionSectionIndex = 0;
if ([self recentlyClosedTabsSectionExists]) {
firstSessionSectionIndex++;
}
return firstSessionSectionIndex;
}
#pragma mark - Public
- (synced_sessions::DistantSession const*)sessionForTableSectionWithIdentifier:
(NSInteger)sectionIdentifer {
NSInteger section =
[self.tableViewModel sectionForSectionIdentifier:sectionIdentifer];
DCHECK([self isSessionSectionIdentifier:sectionIdentifer]);
const synced_sessions::DistantTabsSet& tabsSet =
_displayedTabs[section - [self firstSessionSectionIndex]];
return _syncedSessions->GetSessionWithTag(tabsSet.session_tag);
}
- (void)removeSessionAtTableSectionWithIdentifier:(NSInteger)sectionIdentifier {
DCHECK([self isSessionSectionIdentifier:sectionIdentifier]);
// Save the sessionTag before removing it from the table. It will be needed to
// delete the session later.
synced_sessions::DistantSession const* session =
[self sessionForTableSectionWithIdentifier:sectionIdentifier];
std::string sessionTag = session->tag;
// Remove the section and, on completion, the delete the session.
__weak __typeof(self) weakSelf = self;
[self.tableView
performBatchUpdates:^{
[weakSelf removeSection:sectionIdentifier forSessionWithTag:sessionTag];
}
completion:^(BOOL) {
[weakSelf deleteSession:sessionTag];
}];
}
- (void)setSearchTerms:(NSString*)searchTerms {
if (_searchTerms == searchTerms ||
// No need for an update if transitioning between nil and empty string.
// (Length of both `_searchTerms` and `searchTerms` will be zero.)
(!_searchTerms.length && !searchTerms.length)) {
return;
}
_searchTerms = searchTerms;
if (self.preventUpdates)
return;
TabsSearchService* search_service =
TabsSearchServiceFactory::GetForBrowserState(self.browserState);
__weak RecentTabsTableViewController* weakSelf = self;
const std::u16string& search_terms =
base::SysNSStringToUTF16(self.searchTerms);
search_service->SearchRecentlyClosed(
search_terms,
base::BindOnce(^(
std::vector<TabsSearchService::RecentlyClosedItemPair> results) {
RecentTabsTableViewController* strongSelf = weakSelf;
if (!strongSelf) {
return;
}
std::vector<RecentlyClosedTableViewItemPair> matchedItems;
for (TabsSearchService::RecentlyClosedItemPair item_pair : results) {
const sessions::SerializedNavigationEntry& navigationEntry =
item_pair.second;
TableViewURLItem* recentlyClosedTab =
[[TableViewURLItem alloc] initWithType:ItemTypeRecentlyClosed];
recentlyClosedTab.title =
base::SysUTF16ToNSString(navigationEntry.title());
recentlyClosedTab.URL =
[[CrURL alloc] initWithGURL:navigationEntry.virtual_url()];
RecentlyClosedTableViewItemPair item(item_pair.first,
recentlyClosedTab);
matchedItems.push_back(item);
}
[strongSelf setRecentlyClosedItems:matchedItems];
}));
search_service->SearchRemoteTabs(
search_terms,
base::BindOnce(^(
std::unique_ptr<synced_sessions::SyncedSessions> synced_sessions,
std::vector<synced_sessions::DistantTabsSet> matching_distant_tabs) {
[weakSelf setSyncedSessions:std::move(synced_sessions)
distantSessionTabs:matching_distant_tabs];
}));
[self loadModel];
[self.tableView reloadData];
}
// Helper to set the distant tabs to be displayed. The tabs referenced in
// `displayedTabs` must be owned by `syncedSessions`.
- (void)setSyncedSessions:
(std::unique_ptr<synced_sessions::SyncedSessions>)syncedSessions
distantSessionTabs:
(std::vector<synced_sessions::DistantTabsSet>)displayedTabs {
_syncedSessions = std::move(syncedSessions);
_displayedTabs = displayedTabs;
}
// Helper to set the recently closed items vector.
- (void)setRecentlyClosedItems:
(std::vector<RecentlyClosedTableViewItemPair>)recentlyClosedItems {
_recentlyClosedItems = recentlyClosedItems;
}
// Helper for removeSessionAtTableSectionWithIdentifier
- (void)removeSection:(NSInteger)sectionIdentifier
forSessionWithTag:(std::string)sessionTag {
NSInteger sectionIndex =
[self.tableViewModel sectionForSectionIdentifier:sectionIdentifier];
[self.tableViewModel removeSectionWithIdentifier:sectionIdentifier];
for (NSUInteger i = 0; i < _displayedTabs.size(); i++) {
if (sessionTag == _displayedTabs[i].session_tag) {
_displayedTabs.erase(_displayedTabs.begin() + i);
break;
}
}
_syncedSessions->EraseSessionWithTag(sessionTag);
[self.tableView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex]
withRowAnimation:UITableViewRowAnimationLeft];
}
// Helper for removeSessionAtTableSectionWithIdentifier
- (void)deleteSession:(std::string)sessionTag {
SessionSyncServiceFactory::GetForBrowserState(self.browserState)
->GetOpenTabsUIDelegate()
->DeleteForeignSession(sessionTag);
}
#pragma mark - Private
// Returns YES if `sectionIdentifier` is a Sessions sectionIdentifier.
- (BOOL)isSessionSectionIdentifier:(NSInteger)sectionIdentifier {
NSArray* sessionSectionIdentifiers = [self allSessionSectionIdentifiers];
NSNumber* sectionIdentifierObject = @(sectionIdentifier);
return [sessionSectionIdentifiers containsObject:sectionIdentifierObject];
}
// Returns YES if the recent tabs is presented modally.
- (BOOL)isPresentedModally {
return self.navigationController.presentingViewController;
}
#pragma mark - Consumer Protocol
- (void)refreshUserState:(SessionsSyncUserState)newSessionState {
if ((newSessionState == self.sessionState &&
self.sessionState !=
SessionsSyncUserState::USER_SIGNED_IN_SYNC_ON_WITH_SESSIONS) ||
self.signinPromoViewMediator.showSpinner) {
// No need to refresh the sections since all states other than
// USER_SIGNED_IN_SYNC_ON_WITH_SESSIONS only have static content. This means
// that if the previous State is the same as the new one the static content
// won't change.
return;
}
if (!self.searchTerms.length) {
// A manual item refresh is necessary when tab search is disabled or there
// is no search term.
sync_sessions::SessionSyncService* syncService =
SessionSyncServiceFactory::GetForBrowserState(self.browserState);
auto syncedSessions =
std::make_unique<synced_sessions::SyncedSessions>(syncService);
std::vector<synced_sessions::DistantTabsSet> displayedTabs;
for (size_t s = 0; s < syncedSessions->GetSessionCount(); s++) {
const synced_sessions::DistantSession* session =
syncedSessions->GetSession(s);
synced_sessions::DistantTabsSet distant_tabs;
distant_tabs.session_tag = session->tag;
displayedTabs.push_back(distant_tabs);
}
// Reset `_displayedTabs` to contain all sessions and tabs.
[self setSyncedSessions:std::move(syncedSessions)
distantSessionTabs:displayedTabs];
}
if (!self.preventUpdates && !self.searchTerms.length) {
// Update the TableView and TableViewModel sections to match the new
// sessionState.
// Turn Off animations since UITableViewRowAnimationNone still animates.
BOOL animationsWereEnabled = [UIView areAnimationsEnabled];
[UIView setAnimationsEnabled:NO];
if (newSessionState ==
SessionsSyncUserState::USER_SIGNED_IN_SYNC_ON_WITH_SESSIONS) {
[self.tableView performBatchUpdates:^{
[self updateSessionSections];
}
completion:nil];
} else {
[self.tableView performBatchUpdates:^{
[self updateOtherDevicesSectionForState:newSessionState];
}
completion:nil];
}
[UIView setAnimationsEnabled:animationsWereEnabled];
}
// Table updates must happen before `sessionState` gets updated, since some
// table updates rely on knowing the previous state.
self.sessionState = newSessionState;
if (self.sessionState != SessionsSyncUserState::USER_SIGNED_OUT) {
[self.signinPromoViewMediator disconnect];
self.signinPromoViewMediator = nil;
}
}
- (void)refreshRecentlyClosedTabs {
if (self.preventUpdates)
return;
// Do not try to reload section if it doesn't exist.
if (![self recentlyClosedTabsSectionExists]) {
return;
}
[self.tableView performBatchUpdates:^{
[self updateRecentlyClosedSection];
}
completion:nil];
}
- (void)setTabRestoreService:(sessions::TabRestoreService*)tabRestoreService {
_tabRestoreService = tabRestoreService;
}
- (void)dismissModals {
[self.signinPromoViewMediator disconnect];
self.signinPromoViewMediator = nil;
ios::provider::DismissModalsForTableView(self.tableView);
}
#pragma mark - UITableViewDelegate
- (void)tableView:(UITableView*)tableView
didSelectRowAtIndexPath:(NSIndexPath*)indexPath {
DCHECK_EQ(tableView, self.tableView);
[self.tableView deselectRowAtIndexPath:indexPath animated:YES];
NSInteger itemTypeSelected =
[self.tableViewModel itemTypeForIndexPath:indexPath];
switch (itemTypeSelected) {
case ItemTypeRecentlyClosed:
[self openTabWithTabRestoreEntryId:
[self tabRestoreEntryIdAtIndexPath:indexPath]];
break;
case ItemTypeSessionTabData:
[self
openTabWithContentOfDistantTab:[self
distantTabAtIndexPath:indexPath]];
break;
case ItemTypeShowFullHistory:
base::RecordAction(
base::UserMetricsAction("MobileRecentTabManagerShowFullHistory"));
[tableView deselectRowAtIndexPath:indexPath animated:NO];
// Tapping "show full history" attempts to dismiss recent tabs to show the
// history UI. It is reasonable to ignore this if a modal UI is already
// showing above recent tabs. This can happen when a user simultaneously
// taps "show full history" and "enable sync". The sync settings UI
// appears first and we should not dismiss it to display history.
if (!self.presentedViewController) {
[self.presentationDelegate
showHistoryFromRecentTabsFilteredBySearchTerms:nil];
}
break;
case ItemTypeSuggestedActionSearchHistory:
base::RecordAction(
base::UserMetricsAction("TabsSearch.SuggestedActions.SearchHistory"));
[tableView deselectRowAtIndexPath:indexPath animated:NO];
// Tapping "show full history" attempts to dismiss recent tabs to show the
// history UI. It is reasonable to ignore this if a modal UI is already
// showing above recent tabs. This can happen when a user simultaneously
// taps "show full history" and "enable sync". The sync settings UI
// appears first and we should not dismiss it to display history.
if (!self.presentedViewController) {
[self.presentationDelegate
showHistoryFromRecentTabsFilteredBySearchTerms:self.searchTerms];
}
break;
case ItemTypeSuggestedActionSearchOpenTabs:
base::RecordAction(
base::UserMetricsAction("TabsSearch.SuggestedActions.OpenTabs"));
[tableView deselectRowAtIndexPath:indexPath animated:NO];
// Tapping "show full history" attempts to dismiss recent tabs to show the
// history UI. It is reasonable to ignore this if a modal UI is already
// showing above recent tabs. This can happen when a user simultaneously
// taps "show full history" and "enable sync". The sync settings UI
// appears first and we should not dismiss it to display history.
if (!self.presentedViewController &&
[self.presentationDelegate
respondsToSelector:@selector(showRegularTabGridFromRecentTabs)]) {
[self.presentationDelegate showRegularTabGridFromRecentTabs];
}
break;
case ItemTypeSuggestedActionSearchWeb:
[self openNewTabWithCurrentSearchTerm];
break;
}
}
- (CGFloat)tableView:(UITableView*)tableView
heightForHeaderInSection:(NSInteger)section {
DCHECK_EQ(tableView, self.tableView);
return UITableViewAutomaticDimension;
}
- (CGFloat)tableView:(UITableView*)tableView
heightForFooterInSection:(NSInteger)section {
// If section is collapsed there's no need to add a separation space.
return [self.tableViewModel
sectionIsCollapsed:[self.tableViewModel
sectionIdentifierForSectionIndex:section]]
? 1.0
: kSeparationSpaceBetweenSections;
}
- (void)scrollViewDidScroll:(UIScrollView*)scrollView {
[self.UIDelegate recentTabsScrollViewDidScroll:self];
}
#pragma mark - UITableViewDataSource
- (UITableViewCell*)tableView:(UITableView*)tableView
cellForRowAtIndexPath:(NSIndexPath*)indexPath {
DCHECK_EQ(tableView, self.tableView);
UITableViewCell* cell =
[super tableView:tableView cellForRowAtIndexPath:indexPath];
NSInteger itemTypeSelected =
[self.tableViewModel itemTypeForIndexPath:indexPath];
// If SigninPromo will be shown, `self.signinPromoViewMediator` must know.
if (itemTypeSelected == ItemTypeOtherDevicesSigninPromo) {
[self.signinPromoViewMediator signinPromoViewIsVisible];
TableViewSigninPromoCell* signinPromoCell =
base::apple::ObjCCastStrict<TableViewSigninPromoCell>(cell);
if (base::FeatureList::IsEnabled(
syncer::kReplaceSyncPromosWithSignInPromos)) {
TableViewSigninPromoItem* signinPromoItem =
base::apple::ObjCCastStrict<TableViewSigninPromoItem>(
[self.tableViewModel itemAtIndexPath:indexPath]);
[signinPromoItem.configurator
configureSigninPromoView:signinPromoCell.signinPromoView
withStyle:SigninPromoViewStyleOnlyButton];
} else {
signinPromoCell.signinPromoView.imageView.hidden = YES;
signinPromoCell.signinPromoView.textLabel.hidden = YES;
}
// Disable animations when setting the background color to prevent flash on
// rotation.
[UIView setAnimationsEnabled:NO];
signinPromoCell.backgroundColor = nil;
[UIView setAnimationsEnabled:YES];
}
// Retrieve favicons for closed tabs and remote sessions.
if (itemTypeSelected == ItemTypeRecentlyClosed ||
itemTypeSelected == ItemTypeSessionTabData) {
[self loadFaviconForCell:cell indexPath:indexPath];
}
// ItemTypeOtherDevicesNoSessions and ItemTypeSigninDisabled should not be
// selectable.
if (itemTypeSelected == ItemTypeOtherDevicesNoSessions ||
itemTypeSelected == ItemTypeSigninDisabled) {
cell.selectionStyle = UITableViewCellSelectionStyleNone;
}
// Set button action method for ItemTypeOtherDevicesSyncOff.
if (itemTypeSelected == ItemTypeOtherDevicesSyncOff) {
TableViewIllustratedCell* illustratedCell =
base::apple::ObjCCastStrict<TableViewIllustratedCell>(cell);
[illustratedCell.button addTarget:self
action:@selector(updateSyncState)
forControlEvents:UIControlEventTouchUpInside];
illustratedCell.button.accessibilityIdentifier =
kRecentTabsTabSyncOffButtonAccessibilityIdentifier;
}
// Hide the separator between this cell and the SignIn Promo.
if (itemTypeSelected == ItemTypeOtherDevicesSignedOut) {
cell.separatorInset =
UIEdgeInsetsMake(0, self.tableView.bounds.size.width, 0, 0);
}
// Update the history search result count once available.
if (itemTypeSelected == ItemTypeSuggestedActionSearchHistory) {
TabsSearchService* search_service =
TabsSearchServiceFactory::GetForBrowserState(self.browserState);
__weak TableViewTabsSearchSuggestedHistoryCell* weakCell =
base::apple::ObjCCastStrict<TableViewTabsSearchSuggestedHistoryCell>(
cell);
NSString* currentSearchTerm = self.searchTerms;
weakCell.searchTerm = currentSearchTerm;
const std::u16string& search_terms =
base::SysNSStringToUTF16(currentSearchTerm);
search_service->SearchHistory(
search_terms, base::BindOnce(^(size_t resultCount) {
if ([weakCell.searchTerm isEqualToString:currentSearchTerm]) {
[weakCell updateHistoryResultsCount:resultCount];
}
}));
}
[cell layoutIfNeeded];
return cell;
}
- (UIView*)tableView:(UITableView*)tableView
viewForHeaderInSection:(NSInteger)section {
UIView* header = [super tableView:tableView viewForHeaderInSection:section];
// Set the header tag as the sectionIdentifer in order to recognize which
// header was tapped.
header.tag = [self.tableViewModel sectionIdentifierForSectionIndex:section];
// Remove all existing gestureRecognizers since the header might be reused.
for (UIGestureRecognizer* recognizer in header.gestureRecognizers) {
[header removeGestureRecognizer:recognizer];
}
// Gesture recognizer for long press context menu.
[header
addInteraction:[[UIContextMenuInteraction alloc] initWithDelegate:self]];
// Gesture recognizer for header collapsing/expanding.
UITapGestureRecognizer* tapGesture =
[[UITapGestureRecognizer alloc] initWithTarget:self
action:@selector(handleTap:)];
[header addGestureRecognizer:tapGesture];
return header;
}
- (UIContextMenuConfiguration*)tableView:(UITableView*)tableView
contextMenuConfigurationForRowAtIndexPath:(NSIndexPath*)indexPath
point:(CGPoint)point {
NSInteger itemType = [self.tableViewModel itemTypeForIndexPath:indexPath];
if (itemType != ItemTypeRecentlyClosed && itemType != ItemTypeSessionTabData)
return nil;
TableViewItem* item = [self.tableViewModel itemAtIndexPath:indexPath];
TableViewURLItem* URLItem =
base::apple::ObjCCastStrict<TableViewURLItem>(item);
return [self.menuProvider
contextMenuConfigurationForItem:URLItem
fromView:[tableView
cellForRowAtIndexPath:indexPath]];
}
#pragma mark - UIContextMenuInteractionDelegate
- (UIContextMenuConfiguration*)contextMenuInteraction:
(UIContextMenuInteraction*)interaction
configurationForMenuAtLocation:(CGPoint)location {
UIView* header = [interaction view];
NSInteger tappedHeaderSectionIdentifier = header.tag;
if (![self isSessionSectionIdentifier:tappedHeaderSectionIdentifier])
return [[UIContextMenuConfiguration alloc] init];
return
[self.menuProvider contextMenuConfigurationForHeaderWithSectionIdentifier:
tappedHeaderSectionIdentifier];
}
#pragma mark - TableViewURLDragDataSource
- (URLInfo*)tableView:(UITableView*)tableView
URLInfoAtIndexPath:(NSIndexPath*)indexPath {
NSInteger itemType = [self.tableViewModel itemTypeForIndexPath:indexPath];
switch (itemType) {
case ItemTypeRecentlyClosed:
case ItemTypeSessionTabData: {
TableViewItem* item = [self.tableViewModel itemAtIndexPath:indexPath];
TableViewURLItem* URLItem =
base::apple::ObjCCastStrict<TableViewURLItem>(item);
GURL gurl;
if (URLItem.URL)
gurl = URLItem.URL.gurl;
return [[URLInfo alloc] initWithURL:gurl title:URLItem.title];
}
case ItemTypeRecentlyClosedHeader:
case ItemTypeOtherDevicesHeader:
case ItemTypeOtherDevicesSyncOff:
case ItemTypeOtherDevicesNoSessions:
case ItemTypeOtherDevicesSigninPromo:
case ItemTypeOtherDevicesSyncInProgressHeader:
case ItemTypeSessionHeader:
case ItemTypeShowFullHistory:
case ItemTypeSigninDisabled:
break;
}
return nil;
}
#pragma mark - Recently closed tab helpers
- (BOOL)recentlyClosedTabsSectionExists {
// The recently closed section does not exist if the user is searching and
// there are no matching recently closed items.
if (self.searchTerms.length && [self numberOfRecentlyClosedTabs] == 0) {
return NO;
}
return YES;
}
- (NSInteger)numberOfRecentlyClosedTabs {
if (!self.tabRestoreService)
return 0;
return _recentlyClosedItems.size();
}
- (const SessionID)tabRestoreEntryIdAtIndexPath:(NSIndexPath*)indexPath {
DCHECK_EQ(
[self.tableViewModel sectionIdentifierForSectionIndex:indexPath.section],
SectionIdentifierRecentlyClosedTabs);
NSInteger index = indexPath.row;
DCHECK_LE(index, [self numberOfRecentlyClosedTabs]);
if (!self.tabRestoreService)
return SessionID::InvalidValue();
return _recentlyClosedItems[index].first;
}
// Retrieves favicon from FaviconLoader and sets image in URLCell.
- (void)loadFaviconForCell:(UITableViewCell*)cell
indexPath:(NSIndexPath*)indexPath {
TableViewItem* item = [self.tableViewModel itemAtIndexPath:indexPath];
DCHECK(item);
DCHECK(cell);
TableViewURLItem* URLItem =
base::apple::ObjCCastStrict<TableViewURLItem>(item);
TableViewURLCell* URLCell =
base::apple::ObjCCastStrict<TableViewURLCell>(cell);
NSString* itemIdentifier = URLItem.uniqueIdentifier;
[self.imageDataSource
faviconForPageURL:URLItem.URL
completion:^(FaviconAttributes* attributes) {
// Only set favicon if the cell hasn't been reused.
if ([URLCell.cellUniqueIdentifier
isEqualToString:itemIdentifier]) {
DCHECK(attributes);
[URLCell.faviconView configureWithAttributes:attributes];
}
}];
}
#pragma mark - Distant Sessions helpers
- (NSUInteger)numberOfSessions {
if (!_syncedSessions)
return 0;
return _displayedTabs.size();
}
// Returns the Session Index for a given Session Tab `indexPath`.
- (size_t)indexOfSessionForTabAtIndexPath:(NSIndexPath*)indexPath {
DCHECK_EQ([self.tableViewModel itemTypeForIndexPath:indexPath],
ItemTypeSessionTabData);
// Get the sectionIdentifier for `indexPath`,
NSNumber* sectionIdentifierForIndexPath = @(
[self.tableViewModel sectionIdentifierForSectionIndex:indexPath.section]);
// Get the index of this sectionIdentifier.
size_t indexOfSession = [[self allSessionSectionIdentifiers]
indexOfObject:sectionIdentifierForIndexPath];
DCHECK_LT(indexOfSession, _displayedTabs.size());
return indexOfSession;
}
- (synced_sessions::DistantSession const*)sessionForTabAtIndexPath:
(NSIndexPath*)indexPath {
const synced_sessions::DistantTabsSet& tabs_set =
_displayedTabs[[self indexOfSessionForTabAtIndexPath:indexPath]];
return _syncedSessions->GetSessionWithTag(tabs_set.session_tag);
}
- (synced_sessions::DistantTab const*)distantTabAtIndexPath:
(NSIndexPath*)indexPath {
DCHECK_EQ([self.tableViewModel itemTypeForIndexPath:indexPath],
ItemTypeSessionTabData);
size_t indexOfDistantTab = indexPath.row;
synced_sessions::DistantSession const* session =
[self sessionForTabAtIndexPath:indexPath];
const synced_sessions::DistantTabsSet* tabs_set =
[self distantTabsSetForSessionWithTag:session->tag];
if (tabs_set->filtered_tabs) {
DCHECK_LT(indexOfDistantTab, tabs_set->filtered_tabs->size());
return tabs_set->filtered_tabs.value()[indexOfDistantTab];
}
// If filtered_tabs is null, all tabs in `session` should be used.
DCHECK_LT(indexOfDistantTab, session->tabs.size());
return session->tabs[indexOfDistantTab].get();
}
- (const synced_sessions::DistantTabsSet*)distantTabsSetForSessionWithTag:
(const std::string&)sessionTag {
for (const synced_sessions::DistantTabsSet& tabs_set : _displayedTabs) {
if (sessionTag == tabs_set.session_tag) {
return &tabs_set;
}
}
return nullptr;
}
- (NSString*)lastSyncStringForSesssion:
(synced_sessions::DistantSession const*)session {
base::Time time = session->modified_time;
NSDate* lastUsedDate = [NSDate dateWithTimeIntervalSince1970:time.ToTimeT()];
NSString* dateString =
[NSDateFormatter localizedStringFromDate:lastUsedDate
dateStyle:NSDateFormatterShortStyle
timeStyle:NSDateFormatterNoStyle];
NSString* timeString;
base::TimeDelta last_used_delta;
if (base::Time::Now() > time)
last_used_delta = base::Time::Now() - time;
if (last_used_delta.InMicroseconds() < base::Time::kMicrosecondsPerMinute) {
timeString = l10n_util::GetNSString(IDS_IOS_OPEN_TABS_RECENTLY_SYNCED);
// This will return something similar to "Seconds ago"
return [NSString stringWithFormat:@"%@", timeString];
}
NSDate* date = [NSDate dateWithTimeIntervalSince1970:time.ToTimeT()];
timeString =
[NSDateFormatter localizedStringFromDate:date
dateStyle:NSDateFormatterNoStyle
timeStyle:NSDateFormatterShortStyle];
NSInteger today = [[NSCalendar currentCalendar] component:NSCalendarUnitDay
fromDate:[NSDate date]];
NSInteger dateDay =
[[NSCalendar currentCalendar] component:NSCalendarUnitDay fromDate:date];
if (today == dateDay) {
timeString = base::SysUTF16ToNSString(
ui::TimeFormat::Simple(ui::TimeFormat::FORMAT_ELAPSED,
ui::TimeFormat::LENGTH_SHORT, last_used_delta));
// This will return something similar to "1 min/hour ago"
return [NSString stringWithFormat:@"%@", timeString];
}
if (today - dateDay == 1) {
dateString = l10n_util::GetNSString(IDS_IOS_OPEN_TABS_SYNCED_YESTERDAY);
// This will return something similar to "H:MM Yesterday"
return [NSString stringWithFormat:@"%@ %@", timeString, dateString];
}
// This will return something similar to "H:MM mm/dd/yy"
return [NSString stringWithFormat:@"%@ %@", timeString, dateString];
}
#pragma mark - Navigation helpers
- (void)openTabWithContentOfDistantTab:
(synced_sessions::DistantTab const*)distantTab {
if (!self.browser) {
// Prevent interactions if the browser is nil, for example during dismissal.
return;
}
// Shouldn't reach this if in incognito.
DCHECK(!self.isIncognito);
// It is reasonable to ignore this request if a modal UI is already showing
// above recent tabs. This can happen when a user simultaneously taps a
// distant tab and "enable sync". The sync settings UI appears first and we
// should not dismiss it to show a distant tab.
if (self.presentedViewController)
return;
sync_sessions::OpenTabsUIDelegate* openTabs =
SessionSyncServiceFactory::GetForBrowserState(self.browserState)
->GetOpenTabsUIDelegate();
const sessions::SessionTab* toLoad = nullptr;
if (openTabs->GetForeignTab(distantTab->session_tag, distantTab->tab_id,
&toLoad)) {
base::TimeDelta time_since_last_use = base::Time::Now() - toLoad->timestamp;
base::UmaHistogramCustomTimes("IOS.DistantTab.TimeSinceLastUse",
time_since_last_use, base::Minutes(1),
base::Days(24), 50);
base::RecordAction(base::UserMetricsAction(
"MobileRecentTabManagerTabFromOtherDeviceOpened"));
if (self.searchTerms.length) {
base::RecordAction(base::UserMetricsAction(
"MobileRecentTabManagerTabFromOtherDeviceOpenedSearchResult"));
self.searchTerms = @"";
}
new_tab_page_uma::RecordAction(
self.isIncognito, self.webStateList->GetActiveWebState(),
new_tab_page_uma::ACTION_OPENED_FOREIGN_SESSION);
std::unique_ptr<web::WebState> web_state =
session_util::CreateWebStateWithNavigationEntries(
self.browserState, toLoad->current_navigation_index,
toLoad->navigations);
switch (self.restoredTabDisposition) {
case WindowOpenDisposition::CURRENT_TAB:
self.webStateList->ReplaceWebStateAt(self.webStateList->active_index(),
std::move(web_state));
break;
case WindowOpenDisposition::NEW_FOREGROUND_TAB:
self.webStateList->InsertWebState(
self.webStateList->count(), std::move(web_state),
(WebStateList::INSERT_FORCE_INDEX | WebStateList::INSERT_ACTIVATE),
WebStateOpener());
break;
default:
NOTREACHED();
break;
}
}
[self.presentationDelegate showActiveRegularTabFromRecentTabs];
}
- (void)openTabWithTabRestoreEntryId:(const SessionID)entry_id {
if (!self.browser) {
// Prevent interactions if the browser is nil, for example during dismissal.
return;
}
// It is reasonable to ignore this request if a modal UI is already showing
// above recent tabs. This can happen when a user simultaneously taps a
// recently closed tab and "enable sync". The sync settings UI appears first
// and we should not dismiss it to restore a recently closed tab.
if (self.presentedViewController)
return;
base::RecordAction(
base::UserMetricsAction("MobileRecentTabManagerRecentTabOpened"));
if (self.searchTerms.length) {
base::RecordAction(base::UserMetricsAction(
"MobileRecentTabManagerRecentTabOpenedSearchResult"));
}
new_tab_page_uma::RecordAction(
self.isIncognito, self.webStateList->GetActiveWebState(),
new_tab_page_uma::ACTION_OPENED_RECENTLY_CLOSED_ENTRY);
// If RecentTabs is being displayed from incognito, the resulting tab will
// open in the corresponding normal BVC. Change the disposition to avoid
// clobbering any tabs.
WindowOpenDisposition disposition =
self.isIncognito ? WindowOpenDisposition::NEW_FOREGROUND_TAB
: self.restoredTabDisposition;
RestoreTab(entry_id, disposition, self.browser);
[self.presentationDelegate showActiveRegularTabFromRecentTabs];
}
- (void)openNewTabWithCurrentSearchTerm {
if (!self.browser) {
// Prevent interactions if the browser is nil, for example during dismissal.
return;
}
// It is reasonable to ignore this request if a modal UI is already showing
// above recent tabs. This can happen when a user simultaneously taps a
// recently closed tab and "enable sync". The sync settings UI appears first
// and we should not dismiss it to restore a recently closed tab.
if (self.presentedViewController)
return;
base::RecordAction(
base::UserMetricsAction("TabsSearch.SuggestedActions.SearchOnWeb"));
TemplateURLService* templateURLService =
ios::TemplateURLServiceFactory::GetForBrowserState(self.browserState);
const TemplateURL* defaultURL =
templateURLService->GetDefaultSearchProvider();
DCHECK(defaultURL);
TemplateURLRef::SearchTermsArgs search_args(
base::SysNSStringToUTF16(self.searchTerms));
GURL searchUrl(defaultURL->url_ref().ReplaceSearchTerms(
search_args, templateURLService->search_terms_data()));
web::WebState::CreateParams params(self.browserState);
auto webState = web::WebState::Create(params);
web::WebState* webStatePtr = webState.get();
self.webStateList->InsertWebState(
self.webStateList->count(), std::move(webState),
(WebStateList::INSERT_FORCE_INDEX | WebStateList::INSERT_ACTIVATE),
WebStateOpener());
webStatePtr->OpenURL(web::WebState::OpenURLParams(
searchUrl, web::Referrer(), WindowOpenDisposition::CURRENT_TAB,
ui::PAGE_TRANSITION_GENERATED, /*is_renderer_initiated=*/false));
[self.presentationDelegate showActiveRegularTabFromRecentTabs];
}
#pragma mark - Collapse/Expand sections
- (void)handleTap:(UITapGestureRecognizer*)sender {
UIView* headerTapped = sender.view;
NSInteger tappedHeaderSectionIdentifier = headerTapped.tag;
if (sender.state == UIGestureRecognizerStateEnded) {
NSInteger section = [self.tableViewModel
sectionForSectionIdentifier:tappedHeaderSectionIdentifier];
ListItem* headerItem = [self.tableViewModel headerForSectionIndex:section];
// Suggested actions header is not interactable.
if (headerItem.type == ItemTypeSuggestedActionsHeader) {
return;
}
[self toggleExpansionOfSectionIdentifier:tappedHeaderSectionIdentifier];
UITableViewHeaderFooterView* headerView =
[self.tableView headerViewForSection:section];
// Highlight and collapse the section header being tapped.
// Don't for the Loading Other Devices section header.
if (headerItem.type == ItemTypeRecentlyClosedHeader ||
headerItem.type == ItemTypeSessionHeader) {
TableViewDisclosureHeaderFooterView* disclosureHeaderView =
base::apple::ObjCCastStrict<TableViewDisclosureHeaderFooterView>(
headerView);
TableViewDisclosureHeaderFooterItem* disclosureItem =
base::apple::ObjCCastStrict<TableViewDisclosureHeaderFooterItem>(
headerItem);
BOOL collapsed = [self.tableViewModel
sectionIsCollapsed:[self.tableViewModel
sectionIdentifierForSectionIndex:section]];
DisclosureDirection direction =
collapsed ? DisclosureDirectionTrailing : DisclosureDirectionDown;
[disclosureHeaderView rotateToDirection:direction];
disclosureItem.collapsed = collapsed;
}
}
}
- (void)toggleExpansionOfSectionIdentifier:(NSInteger)sectionIdentifier {
NSMutableArray* cellIndexPathsToDeleteOrInsert = [NSMutableArray array];
NSInteger sectionIndex =
[self.tableViewModel sectionForSectionIdentifier:sectionIdentifier];
NSArray* items =
[self.tableViewModel itemsInSectionWithIdentifier:sectionIdentifier];
for (NSUInteger i = 0; i < [items count]; i++) {
NSIndexPath* tabIndexPath =
[NSIndexPath indexPathForRow:i inSection:sectionIndex];
[cellIndexPathsToDeleteOrInsert addObject:tabIndexPath];
}
// No update required if `cellIndexPathsToDeleteOrInsert` is empty.
// Additionally, calling `performBatchUpdates` if the table view is not
// already displaying the current model state could crash. (crbug.com/1328988)
if ([cellIndexPathsToDeleteOrInsert count] == 0) {
return;
}
void (^tableUpdates)(void) = ^{
if ([self.tableViewModel sectionIsCollapsed:sectionIdentifier]) {
[self.tableViewModel setSection:sectionIdentifier collapsed:NO];
[self.tableView insertRowsAtIndexPaths:cellIndexPathsToDeleteOrInsert
withRowAnimation:UITableViewRowAnimationFade];
} else {
[self.tableViewModel setSection:sectionIdentifier collapsed:YES];
[self.tableView deleteRowsAtIndexPaths:cellIndexPathsToDeleteOrInsert
withRowAnimation:UITableViewRowAnimationFade];
}
};
[self.tableView performBatchUpdates:tableUpdates completion:nil];
}
#pragma mark - SigninPromoViewConsumer
- (void)configureSigninPromoWithConfigurator:
(SigninPromoViewConfigurator*)configurator
identityChanged:(BOOL)identityChanged {
DCHECK(self.signinPromoViewMediator);
if (![self.tableViewModel
hasSectionForSectionIdentifier:SectionIdentifierOtherDevices] ||
![self.tableViewModel hasItemForItemType:ItemTypeOtherDevicesSigninPromo
sectionIdentifier:SectionIdentifierOtherDevices]) {
// Need to remove the sign-in promo view mediator when the section doesn't
// exist anymore. The mediator should not be removed each time the section
// is removed since the section is replaced at each reload.
// Metrics would be recorded too often.
// The other device section can be present even without the sync promo. This
// happens when sync is disabled.
[self.signinPromoViewMediator disconnect];
self.signinPromoViewMediator = nil;
return;
}
if ([self.tableViewModel hasItemForItemType:ItemTypeOtherDevicesSigninPromo
sectionIdentifier:SectionIdentifierOtherDevices]) {
// Update the TableViewSigninPromoItem configurator. It will be used by the
// item to configure the cell once `self.tableView` requests a cell on
// cellForRowAtIndexPath.
NSIndexPath* indexPath = [self.tableViewModel
indexPathForItemType:ItemTypeOtherDevicesSigninPromo
sectionIdentifier:SectionIdentifierOtherDevices];
TableViewItem* item = [self.tableViewModel itemAtIndexPath:indexPath];
TableViewSigninPromoItem* signInItem =
base::apple::ObjCCastStrict<TableViewSigninPromoItem>(item);
signInItem.configurator = configurator;
// If section is collapsed no tableView update is needed.
if ([self.tableViewModel
sectionIsCollapsed:SectionIdentifierOtherDevices]) {
return;
}
// After setting the new configurator to the item, reload the item's Cell.
[self reloadCellsForItems:@[ signInItem ]
withRowAnimation:UITableViewRowAnimationNone];
}
}
- (void)signinDidFinish {
if (base::FeatureList::IsEnabled(
syncer::kReplaceSyncPromosWithSignInPromos)) {
[self.presentationDelegate showHistorySyncOptInAfterDedicatedSignIn:YES];
} else {
[self.delegate refreshSessionsView];
}
}
#pragma mark - SyncPresenter
- (void)showPrimaryAccountReauth {
[self.handler showSignin:[[ShowSigninCommand alloc]
initWithOperation:AuthenticationOperation::
kPrimaryAccountReauth
accessPoint:signin_metrics::AccessPoint::
ACCESS_POINT_RECENT_TABS]
baseViewController:self];
}
- (void)showSyncPassphraseSettings {
[self.handler showSyncPassphraseSettingsFromViewController:self];
}
- (void)showGoogleServicesSettings {
[self.handler showGoogleServicesSettingsFromViewController:self];
}
- (void)showAccountSettings {
[self.handler showAccountsSettingsFromViewController:self];
}
- (void)showTrustedVaultReauthForFetchKeysWithTrigger:
(syncer::TrustedVaultUserActionTriggerForUMA)trigger {
[self.handler showTrustedVaultReauthForFetchKeysFromViewController:self
trigger:trigger];
}
- (void)showTrustedVaultReauthForDegradedRecoverabilityWithTrigger:
(syncer::TrustedVaultUserActionTriggerForUMA)trigger {
[self.handler
showTrustedVaultReauthForDegradedRecoverabilityFromViewController:self
trigger:
trigger];
}
#pragma mark - SigninPresenter
- (void)showSignin:(ShowSigninCommand*)command {
[self.handler showSignin:command baseViewController:self];
}
#pragma mark - UIAdaptivePresentationControllerDelegate
- (void)presentationControllerDidDismiss:
(UIPresentationController*)presentationController {
base::RecordAction(base::UserMetricsAction("IOSRecentTabsCloseWithSwipe"));
[self.presentationDelegate showActiveRegularTabFromRecentTabs];
}
#pragma mark - Accessibility
- (BOOL)accessibilityPerformEscape {
[self.presentationDelegate showActiveRegularTabFromRecentTabs];
return YES;
}
#pragma mark - UIResponder
// To always be able to register key commands via -keyCommands, the VC must be
// able to become first responder.
- (BOOL)canBecomeFirstResponder {
return YES;
}
- (NSArray<UIKeyCommand*>*)keyCommands {
return @[ UIKeyCommand.cr_close ];
}
- (BOOL)canPerformAction:(SEL)action withSender:(id)sender {
if (sel_isEqual(action, @selector(keyCommand_close))) {
return [self isPresentedModally];
}
return [super canPerformAction:action withSender:sender];
}
- (void)keyCommand_close {
base::RecordAction(base::UserMetricsAction("MobileKeyCommandClose"));
[self.presentationDelegate showActiveRegularTabFromRecentTabs];
}
#pragma mark - Private Helpers
- (void)updateSyncState {
syncer::SyncService* const syncService = self.syncService;
if (!syncService) {
return;
}
syncer::SyncService::UserActionableError error =
syncService->GetUserActionableError();
if (error == syncer::SyncService::UserActionableError::kSignInNeedsUpdate) {
[self showPrimaryAccountReauth];
} else if (ShouldShowSyncSettings(error)) {
[self showSyncSettingsOrHistoryOptIn];
} else if (error ==
syncer::SyncService::UserActionableError::kNeedsPassphrase) {
[self showSyncPassphraseSettings];
}
}
// Show the History Sync Opt-In screen if the sync-related UI is disabled, and
// the account is not syncing. Otherwise, show the sync settings screen.
- (void)showSyncSettingsOrHistoryOptIn {
// TODO(crbug.com/1462326): This logic should be moved outside of the
// ViewController.
BOOL isSyncUIDisabled =
base::FeatureList::IsEnabled(syncer::kReplaceSyncPromosWithSignInPromos);
AuthenticationService* authenticationService =
AuthenticationServiceFactory::GetForBrowserState(_browserState);
// TODO(crbug.com/1466884): Delete the usage of ConsentLevel::kSync after
// Phase 2 on iOS is launched. See ConsentLevel::kSync documentation for
// details.
BOOL hasSyncingAccount =
authenticationService->HasPrimaryIdentity(signin::ConsentLevel::kSync);
if (!isSyncUIDisabled || hasSyncingAccount) {
[self.handler showSyncSettingsFromViewController:self];
} else {
[self.presentationDelegate showHistorySyncOptInAfterDedicatedSignIn:NO];
}
}
@end
@implementation ListModelCollapsedSceneSessionMediator {
UISceneSession* _session;
}
- (instancetype)initWithSession:(UISceneSession*)session {
self = [super init];
if (self) {
_session = session;
}
return self;
}
- (void)setSectionKey:(NSString*)sectionKey collapsed:(BOOL)collapsed {
NSMutableDictionary* newUserInfo =
[NSMutableDictionary dictionaryWithDictionary:_session.userInfo];
NSMutableDictionary* newCollapsedSection = [NSMutableDictionary
dictionaryWithDictionary:newUserInfo[kListModelCollapsedKey]];
newUserInfo[kListModelCollapsedKey] = newCollapsedSection;
newCollapsedSection[sectionKey] = [NSNumber numberWithBool:collapsed];
_session.userInfo = newUserInfo;
}
- (BOOL)sectionKeyIsCollapsed:(NSString*)sectionKey {
NSDictionary* collapsedSections = _session.userInfo[kListModelCollapsedKey];
NSNumber* value = (NSNumber*)[collapsedSections valueForKey:sectionKey];
return [value boolValue];
}
@end