chromium/chrome/android/java/src/org/chromium/chrome/browser/dom_distiller/TabDistillabilityProvider.java

// Copyright 2019 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 androidx.annotation.IntDef;
import androidx.annotation.Nullable;

import org.chromium.base.ObserverList;
import org.chromium.base.UserData;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.chrome.browser.tab.EmptyTabObserver;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.components.dom_distiller.content.DistillablePageUtils;
import org.chromium.components.dom_distiller.content.DistillablePageUtils.PageDistillableDelegate;
import org.chromium.content_public.browser.NavigationHandle;
import org.chromium.content_public.browser.WebContents;
import org.chromium.ui.base.WindowAndroid;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

/** A mechanism for clients interested in the distillability of a page to receive updates. */
public class TabDistillabilityProvider extends EmptyTabObserver
        implements PageDistillableDelegate, UserData {
    public static final Class<TabDistillabilityProvider> USER_DATA_KEY =
            TabDistillabilityProvider.class;

    // These values are persisted to logs. Entries should not be renumbered and
    // numeric values should never be reused.
    @IntDef({
        ContentClassification.OTHER,
        ContentClassification.LONG_ARTICLE,
        ContentClassification.COUNT
    })
    @Retention(RetentionPolicy.SOURCE)
    public @interface ContentClassification {
        int OTHER = 0;
        int LONG_ARTICLE = 1;
        int COUNT = 2;
    };

    /** An observer of the distillable state of a tab and its active web content. */
    public interface DistillabilityObserver {
        /**
         * Called when the distillability status changes.
         * @param tab The tab the event was triggered for.
         * @param isDistillable Whether the page is distillable.
         * @param isLast Whether the update is the last one for this page.
         * @param isMobileOptimized Whether the page is optimized for mobile. Only valid when
         *                          the heuristics is ADABOOST_MODEL or ALL_ARTICLES.
         */
        void onIsPageDistillableResult(
                Tab tab, boolean isDistillable, boolean isLast, boolean isMobileOptimized);
    }

    /** The list of observers to propagate events to. */
    private final ObserverList<DistillabilityObserver> mObserverList;

    /** The tab this provider represents. */
    private Tab mTab;

    /** Whether the distillability has been determined for the tab in its current state. */
    private boolean mDistillabilityDetermined;

    /** The last web contents that the distillability delegate was attached to. */
    private WebContents mWebContents;

    /** Cached results from the last result from native. */
    private boolean mIsDistillable;

    private boolean mIsLast;
    private boolean mIsLongArticle;
    private boolean mIsMobileOptimized;

    public static void createForTab(Tab tab) {
        assert get(tab) == null;
        tab.getUserDataHost().setUserData(USER_DATA_KEY, new TabDistillabilityProvider(tab));
    }

    public static @Nullable TabDistillabilityProvider get(Tab tab) {
        return tab.getUserDataHost().getUserData(USER_DATA_KEY);
    }

    private TabDistillabilityProvider(Tab tab) {
        mTab = tab;
        mObserverList = new ObserverList<>();
        resetState();
        mTab.addObserver(this);
    }

    /**
     * Add an observer of distillability updates for this helper.
     * @param observer The observer to add.
     */
    public void addObserver(DistillabilityObserver observer) {
        mObserverList.addObserver(observer);
    }

    /**
     * Remove an observer of distillability updates for this helper.
     * @param observer The observer to remove.
     */
    public void removeObserver(DistillabilityObserver observer) {
        mObserverList.removeObserver(observer);
    }

    /**
     * @return Whether the web content has provided a signal about disillability for the current
     *         page.
     */
    public boolean isDistillabilityDetermined() {
        return mDistillabilityDetermined;
    }

    /** @return Whether the current page is considered distillable. */
    public boolean isDistillable() {
        return mIsDistillable;
    }

    /** @return Whether the last signal has been received from the web content. */
    public boolean isLast() {
        return mIsLast;
    }

    /** @return Whether the current page is considered to be mobile optimized. */
    public boolean isMobileOptimized() {
        return mIsMobileOptimized;
    }

    /**
     * Reset any of the cached values from native distiller and reattach the delegate if necessary.
     */
    private void resetState() {
        mDistillabilityDetermined = false;
        mIsDistillable = false;
        mIsLast = false;
        mIsLongArticle = false;
        mIsMobileOptimized = false;

        if (mTab != null
                && mTab.getWebContents() != null
                && mTab.getWebContents() != mWebContents) {
            mWebContents = mTab.getWebContents();
            DistillablePageUtils.setDelegate(mWebContents, this);
        }
    }

    /** Records the Content.Classification metric if the distillability has been determined. */
    private void recordContentClassificationMetric() {
        // If the distillability was determined, record the Content Classification. Should be called
        // before #resetState().
        if (isDistillabilityDetermined()) {
            RecordHistogram.recordEnumeratedHistogram(
                    "Content.Classification",
                    mIsLongArticle
                            ? ContentClassification.LONG_ARTICLE
                            : ContentClassification.OTHER,
                    ContentClassification.COUNT);
        }
    }

    @Override
    public void onIsPageDistillableResult(
            boolean isDistillable,
            boolean isLast,
            boolean isLongArticle,
            boolean isMobileOptimized) {
        mIsDistillable = isDistillable;
        mIsLast = isLast;
        mIsLongArticle = isLongArticle;
        mIsMobileOptimized = isMobileOptimized;

        mDistillabilityDetermined = true;

        for (DistillabilityObserver o : mObserverList) {
            o.onIsPageDistillableResult(mTab, mIsDistillable, mIsLast, mIsMobileOptimized);
        }
    }

    @Override
    public void onContentChanged(Tab tab) {
        recordContentClassificationMetric();
        resetState();
    }

    @Override
    public void onActivityAttachmentChanged(Tab tab, @Nullable WindowAndroid window) {
        if (window != null) return;
        resetState();
    }

    @Override
    public void onDidFinishNavigationInPrimaryMainFrame(Tab tab, NavigationHandle navigation) {
        recordContentClassificationMetric();
        resetState();
    }

    @Override
    public void destroy() {
        mObserverList.clear();
        mTab.removeObserver(this);
        mTab = null;
        mWebContents = null;
        resetState();
    }
}