| // 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 |