blob: 4543ca77d8547696ba3a8c87f161873a3885ddc4 [file] [log] [blame]
// Copyright 2018 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/ui/recent_tabs/recent_tabs_table_view_controller.h"
#include "base/check_op.h"
#include "base/mac/foundation_util.h"
#include "base/metrics/histogram_macros.h"
#include "base/metrics/user_metrics.h"
#include "base/metrics/user_metrics_action.h"
#include "base/notreached.h"
#import "base/numerics/safe_conversions.h"
#include "base/strings/sys_string_conversions.h"
#include "components/prefs/pref_service.h"
#include "components/sessions/core/tab_restore_service.h"
#include "components/strings/grit/components_strings.h"
#include "components/sync_sessions/open_tabs_ui_delegate.h"
#include "components/sync_sessions/session_sync_service.h"
#import "ios/chrome/app/tests_hook.h"
#include "ios/chrome/browser/browser_state/chrome_browser_state.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/main/browser.h"
#import "ios/chrome/browser/metrics/new_tab_page_uma.h"
#include "ios/chrome/browser/sessions/live_tab_context_browser_agent.h"
#include "ios/chrome/browser/sessions/session_util.h"
#include "ios/chrome/browser/sync/session_sync_service_factory.h"
#import "ios/chrome/browser/ui/alert_coordinator/action_sheet_coordinator.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/signin/signin_utils.h"
#import "ios/chrome/browser/ui/authentication/signin_promo_view_mediator.h"
#include "ios/chrome/browser/ui/commands/application_commands.h"
#import "ios/chrome/browser/ui/commands/open_new_tab_command.h"
#import "ios/chrome/browser/ui/commands/show_signin_command.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"
#include "ios/chrome/browser/ui/recent_tabs/synced_sessions.h"
#import "ios/chrome/browser/ui/settings/sync/utils/sync_presenter.h"
#import "ios/chrome/browser/ui/settings/sync/utils/sync_util.h"
#import "ios/chrome/browser/ui/table_view/cells/table_view_activity_indicator_header_footer_item.h"
#import "ios/chrome/browser/ui/table_view/cells/table_view_cells_constants.h"
#import "ios/chrome/browser/ui/table_view/cells/table_view_disclosure_header_footer_item.h"
#import "ios/chrome/browser/ui/table_view/cells/table_view_illustrated_item.h"
#import "ios/chrome/browser/ui/table_view/cells/table_view_image_item.h"
#import "ios/chrome/browser/ui/table_view/cells/table_view_text_button_item.h"
#import "ios/chrome/browser/ui/table_view/cells/table_view_text_item.h"
#import "ios/chrome/browser/ui/table_view/cells/table_view_url_item.h"
#import "ios/chrome/browser/ui/table_view/chrome_table_view_styler.h"
#import "ios/chrome/browser/ui/table_view/table_view_favicon_data_source.h"
#import "ios/chrome/browser/ui/table_view/table_view_utils.h"
#include "ios/chrome/browser/ui/ui_feature_flags.h"
#import "ios/chrome/browser/ui/util/menu_util.h"
#include "ios/chrome/browser/ui/util/ui_util.h"
#import "ios/chrome/browser/ui/util/uikit_ui_util.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"
#include "ios/chrome/browser/web_state_list/web_state_list.h"
#include "ios/chrome/browser/web_state_list/web_state_opener.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"
#include "ios/chrome/grit/ios_chromium_strings.h"
#include "ios/chrome/grit/ios_strings.h"
#import "ios/public/provider/chrome/browser/signin/signin_presenter.h"
#import "ios/web/public/web_state.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/l10n/time_format.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
namespace {
typedef NS_ENUM(NSInteger, SectionIdentifier) {
SectionIdentifierRecentlyClosedTabs = kSectionIdentifierEnumZero,
SectionIdentifierOtherDevices,
// The first SessionsSectionIdentifier index.
kFirstSessionSectionIdentifier,
};
typedef NS_ENUM(NSInteger, ItemType) {
ItemTypeRecentlyClosedHeader = kItemTypeEnumZero,
ItemTypeRecentlyClosed,
ItemTypeOtherDevicesHeader,
ItemTypeOtherDevicesSyncOff,
ItemTypeOtherDevicesNoSessions,
ItemTypeOtherDevicesSignedOut,
ItemTypeOtherDevicesSigninPromo,
ItemTypeOtherDevicesSyncInProgressHeader,
ItemTypeSessionHeader,
ItemTypeSessionTabData,
ItemTypeShowFullHistory,
ItemTypeSigninDisabled,
};
// 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";
// There are 2 static sections before the first SessionSection.
int const kNumberOfSectionsBeforeSessions = 1;
// 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;
} // namespace
API_AVAILABLE(ios(13.0))
@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,
SyncPresenter,
TableViewURLDragDataSource,
UIContextMenuInteractionDelegate,
UIGestureRecognizerDelegate> {
std::unique_ptr<synced_sessions::SyncedSessions> _syncedSessions;
}
// The service that manages the recently closed tabs
@property(nonatomic, assign) sessions::TabRestoreService* tabRestoreService;
// The sync state.
@property(nonatomic, assign) SessionsSyncUserState sessionState;
// Handles displaying the context menu for all form factors.
@property(nonatomic, strong) ActionSheetCoordinator* contextMenuCoordinator;
@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 = base::FeatureList::IsEnabled(kSettingsRefresh)
? ChromeTableViewStyle()
: UITableViewStylePlain;
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)dealloc {
[_signinPromoViewMediator signinPromoViewIsRemoved];
}
- (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];
}
}
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
}
#pragma mark - Setters & Getters
- (void)setBrowser:(Browser*)browser {
DCHECK(browser);
_browser = 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();
}
- (WebStateList*)webStateList {
return self.browser->GetWebStateList();
}
- (void)setPreventUpdates:(BOOL)preventUpdates {
if (_preventUpdates == preventUpdates)
return;
_preventUpdates = preventUpdates;
if (preventUpdates)
return;
[self loadModel];
[self.tableView reloadData];
}
#pragma mark - TableViewModel
- (void)loadModel {
[super loadModel];
if (@available(iOS 13, *)) {
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];
}
}
#pragma mark Recently Closed Section
- (void)addRecentlyClosedSection {
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 (base::FeatureList::IsEnabled(kIllustratedEmptyStates) &&
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];
// Add show full history item last.
TableViewImageItem* historyItem =
[[TableViewImageItem alloc] initWithType:ItemTypeShowFullHistory];
historyItem.title = l10n_util::GetNSString(IDS_HISTORY_SHOWFULLHISTORY_LINK);
historyItem.image = [UIImage imageNamed:@"show_history"];
if (self.styler.tintColor) {
historyItem.textColor = self.styler.tintColor;
} else {
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;
for (auto iter = self.tabRestoreService->entries().begin();
iter != self.tabRestoreService->entries().end(); ++iter) {
const sessions::TabRestoreService::Entry* entry = iter->get();
DCHECK(entry);
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];
// Configure and add the Item.
TableViewURLItem* recentlyClosedTab =
[[TableViewURLItem alloc] initWithType:ItemTypeRecentlyClosed];
recentlyClosedTab.title = base::SysUTF16ToNSString(navigationEntry.title());
recentlyClosedTab.URL = navigationEntry.virtual_url();
[self.tableViewModel addItem:recentlyClosedTab
toSectionWithIdentifier:SectionIdentifierRecentlyClosedTabs];
}
}
// Updates the recently closed tabs section by clobbering and reinserting
// section. Needs to be called inside a [UITableView beginUpdates] block on
// iOS10, or performBatchUpdates on iOS11+.
- (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 [UITableView beginUpdates] block on iOS10, or
// performBatchUpdates on iOS11+.
- (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->GetSession(i);
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 {
TableViewModel* model = self.tableViewModel;
NSInteger numberOfTabs = base::checked_cast<NSInteger>(session->tabs.size());
for (int i = 0; i < numberOfTabs; i++) {
synced_sessions::DistantTab const* sessionTab = session->tabs[i].get();
NSString* title = base::SysUTF16ToNSString(sessionTab->title);
TableViewURLItem* sessionTabItem =
[[TableViewURLItem alloc] initWithType:ItemTypeSessionTabData];
sessionTabItem.title = title;
sessionTabItem.URL = sessionTab->virtual_url;
[model addItem:sessionTabItem
toSectionWithIdentifier:[self sectionIdentifierForSession:session]];
}
}
// Remove all SessionSections from |self.tableViewModel| and |self.tableView|
// Needs to be called inside a [UITableView beginUpdates] block on iOS10, or
// performBatchUpdates on iOS11+.
- (void)removeSessionSections {
// |_syncedSessions| has been updated by now, that means that
// |self.tableViewModel| does not reflect |_syncedSessions| data.
NSInteger sectionIdentifierToRemove = kFirstSessionSectionIdentifier;
NSInteger sectionToDelete = kNumberOfSectionsBeforeSessions;
while ([self.tableViewModel numberOfSections] >
kNumberOfSectionsBeforeSessions) {
// A SectionIdentifier could've been deleted previously, do not rely on
// these being in sequential order at this point.
if ([self.tableViewModel
hasSectionForSectionIdentifier:sectionIdentifierToRemove]) {
[self.tableView
deleteSections:[NSIndexSet indexSetWithIndex:sectionToDelete]
withRowAnimation:UITableViewRowAnimationNone];
sectionToDelete++;
[self.tableViewModel
removeSectionWithIdentifier:sectionIdentifierToRemove];
}
sectionIdentifierToRemove++;
}
}
#pragma mark Other Devices Section
// Cleans up the model in order to update the Other devices section. Needs to be
// called inside a [UITableView beginUpdates] block on iOS10, or
// performBatchUpdates on iOS11+.
- (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 {
// 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 mesage.
if (!signin::IsSigninAllowed(self.browserState->GetPrefs()) &&
signin::IsSigninAllowedByPolicy()) {
return;
}
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);
[model setHeader:header
forSectionWithIdentifier:SectionIdentifierOtherDevices];
header.collapsed =
[self.tableViewModel sectionIsCollapsed:SectionIdentifierOtherDevices];
}
if (!signin::IsSigninAllowed(self.browserState->GetPrefs())) {
// If sign-in is disabled by policy, don't show an illustration or a sign-in
// promo.
TableViewTextItem* disabledByOrganizationText =
[[TableViewTextItem alloc] initWithType:ItemTypeSigninDisabled];
disabledByOrganizationText.text =
l10n_util::GetNSString(IDS_IOS_RECENT_TABS_DISABLED_BY_ORGANIZATION);
disabledByOrganizationText.textColor =
[UIColor colorNamed:kTextSecondaryColor];
[self.tableViewModel addItem:disabledByOrganizationText
toSectionWithIdentifier:SectionIdentifierOtherDevices];
} else if (base::FeatureList::IsEnabled(kIllustratedEmptyStates)) {
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_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 = 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];
} else {
// Adds Other Devices item for |state|.
TableViewTextItem* dummyCell = nil;
switch (state) {
case SessionsSyncUserState::USER_SIGNED_IN_SYNC_ON_WITH_SESSIONS:
NOTREACHED();
return;
case SessionsSyncUserState::USER_SIGNED_IN_SYNC_OFF:
[self addUserSignedSyncOffItem];
return;
case SessionsSyncUserState::USER_SIGNED_IN_SYNC_ON_NO_SESSIONS:
dummyCell = [[TableViewTextItem alloc]
initWithType:ItemTypeOtherDevicesNoSessions];
dummyCell.text =
l10n_util::GetNSString(IDS_IOS_OPEN_TABS_NO_SESSION_INSTRUCTIONS);
break;
case SessionsSyncUserState::USER_SIGNED_OUT:
[self addSigninPromoViewItem];
return;
case SessionsSyncUserState::USER_SIGNED_IN_SYNC_IN_PROGRESS:
// Informational text in section header. No need for a cell in the
// section.
NOTREACHED();
return;
}
[self.tableViewModel addItem:dummyCell
toSectionWithIdentifier:SectionIdentifierOtherDevices];
}
}
- (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)addUserSignedSyncOffItem {
TableViewTextButtonItem* signinSyncOffItem = [[TableViewTextButtonItem alloc]
initWithType:ItemTypeOtherDevicesSyncOff];
signinSyncOffItem.text =
l10n_util::GetNSString(IDS_IOS_OPEN_TABS_SYNC_IS_OFF_MOBILE);
signinSyncOffItem.buttonText =
l10n_util::GetNSString(IDS_IOS_OPEN_TABS_ENABLE_SYNC_MOBILE);
[self.tableViewModel addItem:signinSyncOffItem
toSectionWithIdentifier:SectionIdentifierOtherDevices];
}
- (void)addSigninPromoViewItem {
// Init|_signinPromoViewMediator| if nil.
if (!self.signinPromoViewMediator && self.browserState) {
self.signinPromoViewMediator = [[SigninPromoViewMediator alloc]
initWithBrowserState:self.browserState
accessPoint:signin_metrics::AccessPoint::
ACCESS_POINT_RECENT_TABS
presenter:self];
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 - 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++) {
synced_sessions::DistantSession const* session =
_syncedSessions->GetSession(i);
if (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 the all the Session Sections.
- (NSIndexSet*)sessionSectionIndexSet {
// Create a range of all Session Sections.
NSRange rangeOfSessionSections =
NSMakeRange(kNumberOfSectionsBeforeSessions, [self numberOfSessions]);
NSIndexSet* sessionSectionIndexes =
[NSIndexSet indexSetWithIndexesInRange:rangeOfSessionSections];
return sessionSectionIndexes;
}
#pragma mark - Public
- (synced_sessions::DistantSession const*)sessionForTableSectionWithIdentifier:
(NSInteger)sectionIdentifer {
NSInteger section =
[self.tableViewModel sectionForSectionIdentifier:sectionIdentifer];
DCHECK([self isSessionSectionIdentifier:sectionIdentifer]);
return _syncedSessions->GetSession(section - kNumberOfSectionsBeforeSessions);
}
- (void)removeSessionAtTableSectionWithIdentifier:(NSInteger)sectionIdentifier {
DCHECK([self isSessionSectionIdentifier:sectionIdentifier]);
synced_sessions::DistantSession const* session =
[self sessionForTableSectionWithIdentifier:sectionIdentifier];
std::string sessionTagCopy = session->tag;
NSInteger section =
[self.tableViewModel sectionForSectionIdentifier:sectionIdentifier];
__weak __typeof(self) weakSelf = self;
void (^tableUpdates)(void) = ^{
[weakSelf.tableViewModel removeSectionWithIdentifier:sectionIdentifier];
_syncedSessions->EraseSession(section - kNumberOfSectionsBeforeSessions);
[weakSelf.tableView deleteSections:[NSIndexSet indexSetWithIndex:section]
withRowAnimation:UITableViewRowAnimationLeft];
};
[self.tableView performBatchUpdates:tableUpdates
completion:^(BOOL) {
if (!weakSelf)
return;
sync_sessions::OpenTabsUIDelegate* openTabs =
SessionSyncServiceFactory::GetForBrowserState(
weakSelf.browserState)
->GetOpenTabsUIDelegate();
openTabs->DeleteForeignSession(sessionTagCopy);
}];
}
#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];
}
#pragma mark - Consumer Protocol
- (void)refreshUserState:(SessionsSyncUserState)newSessionState {
if ((newSessionState == self.sessionState &&
self.sessionState !=
SessionsSyncUserState::USER_SIGNED_IN_SYNC_ON_WITH_SESSIONS) ||
self.signinPromoViewMediator.signinInProgress) {
// 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;
}
sync_sessions::SessionSyncService* syncService =
SessionSyncServiceFactory::GetForBrowserState(self.browserState);
_syncedSessions.reset(new synced_sessions::SyncedSessions(syncService));
if (!self.preventUpdates) {
// 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 signinPromoViewIsRemoved];
self.signinPromoViewMediator.consumer = nil;
self.signinPromoViewMediator = nil;
}
}
- (void)refreshRecentlyClosedTabs {
if (self.preventUpdates)
return;
[self.tableView performBatchUpdates:^{
[self updateRecentlyClosedSection];
}
completion:nil];
}
- (void)setTabRestoreService:(sessions::TabRestoreService*)tabRestoreService {
_tabRestoreService = tabRestoreService;
}
- (void)dismissModals {
[self.contextMenuCoordinator stop];
}
#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
openTabWithTabRestoreEntry:[self
tabRestoreEntryAtIndexPath: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 showHistoryFromRecentTabs];
}
break;
case ItemTypeOtherDevicesSyncOff:
case ItemTypeOtherDevicesNoSessions:
case ItemTypeOtherDevicesSigninPromo:
case ItemTypeSigninDisabled:
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
sectionIdentifierForSection:section]]
? 1.0
: kSeparationSpaceBetweenSections;
}
#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];
if (base::FeatureList::IsEnabled(kIllustratedEmptyStates)) {
TableViewSigninPromoCell* signinPromoCell =
base::mac::ObjCCastStrict<TableViewSigninPromoCell>(cell);
signinPromoCell.signinPromoView.imageView.hidden = YES;
signinPromoCell.signinPromoView.textLabel.hidden = YES;
if (base::FeatureList::IsEnabled(kSettingsRefresh)) {
signinPromoCell.backgroundColor = nil;
}
}
}
// 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) {
if (base::FeatureList::IsEnabled(kIllustratedEmptyStates)) {
TableViewIllustratedCell* illustratedCell =
base::mac::ObjCCastStrict<TableViewIllustratedCell>(cell);
[illustratedCell.button addTarget:self
action:@selector(updateSyncState)
forControlEvents:UIControlEventTouchUpInside];
} else {
TableViewTextButtonCell* tableViewTextButtonCell =
base::mac::ObjCCastStrict<TableViewTextButtonCell>(cell);
[tableViewTextButtonCell.button addTarget:self
action:@selector(updateSyncState)
forControlEvents:UIControlEventTouchUpInside];
}
}
// Hide the separator between this cell and the SignIn Promo.
if (itemTypeSelected == ItemTypeOtherDevicesSignedOut) {
// This cell should only exist when illustrated-empty-states is enabled.
DCHECK(base::FeatureList::IsEnabled(kIllustratedEmptyStates));
cell.separatorInset =
UIEdgeInsetsMake(0, self.tableView.bounds.size.width, 0, 0);
}
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 sectionIdentifierForSection: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.
if (!IsNativeContextMenuEnabled()) {
UILongPressGestureRecognizer* longPress =
[[UILongPressGestureRecognizer alloc]
initWithTarget:self
action:@selector(handleLongPress:)];
[header addGestureRecognizer:longPress];
} else if (@available(iOS 13, *)) {
[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
API_AVAILABLE(ios(13.0)) {
if (!IsNativeContextMenuEnabled()) {
// Returning nil will allow the gesture to be captured and show the old
// context menus.
return nil;
}
NSInteger itemType = [self.tableViewModel itemTypeForIndexPath:indexPath];
if (itemType != ItemTypeRecentlyClosed && itemType != ItemTypeSessionTabData)
return nil;
TableViewItem* item = [self.tableViewModel itemAtIndexPath:indexPath];
TableViewURLItem* URLItem = base::mac::ObjCCastStrict<TableViewURLItem>(item);
return [self.menuProvider
contextMenuConfigurationForItem:URLItem
fromView:[tableView
cellForRowAtIndexPath:indexPath]];
}
#pragma mark - UIContextMenuInteractionDelegate
- (UIContextMenuConfiguration*)contextMenuInteraction:
(UIContextMenuInteraction*)interaction
configurationForMenuAtLocation:(CGPoint)location
API_AVAILABLE(ios(13.0)) {
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::mac::ObjCCastStrict<TableViewURLItem>(item);
return [[URLInfo alloc] initWithURL:URLItem.URL 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
- (NSInteger)numberOfRecentlyClosedTabs {
if (!self.tabRestoreService)
return 0;
return base::checked_cast<NSInteger>(
self.tabRestoreService->entries().size());
}
- (const sessions::TabRestoreService::Entry*)tabRestoreEntryAtIndexPath:
(NSIndexPath*)indexPath {
DCHECK_EQ([self.tableViewModel sectionIdentifierForSection:indexPath.section],
SectionIdentifierRecentlyClosedTabs);
NSInteger index = indexPath.row;
DCHECK_LE(index, [self numberOfRecentlyClosedTabs]);
if (!self.tabRestoreService)
return nullptr;
// Advance the entry iterator to the correct index.
// Note that std:list<> can only be accessed sequentially, which is
// suboptimal when using Cocoa table APIs. This list doesn't appear
// to get very long, so it probably won't matter for perf.
sessions::TabRestoreService::Entries::const_iterator iter =
self.tabRestoreService->entries().begin();
std::advance(iter, index);
CHECK(*iter);
return iter->get();
}
// 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::mac::ObjCCastStrict<TableViewURLItem>(item);
TableViewURLCell* URLCell = base::mac::ObjCCastStrict<TableViewURLCell>(cell);
NSString* itemIdentifier = URLItem.uniqueIdentifier;
[self.imageDataSource
faviconForURL: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 _syncedSessions->GetSessionCount();
}
// 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 sectionIdentifierForSection:indexPath.section]);
// Get the index of this sectionIdentifier.
size_t indexOfSession = [[self allSessionSectionIdentifiers]
indexOfObject:sectionIdentifierForIndexPath];
DCHECK_LT(indexOfSession, _syncedSessions->GetSessionCount());
return indexOfSession;
}
- (synced_sessions::DistantSession const*)sessionForTabAtIndexPath:
(NSIndexPath*)indexPath {
return _syncedSessions->GetSession(
[self indexOfSessionForTabAtIndexPath:indexPath]);
}
- (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];
DCHECK_LT(indexOfDistantTab, session->tabs.size());
return session->tabs[indexOfDistantTab].get();
}
- (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 {
// 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::RecordAction(base::UserMetricsAction(
"MobileRecentTabManagerTabFromOtherDeviceOpened"));
new_tab_page_uma::RecordAction(
self.browserState, 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)openTabWithTabRestoreEntry:
(const sessions::TabRestoreService::Entry*)entry {
// 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;
// Only TAB type is handled.
// TODO(crbug.com/1056596) : Support WINDOW restoration under multi-window.
DCHECK_EQ(entry->type, sessions::TabRestoreService::TAB);
base::RecordAction(
base::UserMetricsAction("MobileRecentTabManagerRecentTabOpened"));
new_tab_page_uma::RecordAction(
self.browserState, 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];
}
#pragma mark - Collapse/Expand sections
- (void)handleTap:(UITapGestureRecognizer*)sender {
UIView* headerTapped = sender.view;
NSInteger tappedHeaderSectionIdentifier = headerTapped.tag;
if (sender.state == UIGestureRecognizerStateEnded) {
[self toggleExpansionOfSectionIdentifier:tappedHeaderSectionIdentifier];
NSInteger section = [self.tableViewModel
sectionForSectionIdentifier:tappedHeaderSectionIdentifier];
UITableViewHeaderFooterView* headerView =
[self.tableView headerViewForSection:section];
ListItem* headerItem = [self.tableViewModel headerForSection: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::mac::ObjCCastStrict<TableViewDisclosureHeaderFooterView>(
headerView);
TableViewDisclosureHeaderFooterItem* disclosureItem =
base::mac::ObjCCastStrict<TableViewDisclosureHeaderFooterItem>(
headerItem);
BOOL collapsed = [self.tableViewModel
sectionIsCollapsed:[self.tableViewModel
sectionIdentifierForSection:section]];
DisclosureDirection direction =
collapsed ? DisclosureDirectionTrailing : DisclosureDirectionDown;
[disclosureHeaderView animateHighlightAndRotateToDirection: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];
}
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 - Long press and context menus
- (void)handleLongPress:(UILongPressGestureRecognizer*)sender {
if (sender.state != UIGestureRecognizerStateBegan)
return;
// Do not handle the long press and present the context menu if the recent
// tabs UI is not visible.
if (!self.viewLoaded || !self.view.window || self.presentedViewController)
return;
UIView* headerTapped = sender.view;
NSInteger tappedHeaderSectionIdentifier = headerTapped.tag;
NSInteger sectionIdentifier = tappedHeaderSectionIdentifier;
// Only handle LongPress for SessionHeaders.
if (![self isSessionSectionIdentifier:sectionIdentifier])
return;
// Highlight the section header being long pressed.
NSInteger section = [self.tableViewModel
sectionForSectionIdentifier:tappedHeaderSectionIdentifier];
ListItem* headerItem = [self.tableViewModel headerForSection:section];
UITableViewHeaderFooterView* headerView =
[self.tableView headerViewForSection:section];
if (headerItem.type == ItemTypeRecentlyClosedHeader ||
headerItem.type == ItemTypeSessionHeader) {
TableViewDisclosureHeaderFooterView* textHeaderView =
base::mac::ObjCCastStrict<TableViewDisclosureHeaderFooterView>(
headerView);
[textHeaderView animateHighlight];
}
// Get view coordinates in local space.
CGPoint viewCoordinate = [sender locationInView:self.tableView];
// Present sheet/popover using controller that is added to view hierarchy.
self.contextMenuCoordinator = [[ActionSheetCoordinator alloc]
initWithBaseViewController:self
browser:self.browser
title:nil
message:nil
rect:CGRectMake(viewCoordinate.x, viewCoordinate.y,
1.0, 1.0)
view:self.tableView];
// Fill the sheet/popover with buttons.
__weak RecentTabsTableViewController* weakSelf = self;
// "Open all tabs" button.
NSString* openAllButtonLabel =
l10n_util::GetNSString(IDS_IOS_RECENT_TABS_OPEN_ALL_MENU_OPTION);
[self.contextMenuCoordinator
addItemWithTitle:openAllButtonLabel
action:^{
[weakSelf
openTabsFromSessionSectionIdentifier:sectionIdentifier];
}
style:UIAlertActionStyleDefault];
// "Hide for now" button.
NSString* hideButtonLabel =
l10n_util::GetNSString(IDS_IOS_RECENT_TABS_HIDE_MENU_OPTION);
[self.contextMenuCoordinator
addItemWithTitle:hideButtonLabel
action:^{
[weakSelf removeSessionAtTableSectionWithIdentifier:
sectionIdentifier];
}
style:UIAlertActionStyleDefault];
[self.contextMenuCoordinator start];
}
- (void)openTabsFromSessionSectionIdentifier:(NSInteger)sectionIdentifier {
synced_sessions::DistantSession const* session =
[self sessionForTableSectionWithIdentifier:sectionIdentifier];
[self.presentationDelegate openAllTabsFromSession:session];
}
#pragma mark - SigninPromoViewConsumer
- (void)configureSigninPromoWithConfigurator:
(SigninPromoViewConfigurator*)configurator
identityChanged:(BOOL)identityChanged {
DCHECK(self.signinPromoViewMediator);
// 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::mac::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 {
[self.delegate refreshSessionsView];
}
#pragma mark - SyncPresenter
- (void)showReauthenticateSignin {
[self.handler showSignin:[[ShowSigninCommand alloc]
initWithOperation:
AUTHENTICATION_OPERATION_REAUTHENTICATE
accessPoint:signin_metrics::AccessPoint::
ACCESS_POINT_UNKNOWN]
baseViewController:self];
}
- (void)showSyncPassphraseSettings {
[self.handler showSyncPassphraseSettingsFromViewController:self];
}
- (void)showGoogleServicesSettings {
[self.handler showGoogleServicesSettingsFromViewController:self];
}
- (void)showAccountSettings {
[self.handler showAccountsSettingsFromViewController:self];
}
- (void)showTrustedVaultReauthenticationWithRetrievalTrigger:
(syncer::KeyRetrievalTriggerForUMA)retrievalTrigger {
[self.handler
showTrustedVaultReauthenticationFromViewController:self
retrievalTrigger:retrievalTrigger];
}
#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 - Private Helpers
- (void)updateSyncState {
SyncSetupService::SyncServiceState syncState =
GetSyncStateForBrowserState(_browserState);
if (ShouldShowSyncSignin(syncState)) {
[self showReauthenticateSignin];
} else if (ShouldShowSyncSettings(syncState)) {
[self showGoogleServicesSettings];
} else if (ShouldShowSyncPassphraseSettings(syncState)) {
[self showSyncPassphraseSettings];
}
}
@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