chromium/chrome/android/java/src/org/chromium/chrome/browser/usage_stats/SuspendedTab.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.usage_stats;

import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup.LayoutParams;
import android.widget.TextView;

import androidx.annotation.ColorInt;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;

import org.chromium.base.ContextUtils;
import org.chromium.base.Log;
import org.chromium.base.UserData;
import org.chromium.base.supplier.Supplier;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.infobar.InfoBarContainer;
import org.chromium.chrome.browser.media.MediaCaptureDevicesDispatcherAndroid;
import org.chromium.chrome.browser.tab.EmptyTabObserver;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tab.TabViewProvider;
import org.chromium.chrome.browser.tab_ui.TabContentManager;
import org.chromium.components.browser_ui.styles.SemanticColorUtils;
import org.chromium.content_public.browser.WebContents;
import org.chromium.ui.base.WindowAndroid;

/**
 * Represents the suspension page presented when a user tries to visit a site whose fully-qualified
 * domain name (FQDN) has been suspended via Digital Wellbeing.
 */
public class SuspendedTab extends EmptyTabObserver implements UserData, TabViewProvider {
    private static final String DIGITAL_WELLBEING_SITE_DETAILS_ACTION =
            "org.chromium.chrome.browser.usage_stats.action.SHOW_WEBSITE_DETAILS";
    private static final String EXTRA_FQDN_NAME =
            "org.chromium.chrome.browser.usage_stats.extra.FULLY_QUALIFIED_DOMAIN_NAME";
    private static final String TAG = "SuspendedTab";
    private static final Class<SuspendedTab> USER_DATA_KEY = SuspendedTab.class;

    public static boolean isShowing(Tab tab) {
        if (tab == null || !tab.isInitialized()) return false;
        SuspendedTab suspendedTab = get(tab);
        return suspendedTab != null && suspendedTab.isShowing();
    }

    /**
     * @return The SuspendedTab instance for the given Tab object. This can never return null, but
     *         is not safe to call if the tab has been destroyed.
     */
    public static SuspendedTab from(
            Tab tab, Supplier<TabContentManager> tabContentManagerSupplier) {
        assert tab.isInitialized();
        SuspendedTab suspendedTab = get(tab);
        if (suspendedTab == null) {
            suspendedTab =
                    tab.getUserDataHost()
                            .setUserData(
                                    USER_DATA_KEY,
                                    new SuspendedTab(tab, tabContentManagerSupplier));
        }
        return suspendedTab;
    }

    public static SuspendedTab get(Tab tab) {
        return tab.getUserDataHost().getUserData(USER_DATA_KEY);
    }

    @Override
    public @ColorInt int getBackgroundColor(Context context) {
        return SemanticColorUtils.getDefaultBgColor(context);
    }

    private final Tab mTab;
    private final Supplier<TabContentManager> mTabContentManagerSupplier;
    private View mView;
    private String mFqdn;

    private SuspendedTab(Tab tab, Supplier<TabContentManager> tabContentManagerSupplier) {
        mTab = tab;
        mTabContentManagerSupplier = tabContentManagerSupplier;
    }

    /**
     * Show the suspended tab UI within the root view of the associated tab. This will stop loading
     * of mTab so that the page is not also rendered. If the suspended tab is already showing, this
     * will update its fqdn to the given one.
     */
    public void show(String fqdn) {
        mFqdn = fqdn;
        mTab.addObserver(this);
        mTab.stopLoading();

        WebContents webContents = mTab.getWebContents();
        if (webContents != null) {
            webContents.onHide();
            webContents.suspendAllMediaPlayers();
            webContents.setAudioMuted(true);
            if (MediaCaptureDevicesDispatcherAndroid.isCapturingAudio(webContents)
                    || MediaCaptureDevicesDispatcherAndroid.isCapturingVideo(webContents)
                    || MediaCaptureDevicesDispatcherAndroid.isCapturingScreen(webContents)) {
                MediaCaptureDevicesDispatcherAndroid.notifyStopped(webContents);
            }
        }

        InfoBarContainer infoBarContainer = InfoBarContainer.get(mTab);
        if (infoBarContainer != null) {
            infoBarContainer.setHidden(true);
        }

        if (isViewAttached()) {
            updateFqdnText();
        } else {
            attachView();
        }

        TabContentManager tabContentManager = mTabContentManagerSupplier.get();
        if (tabContentManager != null) {
            // We have to wait for the view to layout to cache a new thumbnail for it; otherwise,
            // its width and height won't be available yet.
            mView.post(
                    () -> {
                        tabContentManager.removeTabThumbnail(mTab.getId());
                        tabContentManager.cacheTabThumbnail(mTab);
                    });
        }
    }

    /** Remove the suspended tab UI if it's currently being shown. */
    public void removeIfPresent() {
        removeViewIfPresent();

        WebContents webContents = mTab.getWebContents();
        if (webContents != null) {
            webContents.onShow();
            webContents.setAudioMuted(false);
        }

        mView = null;
        mFqdn = null;
    }

    /** @return the fqdn this SuspendedTab is currently showing for; null if not showing. */
    public String getFqdn() {
        return mFqdn;
    }

    /** @return Whether this SuspendedTab is currently showing. */
    public boolean isShowing() {
        return mFqdn != null;
    }

    @VisibleForTesting
    boolean isViewAttached() {
        return mView != null && mTab.getTabViewManager().isShowing(this);
    }

    private View createView() {
        Context context = mTab.getContext();
        LayoutInflater inflater = LayoutInflater.from(context);

        View suspendedTabView = inflater.inflate(R.layout.suspended_tab, null);
        suspendedTabView.setLayoutParams(
                new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
        return suspendedTabView;
    }

    private void attachView() {
        assert mView == null;

        mView = createView();
        mTab.getTabViewManager().addTabViewProvider(this);
        updateFqdnText();
    }

    private void updateFqdnText() {
        Context context = mTab.getContext();
        TextView explanationText = mView.findViewById(R.id.suspended_tab_explanation);
        explanationText.setText(
                context.getString(R.string.usage_stats_site_paused_explanation, mFqdn));
        setSettingsLinkClickListener();
    }

    private void setSettingsLinkClickListener() {
        Context context = mTab.getContext();
        View settingsLink = mView.findViewById(R.id.suspended_tab_settings_button);
        settingsLink.setOnClickListener(
                new OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        Intent intent = new Intent(DIGITAL_WELLBEING_SITE_DETAILS_ACTION);
                        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                        intent.putExtra(EXTRA_FQDN_NAME, mFqdn);
                        intent.putExtra(
                                Intent.EXTRA_PACKAGE_NAME,
                                ContextUtils.getApplicationContext().getPackageName());
                        try {
                            context.startActivity(intent);
                        } catch (ActivityNotFoundException e) {
                            Log.e(TAG, "No activity found for site details intent", e);
                        }
                    }
                });
    }

    private void removeViewIfPresent() {
        mTab.getTabViewManager().removeTabViewProvider(this);
        mView = null;
    }

    // TabObserver implementation.
    @Override
    public void onActivityAttachmentChanged(Tab tab, @Nullable WindowAndroid window) {
        if (window == null) {
            removeViewIfPresent();
        } else {
            attachView();
        }
    }

    // UserData implementation.
    @Override
    public void destroy() {
        mTab.removeObserver(this);
    }

    @Override
    public int getTabViewProviderType() {
        return Type.SUSPENDED_TAB;
    }

    @Override
    public View getView() {
        return mView;
    }
}