// Copyright 2021 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/start_surface/ui_bundled/start_surface_scene_agent.h"
#import "base/containers/contains.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 "ios/chrome/app/application_delegate/app_state.h"
#import "ios/chrome/app/application_delegate/startup_information.h"
#import "ios/chrome/browser/ntp/model/new_tab_page_tab_helper.h"
#import "ios/chrome/browser/shared/coordinator/scene/scene_controller.h"
#import "ios/chrome/browser/shared/model/browser/browser.h"
#import "ios/chrome/browser/shared/model/browser/browser_provider.h"
#import "ios/chrome/browser/shared/model/browser/browser_provider_interface.h"
#import "ios/chrome/browser/shared/model/url/chrome_url_constants.h"
#import "ios/chrome/browser/shared/model/url/url_util.h"
#import "ios/chrome/browser/shared/model/web_state_list/removing_indexes.h"
#import "ios/chrome/browser/shared/model/web_state_list/tab_group.h"
#import "ios/chrome/browser/shared/model/web_state_list/web_state_list.h"
#import "ios/chrome/browser/shared/public/features/features.h"
#import "ios/chrome/browser/signin/model/chrome_account_manager_service.h"
#import "ios/chrome/browser/signin/model/chrome_account_manager_service_factory.h"
#import "ios/chrome/browser/signin/model/signin_util.h"
#import "ios/chrome/browser/start_surface/ui_bundled/start_surface_features.h"
#import "ios/chrome/browser/start_surface/ui_bundled/start_surface_recent_tab_browser_agent.h"
#import "ios/chrome/browser/start_surface/ui_bundled/start_surface_util.h"
#import "ios/chrome/browser/tab_insertion/model/tab_insertion_browser_agent.h"
#import "ios/web/public/navigation/navigation_item.h"
#import "ios/web/public/navigation/navigation_manager.h"
#import "ios/web/public/web_state.h"
#import "url/gurl.h"
namespace {
// Name of histogram to record the number of excess NTP tabs that are removed.
const char kExcessNTPTabsRemoved[] = "IOS.NTP.ExcessRemovedTabCount";
// Whether `web_state` shows the NTP.
bool IsNTP(const web::WebState* web_state) {
return IsUrlNtp(web_state->GetVisibleURL());
}
// Whether `web_state` shows the NTP and never had a navigation.
bool IsEmptyNTP(const web::WebState* web_state) {
return IsNTP(web_state) && web_state->GetNavigationItemCount() <= 1;
}
} // namespace
@interface StartSurfaceSceneAgent () <AppStateObserver>
// Caches the previous activation level.
@property(nonatomic, assign) SceneActivationLevel previousActivationLevel;
// YES if The AppState was not ready before the SceneState reached a valid
// activation level, so therefore this agent needs to wait for the AppState's
// initStage to reach a valid stage before checking whether the Start Surface
// should be shown.
@property(nonatomic, assign) BOOL waitingForAppStateAfterSceneStateReady;
@end
@implementation StartSurfaceSceneAgent
- (id)init {
self = [super init];
if (self) {
self.previousActivationLevel = SceneActivationLevelUnattached;
}
return self;
}
#pragma mark - ObservingSceneAgent
- (void)setSceneState:(SceneState*)sceneState {
[super setSceneState:sceneState];
[self.sceneState.appState addObserver:self];
}
#pragma mark - AppStateObserver
- (void)appState:(AppState*)appState
didTransitionFromInitStage:(InitStage)previousInitStage {
if (appState.initStage >= InitStageFirstRun &&
self.waitingForAppStateAfterSceneStateReady) {
self.waitingForAppStateAfterSceneStateReady = NO;
[self showStartSurfaceIfNecessary];
}
}
#pragma mark - SceneStateObserver
- (void)sceneStateDidDisableUI:(SceneState*)sceneState {
// Tear down objects tied to the scene state before it is deleted.
[self.sceneState.appState removeObserver:self];
self.waitingForAppStateAfterSceneStateReady = NO;
}
- (void)sceneState:(SceneState*)sceneState
transitionedToActivationLevel:(SceneActivationLevel)level {
if (level != SceneActivationLevelForegroundActive &&
self.previousActivationLevel == SceneActivationLevelForegroundActive) {
// TODO(crbug.com/40167003): Consider when to clear the session object since
// Chrome may be closed without transiting to inactive, e.g. device power
// off, then the previous session object is staled.
SetStartSurfaceSessionObjectForSceneState(sceneState);
}
if (level == SceneActivationLevelBackground &&
self.previousActivationLevel > SceneActivationLevelBackground) {
if (base::FeatureList::IsEnabled(kRemoveExcessNTPs)) {
// Remove duplicate NTP pages upon background event.
if (self.sceneState.browserProviderInterface.mainBrowserProvider
.browser) {
[self removeExcessNTPsInBrowser:self.sceneState.browserProviderInterface
.mainBrowserProvider.browser];
}
if (self.sceneState.browserProviderInterface.incognitoBrowserProvider
.browser) {
[self removeExcessNTPsInBrowser:self.sceneState.browserProviderInterface
.incognitoBrowserProvider.browser];
}
}
}
if (level >= SceneActivationLevelForegroundInactive &&
self.previousActivationLevel < SceneActivationLevelForegroundInactive) {
[self logBackgroundDurationMetricForActivationLevel:level];
[self showStartSurfaceIfNecessary];
}
self.previousActivationLevel = level;
}
- (void)showStartSurfaceIfNecessary {
if (self.sceneState.appState.initStage <= InitStageFirstRun) {
// NO if the app is not yet ready to present normal UI that is required by
// Start Surface.
self.waitingForAppStateAfterSceneStateReady = YES;
return;
}
Browser* browser =
self.sceneState.browserProviderInterface.mainBrowserProvider.browser;
// TODO(crbug.com/343699504): Remove pre-fetching capabilities once these
// are loaded in iSL.
if (IsPrefetchingSystemCapabilitiesOnAppStartup()) {
RunSystemCapabilitiesPrefetch(
ChromeAccountManagerServiceFactory::GetForBrowserState(
browser->GetBrowserState())
->GetAllIdentities());
}
if (!ShouldShowStartSurfaceForSceneState(self.sceneState)) {
return;
}
// Do not show the Start Surface no matter whether it is enabled or not when
// the Tab grid is active by design.
if (self.sceneState.controller.isTabGridVisible) {
return;
}
// If there is no active tab, a NTP will be added, and since there is no
// recent tab.
// Keep showing the last active NTP tab no matter whether the Start Surface is
// enabled or not by design.
// Note that activeWebState could only be nullptr when the Tab grid is active
// for now.
web::WebState* activeWebState =
self.sceneState.browserProviderInterface.mainBrowserProvider.browser
->GetWebStateList()
->GetActiveWebState();
if (!activeWebState || IsUrlNtp(activeWebState->GetVisibleURL())) {
return;
}
base::RecordAction(base::UserMetricsAction("IOS.StartSurface.Show"));
StartSurfaceRecentTabBrowserAgent::FromBrowser(browser)->SaveMostRecentTab();
// Activate the existing NTP tab for the Start surface.
WebStateList* webStateList = browser->GetWebStateList();
for (int i = 0; i < webStateList->count(); i++) {
web::WebState* webState = webStateList->GetWebStateAt(i);
if (IsUrlNtp(webState->GetVisibleURL())) {
NewTabPageTabHelper::FromWebState(webState)->SetShowStartSurface(true);
webStateList->ActivateWebStateAt(i);
return;
}
}
// Create a new NTP since there is no existing one.
TabInsertionBrowserAgent* insertion_agent =
TabInsertionBrowserAgent::FromBrowser(browser);
web::NavigationManager::WebLoadParams web_load_params(
(GURL(kChromeUINewTabURL)));
TabInsertion::Params tab_insertion_params;
tab_insertion_params.should_show_start_surface = true;
insertion_agent->InsertWebState(web_load_params, tab_insertion_params);
}
// Removes empty NTP tabs (i.e. NTPs with no further navigation) in `browser`'s
// WebStateList.
//
// NTPs with navigation are all preserved. If there are none, an empty NTP is
// preserved.
- (void)removeExcessNTPsInBrowser:(Browser*)browser {
WebStateList* webStateList = browser->GetWebStateList();
// Map groups to the indices of its empty NTPs, and whether the group contains
// at least one non-empty NTP (an NTP with navigation), which will be kept.
// Ungrouped tabs correspond to the `nullptr` entry in the map.
std::map<const TabGroup*, std::pair<std::vector<int>, bool>> groupsToNTPs;
for (int index = 0; index < webStateList->count(); ++index) {
const web::WebState* webState = webStateList->GetWebStateAt(index);
const TabGroup* tabGroup = webStateList->GetGroupOfWebStateAt(index);
if (IsEmptyNTP(webState)) {
groupsToNTPs[tabGroup].first.push_back(index);
} else if (IsNTP(webState)) {
groupsToNTPs[tabGroup].second = true;
}
}
// For each group (respectively the ungrouped tabs case), if there are only
// empty NTPs, preserve one NTP by removing it from the list of indices to
// close for the group (respectively the ungrouped tabs case).
for (auto& [group, NTPs] : groupsToNTPs) {
auto& indicesToRemoveInGroup = NTPs.first;
const bool groupHasNonEmptyNTP = NTPs.second;
if (indicesToRemoveInGroup.empty() || groupHasNonEmptyNTP) {
continue;
}
// Remove the last empty NTP from the list of tabs to close.
indicesToRemoveInGroup.pop_back();
}
// Flatten the list of indices to remove.
std::vector<int> indicesToRemove;
for (const auto& [group, NTPs] : groupsToNTPs) {
const auto& indicesToRemoveInGroup = NTPs.first;
indicesToRemove.insert(indicesToRemove.end(),
indicesToRemoveInGroup.begin(),
indicesToRemoveInGroup.end());
}
// Report how many, if any, excess NTPs have been removed.
UMA_HISTOGRAM_COUNTS_100(kExcessNTPTabsRemoved, indicesToRemove.size());
// Perform the operations on the WebStateList, if needed.
if (indicesToRemove.empty()) {
return;
}
const WebStateList::ScopedBatchOperation batch =
webStateList->StartBatchOperation();
// If the active tab is going to be closed, pick the last ungrouped
// NTP as the new active tab, otherwise insert a new NTP.
if (base::Contains(indicesToRemove, webStateList->active_index())) {
int lastUngroupedNTPIndex = WebStateList::kInvalidIndex;
for (int index = webStateList->count() - 1; index >= 0; --index) {
const web::WebState* webState = webStateList->GetWebStateAt(index);
const TabGroup* tabGroup = webStateList->GetGroupOfWebStateAt(index);
if (IsNTP(webState) && !tabGroup &&
!base::Contains(indicesToRemove, index)) {
lastUngroupedNTPIndex = index;
break;
}
}
if (lastUngroupedNTPIndex != WebStateList::kInvalidIndex) {
webStateList->ActivateWebStateAt(lastUngroupedNTPIndex);
} else {
// Insert a new NTP at the very end (this won't invalidate other indices).
web::NavigationManager::WebLoadParams webLoadParams =
web::NavigationManager::WebLoadParams(GURL(kChromeUINewTabURL));
TabInsertion::Params tabInsertionParams;
tabInsertionParams.should_skip_new_tab_animation = true;
TabInsertionBrowserAgent::FromBrowser(browser)->InsertWebState(
webLoadParams, tabInsertionParams);
}
}
// Close the excessive NTPs.
webStateList->CloseWebStatesAtIndices(
WebStateList::CLOSE_NO_FLAGS,
RemovingIndexes(std::move(indicesToRemove)));
}
- (void)logBackgroundDurationMetricForActivationLevel:
(SceneActivationLevel)level {
const base::TimeDelta timeSinceBackground =
GetTimeSinceMostRecentTabWasOpenForSceneState(self.sceneState);
const BOOL isColdStart =
(level > SceneActivationLevelBackground &&
self.sceneState.appState.startupInformation.isColdStart);
if (isColdStart) {
UMA_HISTOGRAM_CUSTOM_COUNTS("IOS.BackgroundTimeBeforeColdStart",
timeSinceBackground.InMinutes(), 1,
base::Hours(12).InMinutes(), 24);
} else {
UMA_HISTOGRAM_CUSTOM_COUNTS("IOS.BackgroundTimeBeforeWarmStart",
timeSinceBackground.InMinutes(), 1,
base::Hours(12).InMinutes(), 24);
}
}
@end