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

import android.app.Activity;
import android.app.DownloadManager;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.graphics.Typeface;
import android.net.Uri;
import android.os.Build;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.TextUtils;
import android.text.style.ClickableSpan;
import android.text.style.StyleSpan;

import androidx.annotation.MainThread;
import androidx.annotation.Nullable;

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

import org.chromium.base.ApplicationStatus;
import org.chromium.base.BuildInfo;
import org.chromium.base.ContentUriUtils;
import org.chromium.base.ContextUtils;
import org.chromium.base.FileUtils;
import org.chromium.base.IntentUtils;
import org.chromium.base.Log;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.ChromeTabbedActivity;
import org.chromium.chrome.browser.IntentHandler;
import org.chromium.chrome.browser.app.download.home.DownloadActivityLauncher;
import org.chromium.chrome.browser.download.items.OfflineContentAggregatorFactory;
import org.chromium.chrome.browser.feature_engagement.TrackerFactory;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.media.MediaViewerUtils;
import org.chromium.chrome.browser.offlinepages.DownloadUiActionFlags;
import org.chromium.chrome.browser.offlinepages.OfflinePageBridge;
import org.chromium.chrome.browser.offlinepages.OfflinePageOrigin;
import org.chromium.chrome.browser.offlinepages.OfflinePageUtils;
import org.chromium.chrome.browser.offlinepages.downloads.OfflinePageDownloadBridge;
import org.chromium.chrome.browser.pdf.PdfUtils;
import org.chromium.chrome.browser.profiles.OTRProfileID;
import org.chromium.chrome.browser.profiles.Profile;
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.document.ChromeAsyncTabLauncher;
import org.chromium.chrome.browser.util.ChromeAccessibilityUtil;
import org.chromium.components.background_task_scheduler.TaskIds;
import org.chromium.components.download.DownloadState;
import org.chromium.components.download.ResumeMode;
import org.chromium.components.embedder_support.util.UrlConstants;
import org.chromium.components.feature_engagement.EventConstants;
import org.chromium.components.feature_engagement.Tracker;
import org.chromium.components.offline_items_collection.ContentId;
import org.chromium.components.offline_items_collection.FailState;
import org.chromium.components.offline_items_collection.LaunchLocation;
import org.chromium.components.offline_items_collection.LegacyHelpers;
import org.chromium.components.offline_items_collection.OfflineItem;
import org.chromium.components.offline_items_collection.OpenParams;
import org.chromium.content_public.browser.BrowserStartupController;
import org.chromium.content_public.browser.LoadUrlParams;
import org.chromium.ui.UiUtils;
import org.chromium.ui.base.DeviceFormFactor;
import org.chromium.ui.base.MimeTypeUtils;
import org.chromium.ui.widget.Toast;
import org.chromium.url.GURL;

import java.io.File;

/** A class containing some utility static methods. */
public class DownloadUtils {
    private static final String TAG = "download";

    public static final String EXTRA_OTR_PROFILE_ID =
            "org.chromium.chrome.browser.download.OTR_PROFILE_ID";
    private static final String MIME_TYPE_ZIP = "application/zip";
    private static final String DOCUMENTS_UI_PACKAGE_NAME = "com.android.documentsui";

    /**
     * Displays the download manager UI. Note the UI is different on tablets and on phones.
     * @param activity The current activity is available.
     * @param tab The current tab if it exists.
     * @param otrProfileID The {@link OTRProfileID} to determine whether download home should be
     * opened in incognito mode. If null, download page will be opened in normal profile.
     * @param source The source where the user action is coming from.
     * @return Whether the UI was shown.
     */
    public static boolean showDownloadManager(
            @Nullable Activity activity,
            @Nullable Tab tab,
            @Nullable OTRProfileID otrProfileID,
            @DownloadOpenSource int source) {
        return showDownloadManager(activity, tab, otrProfileID, source, false);
    }

    /**
     * Displays the download manager UI. Note the UI is different on tablets and on phones.
     *
     * @param activity The current activity is available.
     * @param tab The current tab if it exists.
     * @param otrProfileID The {@link OTRProfileID} to determine whether download home should be
     *     opened in incognito mode. Only used when no valid current or recent tab presents.
     * @param source The source where the user action is coming from.
     * @param showPrefetchedContent Whether the manager should start with prefetched content section
     *     expanded.
     * @return Whether the UI was shown.
     */
    @CalledByNative
    public static boolean showDownloadManager(
            @Nullable Activity activity,
            @Nullable Tab tab,
            @Nullable OTRProfileID otrProfileID,
            @DownloadOpenSource int source,
            boolean showPrefetchedContent) {
        // Figure out what tab was last being viewed by the user.
        if (activity == null) activity = ApplicationStatus.getLastTrackedFocusedActivity();
        Context appContext = ContextUtils.getApplicationContext();
        boolean isTablet;

        if (tab == null && activity instanceof ChromeTabbedActivity) {
            ChromeTabbedActivity chromeActivity = ((ChromeTabbedActivity) activity);
            tab = chromeActivity.getActivityTab();
            isTablet = chromeActivity.isTablet();
        } else {
            Context displayContext = activity != null ? activity : appContext;
            isTablet = DeviceFormFactor.isNonMultiDisplayContextOnTablet(displayContext);
        }

        // Use tab's profile if a valid tab exists.
        if (otrProfileID == null && tab != null) {
            otrProfileID = tab.getProfile().getOTRProfileID();
        }

        // If the profile is off-the-record and it does not exist, then do not start the activity.
        if (OTRProfileID.isOffTheRecord(otrProfileID)
                && !ProfileManager.getLastUsedRegularProfile()
                        .hasOffTheRecordProfile(otrProfileID)) {
            return false;
        }

        if (isTablet) {
            // Download Home shows up as a tab on tablets.
            LoadUrlParams params = new LoadUrlParams(UrlConstants.DOWNLOADS_URL);
            if (tab == null || !tab.isInitialized()) {
                // Open a new tab, which pops Chrome into the foreground.
                ChromeAsyncTabLauncher delegate = new ChromeAsyncTabLauncher(false);
                delegate.launchNewTab(params, TabLaunchType.FROM_CHROME_UI, null);
            } else {
                // Download Home shows up inside an existing tab, but only if the last Activity was
                // the ChromeTabbedActivity.
                tab.loadUrl(params);

                // Bring Chrome to the foreground, if possible. Unless Chrome is already in the
                // foreground, this request is most likely coming from a notification.
                Intent intent =
                        IntentHandler.createTrustedBringTabToFrontIntent(
                                tab.getId(), IntentHandler.BringToFrontSource.NOTIFICATION);
                if (intent != null) {
                    intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                    IntentUtils.safeStartActivity(appContext, intent);
                }
            }
        } else {
            DownloadActivityLauncher.getInstance()
                    .showDownloadActivity(activity, otrProfileID, showPrefetchedContent);
        }

        if (BrowserStartupController.getInstance().isFullBrowserStarted()) {
            Profile profile =
                    otrProfileID == null
                            ? ProfileManager.getLastUsedRegularProfile()
                            : ProfileManager.getLastUsedRegularProfile()
                                    .getOffTheRecordProfile(
                                            otrProfileID, /* createIfNeeded= */ true);
            Tracker tracker = TrackerFactory.getTrackerForProfile(profile);
            tracker.notifyEvent(EventConstants.DOWNLOAD_HOME_OPENED);
        }
        DownloadMetrics.recordDownloadPageOpen(source, tab);
        return true;
    }

    /**
     * @param intent An {@link Intent} instance.
     * @return The {@link OTRProfileID} that is attached to the given intent.
     */
    public static OTRProfileID getOTRProfileIDFromIntent(Intent intent) {
        String serializedId = IntentUtils.safeGetString(intent.getExtras(), EXTRA_OTR_PROFILE_ID);
        return OTRProfileID.deserialize(serializedId);
    }

    /**
     * @param intent An {@link Intent} instance.
     * @return The boolean to state whether the profile exists or not.
     */
    public static boolean doesProfileExistFromIntent(Intent intent) {
        boolean isProfileManagerInitialized = ProfileManager.isInitialized();
        if (!isProfileManagerInitialized) {
            return false;
        }

        String serializedId = IntentUtils.safeGetString(intent.getExtras(), EXTRA_OTR_PROFILE_ID);
        OTRProfileID otrProfileID = OTRProfileID.deserializeWithoutVerify(serializedId);

        return otrProfileID == null
                || ProfileManager.getLastUsedRegularProfile().hasOffTheRecordProfile(otrProfileID);
    }

    /**
     * Called to determine whether to use user initiated Jobs API for ensuring download completion.
     * @return True for using Jobs. False for using Foreground service.
     */
    public static boolean shouldUseUserInitiatedJobs() {
        return ChromeFeatureList.sDownloadsMigrateToJobsAPI.isEnabled()
                && Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE;
    }

    /**
     * Called to determine whether a given job is a user-initiated job or a regular job.
     * @return Whether the job is an user initiated job.
     */
    public static boolean isUserInitiatedJob(int taskId) {
        switch (taskId) {
            case TaskIds.DOWNLOAD_AUTO_RESUMPTION_UNMETERED_JOB_ID:
            case TaskIds.DOWNLOAD_AUTO_RESUMPTION_ANY_NETWORK_JOB_ID:
                return DownloadUtils.shouldUseUserInitiatedJobs();
            default:
                return false;
        }
    }

    /**
     * @return Whether or not pagination headers should be shown on download home.
     */
    public static boolean shouldShowPaginationHeaders() {
        return ChromeAccessibilityUtil.get().isAccessibilityEnabled()
                || UiUtils.isHardwareKeyboardAttached();
    }

    /**
     * Shows a "Downloading..." toast. Should be called after a download has been started.
     * @param context The {@link Context} used to make the toast.
     */
    public static void showDownloadStartToast(Context context) {
        Toast.makeText(context, R.string.download_started, Toast.LENGTH_SHORT).show();
    }

    /**
     * Trigger the download of an Offline Page.
     *
     * @param context Context to pull resources from.
     * @param tab Tab triggering the download.
     */
    public static void downloadOfflinePage(Context context, Tab tab) {
        if (tab.isNativePage() && tab.getNativePage().isPdf()) {
            DownloadController.downloadUrl(tab.getUrl().getSpec(), tab);
            return;
        }
        OfflinePageOrigin origin = new OfflinePageOrigin(context, tab);

        if (tab.isShowingErrorPage()) {
            // The download needs to be scheduled to happen at later time due to current network
            // error.
            final OfflinePageBridge bridge = OfflinePageBridge.getForProfile(tab.getProfile());
            bridge.scheduleDownload(
                    tab.getWebContents(),
                    OfflinePageBridge.ASYNC_NAMESPACE,
                    tab.getUrl().getSpec(),
                    DownloadUiActionFlags.PROMPT_DUPLICATE,
                    origin);
        } else {
            // Otherwise, the download can be started immediately.
            OfflinePageDownloadBridge.startDownload(tab, origin);
        }
        Tracker tracker = TrackerFactory.getTrackerForProfile(tab.getProfile());
        tracker.notifyEvent(EventConstants.DOWNLOAD_PAGE_STARTED);
    }

    /**
     * Whether the user should be allowed to download the current page.
     * @param tab Tab displaying the page that will be downloaded.
     * @return    Whether the "Download Page" button should be enabled.
     */
    public static boolean isAllowedToDownloadPage(Tab tab) {
        if (tab == null) return false;

        // Offline pages isn't supported in Incognito. This should be checked before calling
        // OfflinePageBridge.getForProfile because OfflinePageBridge instance will not be found
        // for incognito profile.
        if (tab.isIncognito()) return false;

        // Check if the page url is supported for saving. Only HTTP and HTTPS pages are allowed.
        if (!OfflinePageBridge.canSavePage(tab.getUrl())) return false;

        // Download will only be allowed for the error page if download button is shown in the page.
        if (tab.isShowingErrorPage()) {
            final OfflinePageBridge bridge = OfflinePageBridge.getForProfile(tab.getProfile());
            return bridge.isShowingDownloadButtonInErrorPage(tab.getWebContents());
        }

        // Don't allow re-downloading the currently displayed offline page.
        if (OfflinePageUtils.isOfflinePage(tab)) return false;

        return true;
    }

    /**
     * Returns a URI that points at the file.
     * @param filePath File path to get a URI for.
     * @return URI that points at that file, either as a content:// URI or a file:// URI.
     */
    @MainThread
    public static Uri getUriForItem(String filePath) {
        if (ContentUriUtils.isContentUri(filePath)) return Uri.parse(filePath);

        // It's ok to use blocking calls on main thread here, since the user is waiting to open or
        // share the file to other apps.
        boolean isOnSDCard = DownloadDirectoryProvider.isDownloadOnSDCard(filePath);
        if (isOnSDCard) {
            // Use custom file provider to generate content URI for download on SD card.
            return DownloadFileProvider.createContentUri(filePath);
        }
        // Use FileProvider to generate content URI or file URI.
        return FileUtils.getUriForFile(new File(filePath));
    }

    /**
     * Get the URI when shared or opened by other apps.
     *
     * @param filePath Downloaded file path.
     * @return URI for other apps to use the file via {@link android.content.ContentResolver}.
     */
    public static Uri getUriForOtherApps(String filePath) {
        return getUriForItem(filePath);
    }

    @CalledByNative
    private static @JniType("std::string") String getUriStringForPath(
            @JniType("std::string") String filePath) {
        if (ContentUriUtils.isContentUri(filePath)) return filePath;
        Uri uri = getUriForItem(filePath);
        return uri != null ? uri.toString() : new String();
    }

    /**
     * Utility method to open an {@link OfflineItem}, which can be a chrome download, offline page.
     * Falls back to open download home.
     * @param contentId The {@link ContentId} of the associated offline item.
     * @param otrProfileID The {@link OTRProfileID} of the download. Null if in regular mode.
     * @param source The location from which the download was opened.
     */
    public static void openItem(
            ContentId contentId,
            OTRProfileID otrProfileID,
            @DownloadOpenSource int source,
            Context context) {
        if (LegacyHelpers.isLegacyAndroidDownload(contentId)) {
            ContextUtils.getApplicationContext()
                    .startActivity(
                            new Intent(DownloadManager.ACTION_VIEW_DOWNLOADS)
                                    .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
        } else {
            OpenParams openParams = new OpenParams(LaunchLocation.PROGRESS_BAR);
            openParams.openInIncognito = OTRProfileID.isOffTheRecord(otrProfileID);
            OfflineContentAggregatorFactory.get().openItem(openParams, contentId);
        }
    }

    /**
     * Opens a file in Chrome or in another app if appropriate.
     * @param filePath Path to the file to open, can be a content Uri.
     * @param mimeType mime type of the file.
     * @param downloadGuid The associated download GUID.
     * @param otrProfileID The {@link OTRProfileID} of the download. Null if in regular mode.
     * @param originalUrl The original url of the downloaded file.
     * @param referrer Referrer of the downloaded file.
     * @param source The source that tries to open the download file.
     * @return whether the file could successfully be opened.
     */
    public static boolean openFile(
            String filePath,
            String mimeType,
            String downloadGuid,
            OTRProfileID otrProfileID,
            String originalUrl,
            String referrer,
            @DownloadOpenSource int source,
            Context context) {
        DownloadMetrics.recordDownloadOpen(source, mimeType);
        DownloadManagerService service = DownloadManagerService.getDownloadManagerService();

        // Check if Chrome should open the file itself.
        if (service.isDownloadOpenableInBrowser(mimeType)) {
            // Share URIs use the content:// scheme when able, which looks bad when displayed
            // in the URL bar.
            Uri contentUri = getUriForItem(filePath);
            Uri fileUri = contentUri;
            if (!ContentUriUtils.isContentUri(filePath)) {
                File file = new File(filePath);
                fileUri = Uri.fromFile(file);
            }
            String normalizedMimeType = Intent.normalizeMimeType(mimeType);

            // Sharing for media files is disabled on automotive.
            boolean isAutomotive = BuildInfo.getInstance().isAutomotive;
            Intent intent =
                    MediaViewerUtils.getMediaViewerIntent(
                            fileUri
                            /* displayUri= */ ,
                            contentUri
                            /* contentUri= */ ,
                            normalizedMimeType,
                            !isAutomotive,
                            !isAutomotive,
                            context);
            IntentHandler.startActivityForTrustedIntent(context, intent);
            service.updateLastAccessTime(downloadGuid, otrProfileID);
            return true;
        }

        // Check if any apps can open the file.
        try {
            // TODO(qinmin): Move this to an AsyncTask so we don't need to temper with strict mode.
            Uri uri =
                    ContentUriUtils.isContentUri(filePath)
                            ? Uri.parse(filePath)
                            : getUriForOtherApps(filePath);
            Intent viewIntent =
                    MediaViewerUtils.createViewIntentForUri(uri, mimeType, originalUrl, referrer);
            context.startActivity(viewIntent);
            service.updateLastAccessTime(downloadGuid, otrProfileID);
            return true;
        } catch (Exception e) {
            Log.e(TAG, "Cannot start activity to open file", e);
        }

        // If this is a zip file, check if Android Files app exists.
        if (MIME_TYPE_ZIP.equals(mimeType)) {
            try {
                PackageInfo packageInfo =
                        context.getPackageManager()
                                .getPackageInfo(
                                        DOCUMENTS_UI_PACKAGE_NAME, PackageManager.GET_ACTIVITIES);
                if (packageInfo != null) {
                    Intent viewDownloadsIntent = new Intent(DownloadManager.ACTION_VIEW_DOWNLOADS);
                    viewDownloadsIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                    viewDownloadsIntent.setPackage(DOCUMENTS_UI_PACKAGE_NAME);
                    context.startActivity(viewDownloadsIntent);
                    return true;
                }
            } catch (Exception e) {
                Log.e(TAG, "Cannot find files app for openning zip files", e);
            }
        }
        // Can't launch the Intent.
        if (source != DownloadOpenSource.DOWNLOAD_PROGRESS_INFO_BAR) {
            Toast.makeText(
                            context,
                            context.getString(R.string.download_cant_open_file),
                            Toast.LENGTH_SHORT)
                    .show();
        }
        return false;
    }

    /**
     * Opens a completed download.
     *
     * @param filePath File path on disk of the download to open.
     * @param mimeType MIME type of the downloaded file.
     * @param downloadGuid Unique GUID of the download.
     * @param otrProfileID User's OTRProfileID.
     * @param originalUrl URL which initially triggered the download itself.
     * @param referer URL of the page which redirected to the download URL.
     * @param source Where this download was initiated from.
     */
    @CalledByNative
    public static void openDownload(
            @JniType("std::string") String filePath,
            @JniType("std::string") String mimeType,
            @JniType("std::string") String downloadGuid,
            OTRProfileID otrProfileID,
            @JniType("std::string") String originalUrl,
            @JniType("std::string") String referer,
            @DownloadOpenSource int source) {
        // Mapping generic MIME type to android openable type based on URL and file extension.
        String newMimeType = MimeUtils.remapGenericMimeType(mimeType, originalUrl, filePath);
        Activity activity = ApplicationStatus.getLastTrackedFocusedActivity();
        DownloadMessageUiController messageUiController =
                DownloadManagerService.getDownloadManagerService().getMessageUiController(null);
        if (messageUiController != null
                && messageUiController.isDownloadInterstitialItem(
                        new GURL(originalUrl), downloadGuid)) {
            return;
        }
        Tab tab = null;
        if (activity instanceof ChromeTabbedActivity chromeActivity) {
            // TODO(crbug.com/356713476): Stop using the deprecated method.
            tab = chromeActivity.getActivityTab();
        }
        if (otrProfileID == null && tab != null) {
            otrProfileID = tab.getProfile().getOTRProfileID();
        }
        boolean isIncognito = OTRProfileID.isOffTheRecord(otrProfileID);
        // TODO(https://crbug.com/327680567): Ensure the pdf page is opened in the intended window.
        if (PdfUtils.shouldOpenPdfInline(isIncognito)
                && newMimeType.equals(MimeTypeUtils.PDF_MIME_TYPE)) {
            LoadUrlParams params = new LoadUrlParams(PdfUtils.encodePdfPageUrl(filePath));
            ChromeAsyncTabLauncher delegate = new ChromeAsyncTabLauncher(isIncognito);
            delegate.launchNewTab(params, TabLaunchType.FROM_CHROME_UI, /* parent= */ null);
            return;
        }
        boolean canOpen =
                DownloadUtils.openFile(
                        filePath,
                        newMimeType,
                        downloadGuid,
                        otrProfileID,
                        originalUrl,
                        referer,
                        source,
                        activity == null ? ContextUtils.getApplicationContext() : activity);
        if (!canOpen) {
            DownloadUtils.showDownloadManager(null, null, otrProfileID, source);
        }
    }

    /**
     * Fires an Intent to open a downloaded item.
     *
     * @param context Context to use.
     * @param intent Intent that can be fired.
     * @return Whether an Activity was successfully started for the Intent.
     */
    static boolean fireOpenIntentForDownload(Context context, Intent intent) {
        try {
            if (TextUtils.equals(intent.getPackage(), context.getPackageName())) {
                IntentHandler.startActivityForTrustedIntent(intent);
            } else {
                context.startActivity(intent);
            }
            return true;
        } catch (ActivityNotFoundException ex) {
            Log.d(
                    TAG,
                    "Activity not found for "
                            + intent.getType()
                            + " over "
                            + intent.getData().getScheme(),
                    ex);
        } catch (SecurityException ex) {
            Log.d(TAG, "cannot open intent: " + intent, ex);
        } catch (Exception ex) {
            Log.d(TAG, "cannot open intent: " + intent, ex);
        }

        return false;
    }

    /**
     * Get the resume mode based on the current fail state, to distinguish the case where download
     * cannot be resumed at all or can be resumed in the middle, or should be restarted from the
     * beginning.
     *
     * @param url URL of the download.
     * @param failState Why the download failed.
     * @return The resume mode for the current fail state.
     */
    public static @ResumeMode int getResumeMode(
            String url, @FailState @JniType("offline_items_collection::FailState") int failState) {
        return DownloadUtilsJni.get().getResumeMode(url, failState);
    }

    /**
     * Query the Download backends about whether a download is paused.
     *
     * <p>The Java-side contains more information about the status of a download than is persisted
     * by the native backend, so it is queried first.
     *
     * @param item Download to check the status of.
     * @return Whether the download is paused or not.
     */
    public static boolean isDownloadPaused(DownloadItem item) {
        DownloadSharedPreferenceHelper helper = DownloadSharedPreferenceHelper.getInstance();
        DownloadSharedPreferenceEntry entry =
                helper.getDownloadSharedPreferenceEntry(item.getContentId());

        if (entry != null) {
            // The Java downloads backend knows more about the download than the native backend.
            return !entry.isAutoResumable;
        } else {
            // Only the native downloads backend knows about the download.
            if (item.getDownloadInfo().state() == DownloadState.IN_PROGRESS) {
                return item.getDownloadInfo().isPaused();
            } else {
                return item.getDownloadInfo().state() == DownloadState.INTERRUPTED;
            }
        }
    }

    /**
     * Return whether a download is pending.
     *
     * @param item Download to check the status of.
     * @return Whether the download is pending or not.
     */
    public static boolean isDownloadPending(DownloadItem item) {
        DownloadSharedPreferenceHelper helper = DownloadSharedPreferenceHelper.getInstance();
        DownloadSharedPreferenceEntry entry =
                helper.getDownloadSharedPreferenceEntry(item.getContentId());
        return entry != null
                && item.getDownloadInfo().state() == DownloadState.INTERRUPTED
                && entry.isAutoResumable;
    }

    /**
     * Gets the duplicate infobar or dialog text for regular downloads.
     *
     * @param context Context to be used.
     * @param template Template of the text to be displayed.
     * @param filePath Suggested file path of the download.
     * @param addSizeStringIfAvailable Whether size string should be added to the dialog text.
     * @param totalBytes Size of the download.
     * @param clickableSpan Action to perform when clicking on the file name.
     * @return message to be displayed on the infobar or dialog.
     */
    public static CharSequence getDownloadMessageText(
            final Context context,
            final String template,
            final String filePath,
            boolean addSizeStringIfAvailable,
            long totalBytes,
            final ClickableSpan clickableSpan) {
        return getMessageText(
                template,
                new File(filePath).getName(),
                addSizeStringIfAvailable,
                totalBytes,
                clickableSpan);
    }

    /**
     * Gets the infobar or dialog text for offline page downloads.
     *
     * @param context Context to be used.
     * @param filePath Path of the file to be displayed.
     * @param duplicateRequestExists Whether a duplicate request exists.
     * @param clickableSpan Action to perform when clicking on the page link.
     * @return message to be displayed on the infobar or dialog.
     */
    public static CharSequence getOfflinePageMessageText(
            final Context context,
            String filePath,
            boolean duplicateRequestExists,
            ClickableSpan clickableSpan) {
        String template =
                context.getString(
                        duplicateRequestExists
                                ? R.string.duplicate_download_request_infobar_text
                                : R.string.duplicate_download_infobar_text);
        return getMessageText(
                template,
                filePath,
                false
                /* addSizeStringIfAvailable= */ ,
                0
                /* totalBytes= */ ,
                clickableSpan);
    }

    /**
     * Open a given page URL.
     * @param context Context to be used.
     * @param pageUrl URL of the page.
     */
    public static void openPageUrl(Context context, String pageUrl) {
        Intent intent = new Intent(Intent.ACTION_VIEW);
        intent.setData(Uri.parse(pageUrl));
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        intent.setPackage(context.getPackageName());
        context.startActivity(intent);
    }

    /**
     * Helper method to get the text to be displayed for duplicate download.
     * @param template Message template.
     * @param fileName Name of the file.
     * @param addSizeStringIfAvailable Whether size string should be added to the dialog text.
     * @param totalBytes Size of the download.
     * @param clickableSpan Action to perform when clicking on the file name.
     * @return message to be displayed on the infobar.
     */
    private static CharSequence getMessageText(
            final String template,
            final String fileName,
            boolean addSizeStringIfAvailable,
            long totalBytes,
            final ClickableSpan clickableSpan) {
        final SpannableString formattedFilePath = new SpannableString(fileName);
        formattedFilePath.setSpan(
                new StyleSpan(Typeface.BOLD),
                0,
                fileName.length(),
                Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
        formattedFilePath.setSpan(
                clickableSpan, 0, fileName.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);

        if (!addSizeStringIfAvailable) {
            return TextUtils.expandTemplate(template, formattedFilePath);
        } else {
            String sizeString = "";
            if (totalBytes > 0) {
                sizeString =
                        " ("
                                + org.chromium.components.browser_ui.util.DownloadUtils
                                        .getStringForBytes(
                                                ContextUtils.getApplicationContext(), totalBytes)
                                + ")";
            }

            return TextUtils.expandTemplate(template, formattedFilePath, sizeString);
        }
    }

    @NativeMethods
    interface Natives {
        int getResumeMode(
                @JniType("std::string") String url,
                @FailState @JniType("offline_items_collection::FailState") int failState);
    }
}