blob: d2ccabaaa908727bd54b93db76b0463697e8d44d [file] [log] [blame]
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "ios/chrome/browser/ui/reader_mode/reader_mode_checker.h"
#include "base/logging.h"
#include "base/strings/sys_string_conversions.h"
#include "components/dom_distiller/core/distillable_page_detector.h"
#include "components/dom_distiller/core/experiments.h"
#include "components/dom_distiller/core/page_features.h"
#include "components/dom_distiller/core/url_utils.h"
#include "components/grit/components_resources.h"
#include "ios/web/public/web_state/js/crw_js_injection_evaluator.h"
#include "ios/web/public/web_state/js/crw_js_injection_receiver.h"
#include "ios/web/public/web_state/web_state.h"
#include "ui/base/resource/resource_bundle.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
ReaderModeCheckerObserver::ReaderModeCheckerObserver(
ReaderModeChecker* readerModeChecker)
: readerModeChecker_(readerModeChecker) {
DCHECK(readerModeChecker);
readerModeChecker_->AddObserver(this);
}
ReaderModeCheckerObserver::~ReaderModeCheckerObserver() {
readerModeChecker_->RemoveObserver(this);
}
enum class ReaderModeChecker::Status {
kSwitchNo, // There is no way a switch could happen.
kSwitchMaybe, // Not sure, it may be possible.
kSwitchPossible, // It is possible to switch to reader mode.
};
ReaderModeChecker::ReaderModeChecker(web::WebState* web_state)
: web::WebStateObserver(web_state),
is_distillable_(Status::kSwitchNo),
weak_factory_(this) {
DCHECK(web_state);
}
ReaderModeChecker::~ReaderModeChecker() {
for (auto& observer : observers_)
observer.ReaderModeCheckerDestroyed();
}
bool ReaderModeChecker::CanSwitchToReaderMode() {
return is_distillable_ != ReaderModeChecker::Status::kSwitchNo;
}
void ReaderModeChecker::PageLoaded(
web::PageLoadCompletionStatus load_completion_status) {
if (load_completion_status != web::PageLoadCompletionStatus::SUCCESS)
return;
if (is_distillable_ == Status::kSwitchPossible)
return;
CheckIsDistillable();
}
void ReaderModeChecker::NavigationItemCommitted(
const web::LoadCommittedDetails& load_details) {
// When NavigationItemCommitted the page may not be completely loaded. But in
// order to get reader mode data faster this class tries to inject the
// javascript early. If it fails, it will be retried at PageLoaded().
is_distillable_ = Status::kSwitchNo;
CheckIsDistillable();
}
void ReaderModeChecker::CheckIsDistillableOG(CRWJSInjectionReceiver* receiver) {
// Retrieve the javascript used to figure out if the page is at all
// distillable.
NSString* is_distillable_js =
base::SysUTF8ToNSString(dom_distiller::url_utils::GetIsDistillableJs());
DCHECK(is_distillable_js);
// In case |this| gets deallocated before the block executes, the block only
// references a weak pointer.
base::WeakPtr<ReaderModeChecker> weak_this(weak_factory_.GetWeakPtr());
[receiver executeJavaScript:is_distillable_js
completionHandler:^(id result, NSError* error) {
if (!weak_this || error || !result)
return; // Inconclusive.
if ([result isEqual:@YES]) {
weak_this->is_distillable_ = Status::kSwitchPossible;
for (auto& observer : weak_this->observers_)
observer.PageIsDistillable();
} else {
weak_this->is_distillable_ = Status::kSwitchMaybe;
}
}];
}
void ReaderModeChecker::CheckIsDistillableDetector(
CRWJSInjectionReceiver* receiver) {
// Retrieve the javascript used to figure out if the page is at all
// distillable.
NSString* extract_features_js = base::SysUTF8ToNSString(
ResourceBundle::GetSharedInstance()
.GetRawDataResource(IDR_EXTRACT_PAGE_FEATURES_JS)
.as_string());
DCHECK(extract_features_js);
// In case |this| gets deallocated before the block executes, the block only
// references a weak pointer.
base::WeakPtr<ReaderModeChecker> weak_this(weak_factory_.GetWeakPtr());
[receiver
executeJavaScript:extract_features_js
completionHandler:^(id result, NSError* error) {
if (!weak_this || error || ![result isKindOfClass:[NSString class]]) {
return; // Inconclusive.
}
const dom_distiller::DistillablePageDetector* detector =
dom_distiller::DistillablePageDetector::GetDefault();
const base::StringValue value(base::SysNSStringToUTF8(result));
std::vector<double> features(
dom_distiller::CalculateDerivedFeaturesFromJSON(&value));
if (detector->Classify(features)) {
weak_this->is_distillable_ = Status::kSwitchPossible;
for (auto& observer : weak_this->observers_)
observer.PageIsDistillable();
} else {
weak_this->is_distillable_ = Status::kSwitchMaybe;
}
}];
}
void ReaderModeChecker::CheckIsDistillable() {
CRWJSInjectionReceiver* receiver = web_state()->GetJSInjectionReceiver();
if (!receiver || !web_state()->ContentIsHTML() ||
web_state()->IsShowingWebInterstitial()) {
is_distillable_ = Status::kSwitchNo;
return;
}
switch (dom_distiller::GetDistillerHeuristicsType()) {
case dom_distiller::DistillerHeuristicsType::NONE:
is_distillable_ = Status::kSwitchMaybe;
break;
case dom_distiller::DistillerHeuristicsType::OG_ARTICLE:
CheckIsDistillableOG(receiver);
break;
case dom_distiller::DistillerHeuristicsType::ADABOOST_MODEL:
CheckIsDistillableDetector(receiver);
break;
case dom_distiller::DistillerHeuristicsType::ALWAYS_TRUE:
is_distillable_ = Status::kSwitchPossible;
break;
}
}
void ReaderModeChecker::WebStateDestroyed() {
is_distillable_ = Status::kSwitchNo;
}
void ReaderModeChecker::AddObserver(ReaderModeCheckerObserver* observer) {
DCHECK(!observers_.HasObserver(observer));
observers_.AddObserver(observer);
}
void ReaderModeChecker::RemoveObserver(ReaderModeCheckerObserver* observer) {
DCHECK(observers_.HasObserver(observer));
observers_.RemoveObserver(observer);
}