chromium/chrome/android/java/src/org/chromium/chrome/browser/offlinepages/downloads/OfflinePageDownloadBridge.java

// Copyright 2016 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.offlinepages.downloads;

import android.app.Activity;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.provider.Browser;

import androidx.browser.customtabs.CustomTabsIntent;

import org.jni_zero.CalledByNative;
import org.jni_zero.JNINamespace;
import org.jni_zero.JniType;
import org.jni_zero.NativeMethods;

import org.chromium.base.ApplicationStatus;
import org.chromium.base.ContextUtils;
import org.chromium.base.IntentUtils;
import org.chromium.chrome.browser.ChromeTabbedActivity;
import org.chromium.chrome.browser.IntentHandler;
import org.chromium.chrome.browser.LaunchIntentDispatcher;
import org.chromium.chrome.browser.app.download.home.DownloadActivity;
import org.chromium.chrome.browser.browserservices.intents.BrowserServicesIntentDataProvider.CustomTabsUiType;
import org.chromium.chrome.browser.customtabs.CustomTabIntentDataProvider;
import org.chromium.chrome.browser.download.DownloadManagerService;
import org.chromium.chrome.browser.offlinepages.OfflinePageOrigin;
import org.chromium.chrome.browser.offlinepages.OfflinePageUtils;
import org.chromium.chrome.browser.profiles.ProfileManager;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tab.TabLaunchType;
import org.chromium.chrome.browser.tabmodel.AsyncTabCreationParams;
import org.chromium.chrome.browser.tabmodel.document.ChromeAsyncTabLauncher;
import org.chromium.components.offline_items_collection.LaunchLocation;
import org.chromium.content_public.browser.LoadUrlParams;

/**
 * Serves as an interface between Download Home UI and offline page related items that are to be
 * displayed in the downloads UI.
 */
@JNINamespace("offline_pages::android")
public class OfflinePageDownloadBridge {
    private static OfflinePageDownloadBridge sInstance;
    private static boolean sIsTesting;
    private long mNativeOfflinePageDownloadBridge;

    /**
     * @return An {@link OfflinePageDownloadBridge} instance singleton.  If one
     *         is not available this will create a new one.
     */
    public static OfflinePageDownloadBridge getInstance() {
        if (sInstance == null) {
            sInstance = new OfflinePageDownloadBridge();
        }
        return sInstance;
    }

    private OfflinePageDownloadBridge() {
        mNativeOfflinePageDownloadBridge =
                sIsTesting
                        ? 0L
                        : OfflinePageDownloadBridgeJni.get().init(OfflinePageDownloadBridge.this);
    }

    /** Destroys the native portion of the bridge. */
    public void destroy() {
        if (mNativeOfflinePageDownloadBridge != 0) {
            OfflinePageDownloadBridgeJni.get()
                    .destroy(mNativeOfflinePageDownloadBridge, OfflinePageDownloadBridge.this);
            mNativeOfflinePageDownloadBridge = 0;
        }
    }

    /**
     * 'Opens' the offline page identified by the given URL and offlineId by navigating to the saved
     * local snapshot. No automatic redirection is happening based on the connection status. If the
     * item with specified GUID is not found or can't be opened, nothing happens.
     */
    @CalledByNative
    private static void openItem(
            final @JniType("std::string") String url,
            final long offlineId,
            final int location,
            final boolean isIncognito,
            final boolean openInCct) {
        OfflinePageUtils.getLoadUrlParamsForOpeningOfflineVersion(
                url,
                offlineId,
                location,
                (params) -> {
                    if (params == null) return;
                    boolean openingFromDownloadsHome =
                            ApplicationStatus.getLastTrackedFocusedActivity()
                                    instanceof DownloadActivity;
                    if (location == LaunchLocation.NET_ERROR_SUGGESTION) {
                        openItemInCurrentTab(offlineId, params);
                    } else if (openInCct && openingFromDownloadsHome) {
                        openItemInCct(offlineId, params, isIncognito);
                    } else {
                        openItemInNewTab(offlineId, params, isIncognito);
                    }
                },
                ProfileManager.getLastUsedRegularProfile());
    }

    /**
     * Opens the offline page identified by the given offlineId and the LoadUrlParams in the current
     * tab. If no tab is current, the page is not opened.
     */
    private static void openItemInCurrentTab(long offlineId, LoadUrlParams params) {
        Activity activity = ApplicationStatus.getLastTrackedFocusedActivity();
        if (activity == null) return;
        Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(params.getUrl()));
        IntentHandler.setIntentExtraHeaders(params.getExtraHeaders(), intent);
        intent.putExtra(
                Browser.EXTRA_APPLICATION_ID, activity.getApplicationContext().getPackageName());
        intent.setPackage(activity.getApplicationContext().getPackageName());
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);

        IntentHandler.startActivityForTrustedIntent(intent);
    }

    /**
     * Opens the offline page identified by the given offlineId and the LoadUrlParams in a new tab.
     */
    private static void openItemInNewTab(
            long offlineId, LoadUrlParams params, boolean isIncognito) {
        ComponentName componentName = getComponentName();
        AsyncTabCreationParams asyncParams =
                componentName == null
                        ? new AsyncTabCreationParams(params)
                        : new AsyncTabCreationParams(params, componentName);
        final ChromeAsyncTabLauncher chromeAsyncTabLauncher =
                new ChromeAsyncTabLauncher(isIncognito);
        chromeAsyncTabLauncher.launchNewTab(
                asyncParams, TabLaunchType.FROM_CHROME_UI, Tab.INVALID_TAB_ID);
    }

    /** Opens the offline page identified by the given offlineId and the LoadUrlParams in a CCT. */
    private static void openItemInCct(long offlineId, LoadUrlParams params, boolean isIncognito) {
        final Context context;
        if (ApplicationStatus.hasVisibleActivities()) {
            context = ApplicationStatus.getLastTrackedFocusedActivity();
        } else {
            context = ContextUtils.getApplicationContext();
        }

        CustomTabsIntent.Builder builder = new CustomTabsIntent.Builder();
        builder.setShowTitle(true);
        builder.addDefaultShareMenuItem();

        CustomTabsIntent customTabIntent = builder.build();
        customTabIntent.intent.setData(Uri.parse(params.getUrl()));

        Intent intent =
                LaunchIntentDispatcher.createCustomTabActivityIntent(
                        context, customTabIntent.intent);
        intent.setPackage(context.getPackageName());
        intent.putExtra(Browser.EXTRA_APPLICATION_ID, context.getPackageName());
        intent.putExtra(CustomTabIntentDataProvider.EXTRA_UI_TYPE, CustomTabsUiType.OFFLINE_PAGE);
        // TODO(crbug.com/40731212): Pass isIncognito boolean here after finding a way not to
        // reload the downloaded page for Incognito CCT.
        intent.putExtra(IntentHandler.EXTRA_OPEN_NEW_INCOGNITO_TAB, false);

        IntentUtils.addTrustedIntentExtras(intent);
        if (!(context instanceof Activity)) intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        IntentHandler.setIntentExtraHeaders(params.getExtraHeaders(), intent);

        context.startActivity(intent);
    }

    /**
     * Starts download of the page currently open in the specified Tab.
     * If tab's contents are not yet loaded completely, we'll wait for it
     * to load enough for snapshot to be reasonable. If the Chrome is made
     * background and killed, the background request remains that will
     * eventually load the page in background and obtain its offline
     * snapshot.
     *
     * @param tab a tab contents of which will be saved locally.
     * @param origin the object encapsulating application origin of the request.
     */
    public static void startDownload(Tab tab, OfflinePageOrigin origin) {
        OfflinePageDownloadBridgeJni.get().startDownload(tab, origin.encodeAsJsonString());
    }

    /** Shows a "Downloading ..." toast for the requested items already scheduled for download. */
    @CalledByNative
    public static void showDownloadingToast() {
        DownloadManagerService.getDownloadManagerService()
                .getMessageUiController(/* otrProfileID= */ null)
                .onDownloadStarted();
    }

    /**
     * Method to ensure that the bridge is created for tests without calling the native portion of
     * initialization.
     * @param isTesting flag indicating whether the constructor will initialize native code.
     */
    static void setIsTesting(boolean isTesting) {
        sIsTesting = isTesting;
    }

    private static ComponentName getComponentName() {
        if (!ApplicationStatus.hasVisibleActivities()) return null;

        Activity activity = ApplicationStatus.getLastTrackedFocusedActivity();
        if (activity instanceof ChromeTabbedActivity) {
            return activity.getComponentName();
        }

        return null;
    }

    @NativeMethods
    interface Natives {
        long init(OfflinePageDownloadBridge caller);

        void destroy(long nativeOfflinePageDownloadBridge, OfflinePageDownloadBridge caller);

        void startDownload(Tab tab, @JniType("std::string") String origin);
    }
}