// Copyright 2015 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.chrome.browser.dom_distiller;
import android.app.Activity;
import android.content.Intent;
import android.content.res.Resources;
import android.net.Uri;
import android.os.SystemClock;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.browser.customtabs.CustomTabsIntent;
import org.chromium.base.CommandLine;
import org.chromium.base.IntentUtils;
import org.chromium.base.SysUtils;
import org.chromium.base.UserData;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.base.supplier.Supplier;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.IntentHandler;
import org.chromium.chrome.browser.browser_controls.BrowserControlsVisibilityManager;
import org.chromium.chrome.browser.browserservices.intents.BrowserServicesIntentDataProvider.CustomTabsUiType;
import org.chromium.chrome.browser.customtabs.CustomTabActivity;
import org.chromium.chrome.browser.customtabs.CustomTabIntentDataProvider;
import org.chromium.chrome.browser.customtabs.IncognitoCustomTabIntentDataProvider;
import org.chromium.chrome.browser.document.ChromeLauncherActivity;
import org.chromium.chrome.browser.dom_distiller.TabDistillabilityProvider.DistillabilityObserver;
import org.chromium.chrome.browser.flags.ChromeSwitches;
import org.chromium.chrome.browser.fullscreen.BrowserControlsManager;
import org.chromium.chrome.browser.fullscreen.BrowserControlsManagerSupplier;
import org.chromium.chrome.browser.fullscreen.FullscreenManager;
import org.chromium.chrome.browser.preferences.Pref;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.tab.EmptyTabObserver;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tab.Tab.LoadUrlResult;
import org.chromium.chrome.browser.tab.TabHidingType;
import org.chromium.chrome.browser.tab.TabSelectionType;
import org.chromium.chrome.browser.tab.TabUtils;
import org.chromium.components.dom_distiller.core.DomDistillerUrlUtils;
import org.chromium.components.messages.DismissReason;
import org.chromium.components.messages.MessageBannerProperties;
import org.chromium.components.messages.MessageDispatcher;
import org.chromium.components.messages.MessageDispatcherProvider;
import org.chromium.components.messages.MessageIdentifier;
import org.chromium.components.messages.MessageScopeType;
import org.chromium.components.messages.PrimaryActionClickBehavior;
import org.chromium.components.navigation_interception.InterceptNavigationDelegate;
import org.chromium.components.user_prefs.UserPrefs;
import org.chromium.content_public.browser.LoadCommittedDetails;
import org.chromium.content_public.browser.LoadUrlParams;
import org.chromium.content_public.browser.NavigationController;
import org.chromium.content_public.browser.NavigationEntry;
import org.chromium.content_public.browser.NavigationHandle;
import org.chromium.content_public.browser.WebContents;
import org.chromium.content_public.browser.WebContentsObserver;
import org.chromium.ui.base.WindowAndroid;
import org.chromium.ui.modelutil.PropertyModel;
import org.chromium.ui.util.ColorUtils;
import org.chromium.url.GURL;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.LinkedHashSet;
/**
* Manages UI effects for reader mode including hiding and showing the
* reader mode and reader mode preferences toolbar icon and hiding the
* browser controls when a reader mode page has finished loading.
*/
public class ReaderModeManager extends EmptyTabObserver implements UserData {
/** Possible states that the distiller can be in on a web page. */
@IntDef({
DistillationStatus.POSSIBLE,
DistillationStatus.NOT_POSSIBLE,
DistillationStatus.STARTED
})
@Retention(RetentionPolicy.SOURCE)
public @interface DistillationStatus {
/** POSSIBLE means reader mode can be entered. */
int POSSIBLE = 0;
/** NOT_POSSIBLE means reader mode cannot be entered. */
int NOT_POSSIBLE = 1;
/** STARTED means reader mode is currently in reader mode. */
int STARTED = 2;
}
/**
* Conditions under which the Reader Mode prompt was dismissed in conjunction with the
* accessibility setting.
*
* <p>Note: These values are persisted to logs. Entries should not be renumbered and numeric
* values should never be reused.
*/
// LINT.IfChange(MessageDismissalCondition)
@IntDef({
MessageDismissalCondition.ACCEPTED_WITH_ACCESSIBILITY_SETTING_SELECTED,
MessageDismissalCondition.ACCEPTED_WITH_ACCESSIBILITY_SETTING_DESELECTED,
MessageDismissalCondition.IGNORED_WITH_ACCESSIBILITY_SETTING_SELECTED,
MessageDismissalCondition.IGNORED_WITH_ACCESSIBILITY_SETTING_DESELECTED,
MessageDismissalCondition.NUM_ENTRIES
})
@Retention(RetentionPolicy.SOURCE)
public @interface MessageDismissalCondition {
int ACCEPTED_WITH_ACCESSIBILITY_SETTING_SELECTED = 0;
int ACCEPTED_WITH_ACCESSIBILITY_SETTING_DESELECTED = 1;
int IGNORED_WITH_ACCESSIBILITY_SETTING_SELECTED = 2;
int IGNORED_WITH_ACCESSIBILITY_SETTING_DESELECTED = 3;
// Number of entries
int NUM_ENTRIES = 4;
}
// LINT.ThenChange(/tools/metrics/histograms/metadata/accessibility/enums.xml:ReaderModeMessageDismissalCondition)
/** The key to access this object from a {@Tab}. */
public static final Class<ReaderModeManager> USER_DATA_KEY = ReaderModeManager.class;
/** The scheme used to access DOM-Distiller. */
public static final String DOM_DISTILLER_SCHEME = "chrome-distiller";
/** The intent extra that indicates origin from Reader Mode */
public static final String EXTRA_READER_MODE_PARENT =
"org.chromium.chrome.browser.dom_distiller.EXTRA_READER_MODE_PARENT";
/** The url of the last page visited if the last page was reader mode page. Otherwise null. */
private GURL mReaderModePageUrl;
/** Whether the fact that the current web page was distillable or not has been recorded. */
private boolean mIsUmaRecorded;
/** The WebContentsObserver responsible for updates to the distillation status of the tab. */
private WebContentsObserver mWebContentsObserver;
/** The distillation status of the tab. */
@DistillationStatus private int mDistillationStatus;
/** If the prompt was dismissed by the user. */
private boolean mIsDismissed;
/**
* The URL that distiller is using for this tab. This is used to check if a result comes back
* from distiller and the user has already loaded a new URL.
*/
private GURL mDistillerUrl;
/** Used to flag that the prompt was shown and recorded by UMA. */
private boolean mShowPromptRecorded;
/** Whether or not the current tab is a Reader Mode page. */
private boolean mIsViewingReaderModePage;
/** The time that the user started viewing Reader Mode content. */
private long mViewStartTimeMs;
/** The distillability observer attached to the tab. */
private DistillabilityObserver mDistillabilityObserver;
/** Whether this manager and tab have been destroyed. */
private boolean mIsDestroyed;
/** The tab this manager is attached to. */
private final Tab mTab;
/** The supplier of MessageDispatcher to display the message. */
private final Supplier<MessageDispatcher> mMessageDispatcherSupplier;
// Hold on to the InterceptNavigationDelegate that the custom tab uses.
InterceptNavigationDelegate mCustomTabNavigationDelegate;
/** Whether the messages UI was requested for a navigation. */
private boolean mMessageRequestedForNavigation;
// Record the sites which users refuse to view in reader mode.
// If the size is larger than the capacity, remove the earliest added site first.
private static final LinkedHashSet<Integer> sMutedSites = new LinkedHashSet<>();
private static final int MAX_SIZE_OF_DECLINED_SITES = 100;
/** Whether the message ui is being shown or has already been shown. */
private boolean mMessageShown;
/** Property Model of Reader mode message. */
private PropertyModel mMessageModel;
ReaderModeManager(Tab tab, Supplier<MessageDispatcher> messageDispatcherSupplier) {
super();
mTab = tab;
mTab.addObserver(this);
mMessageDispatcherSupplier = messageDispatcherSupplier;
}
/**
* Create an instance of the {@link ReaderModeManager} for the provided tab.
* @param tab The tab that will have a manager instance attached to it.
*/
public static void createForTab(Tab tab) {
tab.getUserDataHost()
.setUserData(
USER_DATA_KEY,
new ReaderModeManager(
tab, () -> MessageDispatcherProvider.from(tab.getWindowAndroid())));
}
/** Clear the status map and references to other objects. */
@Override
public void destroy() {
if (mWebContentsObserver != null) mWebContentsObserver.destroy();
mIsDestroyed = true;
}
@Override
public void onLoadUrl(Tab tab, LoadUrlParams params, LoadUrlResult loadUrlResult) {
// If a distiller URL was loaded and this is a custom tab, add a navigation
// handler to bring any navigations back to the main chrome activity.
Activity activity = TabUtils.getActivity(tab);
int uiType = CustomTabsUiType.DEFAULT;
if (activity != null && activity.getIntent().getExtras() != null) {
uiType =
activity.getIntent()
.getExtras()
.getInt(CustomTabIntentDataProvider.EXTRA_UI_TYPE);
}
if (tab == null
|| uiType != CustomTabsUiType.READER_MODE
|| !DomDistillerUrlUtils.isDistilledPage(params.getUrl())) {
return;
}
WebContents webContents = tab.getWebContents();
if (webContents == null) return;
mCustomTabNavigationDelegate =
new InterceptNavigationDelegate() {
@Override
public boolean shouldIgnoreNavigation(
NavigationHandle navigationHandle,
GURL escapedUrl,
boolean hiddenCrossFrame,
boolean isSandboxedFrame) {
if (DomDistillerUrlUtils.isDistilledPage(navigationHandle.getUrl())
|| navigationHandle.isExternalProtocol()) {
return false;
}
Intent returnIntent =
new Intent(Intent.ACTION_VIEW, Uri.parse(escapedUrl.getSpec()));
returnIntent.setClassName(activity, ChromeLauncherActivity.class.getName());
// Set the parent ID of the tab to be created.
returnIntent.putExtra(
EXTRA_READER_MODE_PARENT,
IntentUtils.safeGetInt(
activity.getIntent().getExtras(),
EXTRA_READER_MODE_PARENT,
Tab.INVALID_TAB_ID));
activity.startActivity(returnIntent);
activity.finish();
return true;
}
};
DomDistillerTabUtils.setInterceptNavigationDelegate(
mCustomTabNavigationDelegate, webContents);
}
@Override
public void onShown(Tab shownTab, @TabSelectionType int type) {
// If the reader mode prompt was dismissed, stop here.
if (mIsDismissed) return;
mDistillationStatus = DistillationStatus.NOT_POSSIBLE;
mDistillerUrl = shownTab.getUrl();
if (mDistillabilityObserver == null) setDistillabilityObserver(shownTab);
if (DomDistillerUrlUtils.isDistilledPage(shownTab.getUrl()) && !mIsViewingReaderModePage) {
onStartedReaderMode();
}
// Make sure there is a WebContentsObserver on this tab's WebContents.
if (mWebContentsObserver == null && mTab.getWebContents() != null) {
mWebContentsObserver = createWebContentsObserver();
}
tryShowingPrompt();
}
@Override
public void onHidden(Tab tab, @TabHidingType int reason) {
if (mIsViewingReaderModePage) {
long timeMs = onExitReaderMode();
recordReaderModeViewDuration(timeMs);
}
}
@Override
public void onDestroyed(Tab tab) {
if (tab == null) return;
// If the prompt was not shown for the previous navigation, record it now.
if (!mShowPromptRecorded) {
recordPromptVisibilityForNavigation(false);
}
if (mIsViewingReaderModePage) {
long timeMs = onExitReaderMode();
recordReaderModeViewDuration(timeMs);
}
TabDistillabilityProvider.get(tab).removeObserver(mDistillabilityObserver);
removeTabState();
}
@Override
public void onActivityAttachmentChanged(Tab tab, @Nullable WindowAndroid window) {
// Intentionally do nothing to prevent automatic observer removal on detachment.
}
/** Clear the reader mode state for this manager. */
private void removeTabState() {
if (mWebContentsObserver != null) mWebContentsObserver.destroy();
mDistillationStatus = DistillationStatus.POSSIBLE;
mIsDismissed = false;
mMessageRequestedForNavigation = false;
mDistillerUrl = null;
mShowPromptRecorded = false;
mIsViewingReaderModePage = false;
mDistillabilityObserver = null;
}
@Override
public void onContentChanged(Tab tab) {
// If the content change was because of distiller switching web contents or Reader Mode has
// already been dismissed for this tab do nothing.
if (mIsDismissed && !DomDistillerUrlUtils.isDistilledPage(tab.getUrl())) return;
// If the tab state already existed, only reset the relevant data. Things like view duration
// need to be preserved.
mDistillationStatus = DistillationStatus.NOT_POSSIBLE;
mDistillerUrl = tab.getUrl();
if (tab.getWebContents() != null) {
mWebContentsObserver = createWebContentsObserver();
if (DomDistillerUrlUtils.isDistilledPage(tab.getUrl())) {
mDistillationStatus = DistillationStatus.STARTED;
mReaderModePageUrl = tab.getUrl();
}
}
}
/** A notification that the user started viewing Reader Mode. */
private void onStartedReaderMode() {
mIsViewingReaderModePage = true;
mViewStartTimeMs = SystemClock.elapsedRealtime();
}
/**
* A notification that the user is no longer viewing Reader Mode. This could be because of a
* navigation away from the page, switching tabs, or closing the browser.
* @return The amount of time in ms that the user spent viewing Reader Mode.
*/
private long onExitReaderMode() {
mIsViewingReaderModePage = false;
return SystemClock.elapsedRealtime() - mViewStartTimeMs;
}
/**
* Record if the prompt became visible on the current page. This can be overridden for testing.
* @param visible If the prompt was visible at any time.
*/
private void recordPromptVisibilityForNavigation(boolean visible) {
RecordHistogram.recordBooleanHistogram("DomDistiller.ReaderShownForPageLoad", visible);
}
/** A notification that the prompt was dismissed without being used. */
public void onClosed() {
mIsDismissed = true;
}
/**
* Records the conditions under which the Reader Mode message was dismissed.
* @param dismissReason The message dismissal reason.
*/
public void recordDismissalConditions(@DismissReason int dismissReason) {
if (mTab == null) return;
Profile profile = mTab.getProfile();
boolean a11ySettingSelected =
UserPrefs.get(profile).getBoolean(Pref.READER_FOR_ACCESSIBILITY);
if (dismissReason == DismissReason.PRIMARY_ACTION) {
RecordHistogram.recordEnumeratedHistogram(
"DomDistiller.MessageDismissalCondition",
a11ySettingSelected
? MessageDismissalCondition.ACCEPTED_WITH_ACCESSIBILITY_SETTING_SELECTED
: MessageDismissalCondition
.ACCEPTED_WITH_ACCESSIBILITY_SETTING_DESELECTED,
MessageDismissalCondition.NUM_ENTRIES);
} else {
RecordHistogram.recordEnumeratedHistogram(
"DomDistiller.MessageDismissalCondition",
a11ySettingSelected
? MessageDismissalCondition.IGNORED_WITH_ACCESSIBILITY_SETTING_SELECTED
: MessageDismissalCondition
.IGNORED_WITH_ACCESSIBILITY_SETTING_DESELECTED,
MessageDismissalCondition.NUM_ENTRIES);
}
}
private WebContentsObserver createWebContentsObserver() {
return new WebContentsObserver(mTab.getWebContents()) {
/** Whether or not the previous navigation should be removed. */
private boolean mShouldRemovePreviousNavigation;
/** The index of the last committed distiller page in history. */
private int mLastDistillerPageIndex;
@Override
public void didStartNavigationInPrimaryMainFrame(NavigationHandle navigation) {
if (navigation.isSameDocument()) return;
// Reader Mode should not pollute the navigation stack. To avoid this, watch for
// navigations and prepare to remove any that are "chrome-distiller" urls.
NavigationController controller = mWebContents.get().getNavigationController();
int index = controller.getLastCommittedEntryIndex();
NavigationEntry entry = controller.getEntryAtIndex(index);
if (entry != null && DomDistillerUrlUtils.isDistilledPage(entry.getUrl())) {
mShouldRemovePreviousNavigation = true;
mLastDistillerPageIndex = index;
}
if (mIsDestroyed) return;
mDistillerUrl = navigation.getUrl();
if (DomDistillerUrlUtils.isDistilledPage(navigation.getUrl())) {
mDistillationStatus = DistillationStatus.STARTED;
mReaderModePageUrl = navigation.getUrl();
}
}
@Override
public void didFinishNavigationInPrimaryMainFrame(NavigationHandle navigation) {
// TODO(cjhopman): This should possibly ignore navigations that replace the entry
// (like those from history.replaceState()).
if (!navigation.hasCommitted() || navigation.isSameDocument()) {
return;
}
if (mShouldRemovePreviousNavigation) {
mShouldRemovePreviousNavigation = false;
NavigationController controller = mWebContents.get().getNavigationController();
if (controller.getEntryAtIndex(mLastDistillerPageIndex) != null) {
controller.removeEntryAtIndex(mLastDistillerPageIndex);
}
}
if (mIsDestroyed) return;
mDistillationStatus = DistillationStatus.POSSIBLE;
if (mReaderModePageUrl == null
|| !navigation
.getUrl()
.equals(
DomDistillerUrlUtils.getOriginalUrlFromDistillerUrl(
mReaderModePageUrl))) {
mDistillationStatus = DistillationStatus.NOT_POSSIBLE;
mIsUmaRecorded = false;
}
mReaderModePageUrl = null;
if (mDistillationStatus == DistillationStatus.POSSIBLE) tryShowingPrompt();
}
@Override
public void navigationEntryCommitted(LoadCommittedDetails details) {
if (mIsDestroyed) return;
// Reset closed state of reader mode in this tab once we know a navigation is
// happening.
mIsDismissed = false;
mMessageRequestedForNavigation = false;
// If the prompt was not shown for the previous navigation, record it now.
if (mTab != null && !mTab.isNativePage() && !mTab.isBeingRestored()) {
recordPromptVisibilityForNavigation(false);
}
mShowPromptRecorded = false;
if (mTab != null
&& !DomDistillerUrlUtils.isDistilledPage(mTab.getUrl())
&& mIsViewingReaderModePage) {
long timeMs = onExitReaderMode();
recordReaderModeViewDuration(timeMs);
}
}
};
}
/**
* Record the amount of time the user spent in Reader Mode.
* @param timeMs The amount of time in ms that the user spent in Reader Mode.
*/
private void recordReaderModeViewDuration(long timeMs) {
RecordHistogram.recordLongTimesHistogram("DomDistiller.Time.ViewingReaderModePage", timeMs);
}
/** Try showing the reader mode prompt. */
@VisibleForTesting
void tryShowingPrompt() {
if (mTab == null || mTab.getWebContents() == null) return;
// This prompt should only be shown on incognito or custom tabs, in other cases we'll show a
// toolbar button (contextual page action) instead.
if (!mTab.isCustomTab() && !mTab.isIncognito()) return;
// Test if the user is requesting the desktop site. Ignore this if distiller is set to
// ALWAYS_TRUE.
boolean usingRequestDesktopSite =
mTab.getWebContents().getNavigationController().getUseDesktopUserAgent()
&& !DomDistillerTabUtils.isHeuristicAlwaysTrue();
if (usingRequestDesktopSite
|| mDistillationStatus != DistillationStatus.POSSIBLE
|| mIsDismissed) {
return;
}
if (sMutedSites.contains(urlToHash(mDistillerUrl))) {
return;
}
MessageDispatcher messageDispatcher = mMessageDispatcherSupplier.get();
if (messageDispatcher != null) {
if (!mMessageRequestedForNavigation) {
// If feature is disabled, reader mode message ui is only shown once per tab.
if (mMessageShown) {
return;
}
showReaderModeMessage(messageDispatcher);
mMessageShown = true;
}
mMessageRequestedForNavigation = true;
}
}
private void showReaderModeMessage(MessageDispatcher messageDispatcher) {
if (mMessageModel != null) {
// It is safe to dismiss a message which has been dismissed previously.
messageDispatcher.dismissMessage(mMessageModel, DismissReason.DISMISSED_BY_FEATURE);
}
Resources resources = mTab.getContext().getResources();
// Save url for #onMessageDismissed. mDistillerUrl may have been changed and became
// different from the url when message is enqueued.
GURL url = mDistillerUrl;
mMessageModel =
new PropertyModel.Builder(MessageBannerProperties.ALL_KEYS)
.with(
MessageBannerProperties.MESSAGE_IDENTIFIER,
MessageIdentifier.READER_MODE)
.with(
MessageBannerProperties.TITLE,
resources.getString(R.string.reader_mode_message_title))
.with(
MessageBannerProperties.ICON_RESOURCE_ID,
R.drawable.ic_mobile_friendly)
.with(
MessageBannerProperties.PRIMARY_BUTTON_TEXT,
resources.getString(R.string.reader_mode_message_button))
.with(
MessageBannerProperties.ON_PRIMARY_ACTION,
() -> {
activateReaderMode();
return PrimaryActionClickBehavior.DISMISS_IMMEDIATELY;
})
.with(
MessageBannerProperties.ON_DISMISSED,
(reason) -> onMessageDismissed(url, reason))
.build();
messageDispatcher.enqueueMessage(
mMessageModel, mTab.getWebContents(), MessageScopeType.NAVIGATION, false);
}
private void onMessageDismissed(GURL url, @DismissReason int dismissReason) {
mMessageModel = null;
if (dismissReason == DismissReason.GESTURE) {
onClosed();
}
recordDismissalConditions(dismissReason);
if (dismissReason != DismissReason.PRIMARY_ACTION) {
addUrlToMutedSites(url);
}
}
private void addUrlToMutedSites(GURL url) {
sMutedSites.add(urlToHash(url));
while (sMutedSites.size() > MAX_SIZE_OF_DECLINED_SITES) {
int v = sMutedSites.iterator().next();
sMutedSites.remove(v);
}
}
private void removeUrlFromMutedSites(GURL url) {
sMutedSites.remove(urlToHash(url));
}
public void activateReaderMode() {
// Contextual page action buttons can't be dismissed, instead we consider a shown but unused
// button as "dismissed" and mute the site on setReaderModeUiShown(). When the button gets
// clicked we un-mute the site to prevent the rate limiting logic from showing the CPA
// button for this site on other tabs.
removeUrlFromMutedSites(mDistillerUrl);
if (DomDistillerTabUtils.isCctMode() && !SysUtils.isLowEndDevice()) {
distillInCustomTab();
} else {
navigateToReaderMode();
}
}
/** Navigate the current tab to a Reader Mode URL. */
private void navigateToReaderMode() {
WebContents webContents = mTab.getWebContents();
if (webContents == null) return;
onStartedReaderMode();
FullscreenManager fullscreenManager = getFullscreenManager();
if (fullscreenManager != null) {
// Make sure to exit fullscreen mode before navigating.
fullscreenManager.onExitFullscreen(mTab);
}
// RenderWidgetHostViewAndroid hides the controls after transitioning to reader mode.
// See the long history of the issue in https://crbug.com/825765, https://crbug.com/853686,
// https://crbug.com/861618, https://crbug.com/922388.
// TODO(pshmakov): find a proper solution instead of this workaround.
BrowserControlsVisibilityManager browserControlsVisibilityManager =
getBrowserControlsVisibilityManager();
if (browserControlsVisibilityManager != null) {
getBrowserControlsVisibilityManager()
.getBrowserVisibilityDelegate()
.showControlsTransient();
}
DomDistillerTabUtils.distillCurrentPageAndView(webContents);
}
private @Nullable BrowserControlsManager getBrowserControlsManager() {
return BrowserControlsManagerSupplier.getValueOrNullFrom(mTab.getWindowAndroid());
}
private @Nullable BrowserControlsVisibilityManager getBrowserControlsVisibilityManager() {
return getBrowserControlsManager();
}
private @Nullable FullscreenManager getFullscreenManager() {
BrowserControlsManager browserControlsManager = getBrowserControlsManager();
return browserControlsManager == null
? null
: browserControlsManager.getFullscreenManager();
}
private void distillInCustomTab() {
Activity activity = TabUtils.getActivity(mTab);
WebContents webContents = mTab.getWebContents();
if (webContents == null) return;
GURL url = webContents.getLastCommittedUrl();
onStartedReaderMode();
DomDistillerTabUtils.distillCurrentPage(webContents);
String distillerUrl =
DomDistillerUrlUtils.getDistillerViewUrlFromUrl(
DOM_DISTILLER_SCHEME, url.getSpec(), webContents.getTitle());
CustomTabsIntent.Builder builder = new CustomTabsIntent.Builder();
builder.setShowTitle(true);
builder.setColorScheme(
ColorUtils.inNightMode(activity)
? CustomTabsIntent.COLOR_SCHEME_DARK
: CustomTabsIntent.COLOR_SCHEME_LIGHT);
CustomTabsIntent customTabsIntent = builder.build();
customTabsIntent.intent.setClassName(activity, CustomTabActivity.class.getName());
// Customize items on menu as Reader Mode UI to show 'Find in page' and 'Preference' only.
CustomTabIntentDataProvider.addReaderModeUIExtras(customTabsIntent.intent);
// Add the parent ID as an intent extra for back button functionality.
customTabsIntent.intent.putExtra(EXTRA_READER_MODE_PARENT, mTab.getId());
// Use Incognito CCT if the source page is in Incognito mode.
if (mTab.isIncognito()) {
IncognitoCustomTabIntentDataProvider.addIncognitoExtrasForChromeFeatures(
customTabsIntent.intent, IntentHandler.IncognitoCCTCallerId.READER_MODE);
}
customTabsIntent.launchUrl(activity, Uri.parse(distillerUrl));
}
/**
* Set the observer for updating reader mode status based on whether or not the page should
* be viewed in reader mode.
* @param tabToObserve The tab to attach the observer to.
*/
private void setDistillabilityObserver(final Tab tabToObserve) {
mDistillabilityObserver =
(tab, isDistillable, isLast, isMobileOptimized) -> {
// Make sure the page didn't navigate while waiting for a response.
if (!tab.getUrl().equals(mDistillerUrl)) return;
if (isDistillable
&& !(isMobileOptimized
&& DomDistillerTabUtils.shouldExcludeMobileFriendly(
tabToObserve))) {
mDistillationStatus = DistillationStatus.POSSIBLE;
tryShowingPrompt();
} else {
mDistillationStatus = DistillationStatus.NOT_POSSIBLE;
}
if (!mIsUmaRecorded
&& (mDistillationStatus == DistillationStatus.POSSIBLE || isLast)) {
mIsUmaRecorded = true;
RecordHistogram.recordBooleanHistogram(
"DomDistiller.PageDistillable",
mDistillationStatus == DistillationStatus.POSSIBLE);
}
};
TabDistillabilityProvider.get(tabToObserve).addObserver(mDistillabilityObserver);
}
private int urlToHash(GURL url) {
return url.getHost().hashCode();
}
@VisibleForTesting
int getDistillationStatus() {
return mDistillationStatus;
}
void muteSiteForTesting(GURL url) {
sMutedSites.add(urlToHash(url));
}
void clearSavedSitesForTesting() {
sMutedSites.clear();
}
/** @return Whether Reader mode and its new UI are enabled. */
public static boolean isEnabled() {
boolean enabled =
CommandLine.getInstance().hasSwitch(ChromeSwitches.ENABLE_DOM_DISTILLER)
&& !CommandLine.getInstance()
.hasSwitch(ChromeSwitches.DISABLE_READER_MODE_BOTTOM_BAR)
&& DomDistillerTabUtils.isDistillerHeuristicsEnabled();
return enabled;
}
/**
* Determine if Reader Mode created the intent for a tab being created.
* @param intent The Intent creating a new tab.
* @return True whether the intent was created by Reader Mode.
*/
public static boolean isReaderModeCreatedIntent(@NonNull Intent intent) {
int readerParentId =
IntentUtils.safeGetInt(
intent.getExtras(),
ReaderModeManager.EXTRA_READER_MODE_PARENT,
Tab.INVALID_TAB_ID);
return readerParentId != Tab.INVALID_TAB_ID;
}
/**
* Notify that a reader mode UI was shown for the current tab and URL. Used when the contextual
* page action UI is enabled to update the rate limiting logic.
*/
public void setReaderModeUiShown() {
// Contextual page actions can't be dismissed, so we consider an unused button as
// "dismissed". Interacting with the button will undo this "mute" logic.
addUrlToMutedSites(mDistillerUrl);
mMessageShown = true;
}
}