chromium/chrome/android/feed/core/java/src/org/chromium/chrome/browser/feed/NtpFeedSurfaceLifecycleManager.java

// Copyright 2018 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.feed;

import android.app.Activity;

import androidx.annotation.Nullable;

import org.chromium.base.ResettersForTesting;
import org.chromium.chrome.browser.preferences.Pref;
import org.chromium.chrome.browser.tab.EmptyTabObserver;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tab.TabHidingType;
import org.chromium.chrome.browser.tab.TabObserver;
import org.chromium.chrome.browser.tab.TabSelectionType;
import org.chromium.components.embedder_support.util.UrlUtilities;
import org.chromium.components.prefs.PrefService;
import org.chromium.components.user_prefs.UserPrefs;
import org.chromium.content_public.browser.NavigationController;
import org.chromium.content_public.browser.NavigationEntry;
import org.chromium.url.GURL;

/**
 * Manages the lifecycle of a {@link FeedSurfaceCoordinator} associated with a Tab in an Activity.
 */
public class NtpFeedSurfaceLifecycleManager extends FeedSurfaceLifecycleManager {
    /** Key for the Feed instance state that may be stored in a navigation entry. */
    private static final String FEED_SAVED_INSTANCE_STATE_KEY = "FeedSavedInstanceState";

    private static PrefService sPrefServiceForTesting;

    /** The {@link Tab} that {@link #mCoordinator} is attached to. */
    private final Tab mTab;

    /**
     * The {@link TabObserver} that observes tab state changes and notifies the {@link
     * FeedSurfaceCoordinator} accordingly.
     */
    private final TabObserver mTabObserver;

    /**
     * @param activity The {@link Activity} that the {@link FeedSurfaceCoordinator} is attached to.
     * @param tab The {@link Tab} that the {@link FeedSurfaceCoordinator} is attached to.
     * @param coordinator The coordinator of the feed.
     */
    public NtpFeedSurfaceLifecycleManager(
            Activity activity, Tab tab, FeedSurfaceCoordinator coordinator) {
        super(activity, coordinator);

        // Set mTab before 'start' since 'restoreInstanceState' will use it.
        mTab = tab;
        start();

        // We don't need to handle EmptyTabObserver#onDestroy here since this class will be
        // destroyed when the associated NewTabPage is destroyed.
        mTabObserver =
                new EmptyTabObserver() {
                    @Override
                    public void onInteractabilityChanged(Tab tab, boolean isInteractable) {
                        if (isInteractable) {
                            show();
                        }
                    }

                    @Override
                    public void onShown(Tab tab, @TabSelectionType int type) {
                        show();
                    }

                    @Override
                    public void onHidden(Tab tab, @TabHidingType int type) {
                        hide();
                    }

                    @Override
                    public void onPageLoadStarted(Tab tab, GURL url) {
                        saveInstanceState();
                    }
                };
        mTab.addObserver(mTabObserver);
    }

    /** @return Whether the {@link FeedSurfaceCoordinator} can be shown. */
    @Override
    protected boolean canShow() {
        // We don't call FeedSurfaceCoordinator#onShow to prevent feed services from being warmed up
        // if the user has opted out from article suggestions during the previous session.
        return super.canShow()
                && getPrefService().getBoolean(Pref.ARTICLES_LIST_VISIBLE)
                && !mTab.isHidden();
    }

    /**
     * Clears any dependencies when this class is not needed
     * anymore.
     */
    @Override
    protected void destroy() {
        if (mSurfaceState == SurfaceState.DESTROYED) return;

        super.destroy();
        mTab.removeObserver(mTabObserver);
    }

    /** Save the Feed instance state to the navigation entry if necessary. */
    @Override
    protected void saveInstanceState() {
        if (mTab.getWebContents() == null) return;

        NavigationController controller = mTab.getWebContents().getNavigationController();
        int index = controller.getLastCommittedEntryIndex();
        NavigationEntry entry = controller.getEntryAtIndex(index);
        if (entry == null) return;

        // At least under test conditions this method may be called initially for the load of the
        // NTP itself, at which point the last committed entry is not for the NTP yet. This method
        // will then be called a second time when the user navigates away, at which point the last
        // committed entry is for the NTP. The extra data must only be set in the latter case.
        if (!UrlUtilities.isNtpUrl(entry.getUrl())) return;

        controller.setEntryExtraData(
                index, FEED_SAVED_INSTANCE_STATE_KEY, mCoordinator.getSavedInstanceStateString());
    }

    /**
     * @return The feed instance state saved in navigation entry, or null if it is not previously
     *         saved.
     */
    @Override
    protected @Nullable String restoreInstanceState() {
        if (mTab.getWebContents() == null) return null;

        NavigationController controller = mTab.getWebContents().getNavigationController();
        int index = controller.getLastCommittedEntryIndex();
        return controller.getEntryExtraData(index, FEED_SAVED_INSTANCE_STATE_KEY);
    }

    TabObserver getTabObserverForTesting() {
        return mTabObserver;
    }

    private PrefService getPrefService() {
        if (sPrefServiceForTesting != null) return sPrefServiceForTesting;
        return UserPrefs.get(mTab.getProfile());
    }

    static void setPrefServiceForTesting(PrefService prefServiceForTesting) {
        sPrefServiceForTesting = prefServiceForTesting;
        ResettersForTesting.register(() -> sPrefServiceForTesting = null);
    }
}