// Copyright 2017 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/web/model/sad_tab_tab_helper.h"
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#import <memory>
#import "base/check_op.h"
#import "base/memory/ptr_util.h"
#import "base/metrics/histogram_macros.h"
#import "base/strings/sys_string_conversions.h"
#import "ios/chrome/browser/ntp/model/new_tab_page_tab_helper.h"
#import "ios/chrome/browser/shared/model/url/chrome_url_constants.h"
#import "ios/chrome/browser/shared/model/utils/notification_observer_bridge.h"
#import "ios/chrome/browser/ui/fullscreen/fullscreen_controller.h"
#import "ios/chrome/browser/ui/fullscreen/scoped_fullscreen_disabler.h"
#import "ios/chrome/browser/web/model/features.h"
#import "ios/chrome/browser/web/model/page_placeholder_tab_helper.h"
#import "ios/chrome/browser/web/model/sad_tab_tab_helper_delegate.h"
#import "ios/components/webui/web_ui_url_constants.h"
#import "ios/web/public/navigation/navigation_context.h"
#import "ios/web/public/navigation/navigation_manager.h"
#import "ios/web/public/web_state.h"
namespace {
// Returns true if the application is in UIApplicationStateActive state.
bool IsApplicationStateActive() {
return UIApplication.sharedApplication.applicationState ==
UIApplicationStateActive;
}
} // namespace
SadTabTabHelper::SadTabTabHelper(web::WebState* web_state,
base::TimeDelta repeat_failure_interval)
: web_state_(web_state), repeat_failure_interval_(repeat_failure_interval) {
web_state_->AddObserver(this);
if (web_state_->IsRealized()) {
CreateNotificationObserver();
}
}
SadTabTabHelper::~SadTabTabHelper() {
DCHECK(!web_state_);
}
void SadTabTabHelper::SetDelegate(id<SadTabTabHelperDelegate> delegate) {
delegate_ = delegate;
if (delegate_ && showing_sad_tab_ && web_state_->IsVisible()) {
[delegate_ sadTabTabHelper:this
didShowForRepeatedFailure:repeated_failure_];
}
}
void SadTabTabHelper::WasShown(web::WebState* web_state) {
DCHECK_EQ(web_state_, web_state);
if (requires_reload_on_becoming_visible_) {
ReloadTab();
requires_reload_on_becoming_visible_ = false;
}
if (showing_sad_tab_) {
DCHECK(delegate_);
[delegate_ sadTabTabHelper:this
didShowForRepeatedFailure:repeated_failure_];
}
}
void SadTabTabHelper::WasHidden(web::WebState* web_state) {
if (showing_sad_tab_) {
[delegate_ sadTabTabHelperDidHide:this];
}
}
void SadTabTabHelper::RenderProcessGone(web::WebState* web_state) {
DCHECK_EQ(web_state_, web_state);
// Don't present a sad tab on top of an NTP.
NewTabPageTabHelper* NTPHelper = NewTabPageTabHelper::FromWebState(web_state);
if (NTPHelper && NTPHelper->IsActive()) {
return;
}
// Only show Sad Tab if renderer has crashed in a tab currently visible to the
// user and only if application is active. Otherwise simpy reloading the page
// is a better user experience.
if (!web_state->IsVisible()) {
requires_reload_on_becoming_visible_ = true;
return;
}
if (!IsApplicationStateActive()) {
requires_reload_on_becoming_active_ = true;
return;
}
OnVisibleCrash(web_state->GetLastCommittedURL());
if (repeated_failure_) {
PresentSadTab();
} else {
web_state->GetNavigationManager()->Reload(web::ReloadType::NORMAL,
true /* check_for_repost */);
}
}
void SadTabTabHelper::DidStartNavigation(
web::WebState* web_state,
web::NavigationContext* navigation_context) {
// The sad tab is removed when a new navigation begins.
SetIsShowingSadTab(false);
// NO-OP is fine if `delegate_` is nil since the `delegate_` will be updated
// when it is set.
[delegate_ sadTabTabHelperDismissSadTab:this];
}
void SadTabTabHelper::DidFinishNavigation(
web::WebState* web_state,
web::NavigationContext* navigation_context) {
DCHECK_EQ(web_state_, web_state);
if (navigation_context->GetUrl().host() == kChromeUICrashHost &&
navigation_context->GetUrl().scheme() == kChromeUIScheme) {
OnVisibleCrash(navigation_context->GetUrl());
PresentSadTab();
}
}
void SadTabTabHelper::WebStateDestroyed(web::WebState* web_state) {
DCHECK_EQ(web_state_, web_state);
web_state_->RemoveObserver(this);
web_state_ = nullptr;
}
void SadTabTabHelper::WebStateRealized(web::WebState* web_state) {
CHECK(!background_notification_observer_, base::NotFatalUntil::M125);
CreateNotificationObserver();
}
void SadTabTabHelper::CreateNotificationObserver() {
base::RepeatingCallback<void(NSNotification*)> backgrounding_closure =
base::IgnoreArgs<NSNotification*>(base::BindRepeating(
&SadTabTabHelper::OnAppDidBecomeActive, weak_factory_.GetWeakPtr()));
background_notification_observer_ = [[NSNotificationCenter defaultCenter]
addObserverForName:UIApplicationDidBecomeActiveNotification
object:nil
queue:nil
usingBlock:base::CallbackToBlock(backgrounding_closure)];
}
void SadTabTabHelper::OnVisibleCrash(const GURL& url_causing_failure) {
// Is this failure a repeat-failure requiring the presentation of the Feedback
// UI rather than the Reload UI?
base::TimeDelta seconds_since_last_failure =
last_failed_timer_ ? last_failed_timer_->Elapsed()
: base::TimeDelta::Max();
repeated_failure_ =
(url_causing_failure.EqualsIgnoringRef(last_failed_url_) &&
seconds_since_last_failure < repeat_failure_interval_);
last_failed_url_ = url_causing_failure;
last_failed_timer_ = std::make_unique<base::ElapsedTimer>();
}
void SadTabTabHelper::PresentSadTab() {
// NO-OP is fine if `delegate_` is nil since the `delegate_` will be updated
// when it is set.
[delegate_ sadTabTabHelper:this
presentSadTabForWebState:web_state_
repeatedFailure:repeated_failure_];
SetIsShowingSadTab(true);
bool is_pdf = web_state_->GetContentsMimeType() == "application/pdf";
bool is_chrome_external_file_url =
last_failed_url_.host() == kChromeUIExternalFileHost &&
last_failed_url_.scheme() == kChromeUIScheme;
UMA_HISTOGRAM_BOOLEAN("IOS.SadTab.FileIsPDF", is_pdf);
UMA_HISTOGRAM_BOOLEAN("IOS.SadTab.URLIsChromeExternalFile",
is_chrome_external_file_url);
}
void SadTabTabHelper::SetIsShowingSadTab(bool showing_sad_tab) {
if (showing_sad_tab_ != showing_sad_tab) {
showing_sad_tab_ = showing_sad_tab;
}
}
void SadTabTabHelper::ReloadTab() {
PagePlaceholderTabHelper::FromWebState(web_state_)
->AddPlaceholderForNextNavigation();
web_state_->GetNavigationManager()->LoadIfNecessary();
}
void SadTabTabHelper::OnAppDidBecomeActive() {
if (!requires_reload_on_becoming_active_)
return;
if (web_state_->IsVisible()) {
ReloadTab();
} else {
requires_reload_on_becoming_visible_ = true;
}
requires_reload_on_becoming_active_ = false;
}
WEB_STATE_USER_DATA_KEY_IMPL(SadTabTabHelper)