chromium/chrome/android/java/src/org/chromium/chrome/browser/download/DownloadController.java

// Copyright 2012 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.Manifest.permission;

import androidx.annotation.Nullable;

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

import org.chromium.chrome.browser.pdf.PdfPage;
import org.chromium.chrome.browser.pdf.PdfUtils;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.tab.EmptyTabObserver;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.ui.native_page.NativePage;
import org.chromium.components.download.DownloadCollectionBridge;
import org.chromium.content_public.browser.LoadUrlParams;
import org.chromium.content_public.browser.WebContents;
import org.chromium.content_public.browser.navigation_controller.LoadURLType;
import org.chromium.ui.base.MimeTypeUtils;
import org.chromium.ui.base.WindowAndroid;
import org.chromium.ui.permissions.AndroidPermissionDelegate;
import org.chromium.url.GURL;

/** Java counterpart of android DownloadController. Owned by native. */
public class DownloadController {
    /**
     * Called to download the given URL triggered from a tab.
     *
     * @param url Url to download.
     * @param tab Tab triggering the download.
     */
    public static void downloadUrl(String url, Tab tab) {
        assert hasFileAccess(tab.getWindowAndroid());
        DownloadControllerJni.get().downloadUrl(url, tab.getWebContents());
    }

    /**
     * Notifies the download delegate that a download completed and passes along info about the
     * download. This can be either a POST download or a GET download with authentication.
     */
    @CalledByNative
    private static void onDownloadCompleted(
            @Nullable Tab tab, DownloadInfo downloadInfo, boolean isDownloadSafe) {
        MediaStoreHelper.addImageToGalleryOnSDCard(
                downloadInfo.getFilePath(), downloadInfo.getMimeType());
        if (tab == null
                || !PdfUtils.shouldOpenPdfInline(tab.isIncognito())
                || !downloadInfo.getMimeType().equals(MimeTypeUtils.PDF_MIME_TYPE)) {
            return;
        }
        NativePage nativePage = tab.getNativePage();
        if (nativePage == null || !nativePage.isPdf()) {
            return;
        }
        assert nativePage instanceof PdfPage;
        ((PdfPage) nativePage)
                .onDownloadComplete(
                        downloadInfo.getFileName(), downloadInfo.getFilePath(), isDownloadSafe);
        tab.updateTitle();
    }

    /**
     * Returns whether file access is allowed.
     *
     * @return true if allowed, or false otherwise.
     */
    @CalledByNative
    private static boolean hasFileAccess(WindowAndroid windowAndroid) {
        if (DownloadCollectionBridge.supportsDownloadCollection()) return true;
        AndroidPermissionDelegate delegate = windowAndroid;
        return delegate == null ? false : delegate.hasPermission(permission.WRITE_EXTERNAL_STORAGE);
    }

    /**
     * Requests the storage permission. This should be called from the native code.
     * @param callbackId ID of native callback to notify the result.
     * @param windowAndroid The {@link WindowAndroid} associated with the tab.
     */
    @CalledByNative
    private static void requestFileAccess(final long callbackId, WindowAndroid windowAndroid) {
        if (windowAndroid == null) {
            DownloadControllerJni.get()
                    .onAcquirePermissionResult(
                            callbackId, /* granted= */ false, /* permissionToUpdate= */ "");
            return;
        }
        FileAccessPermissionHelper.requestFileAccessPermissionHelper(
                windowAndroid,
                result -> {
                    DownloadControllerJni.get()
                            .onAcquirePermissionResult(
                                    callbackId,
                                    result.first,
                                    result.second == null ? "" : result.second);
                });
    }

    /**
     * Enqueue a request to download a file using Android DownloadManager.
     * @param url Url to download.
     * @param userAgent User agent to use.
     * @param contentDisposition Content disposition of the request.
     * @param mimeType MIME type.
     * @param cookie Cookie to use.
     * @param referrer Referrer to use.
     */
    @CalledByNative
    private static void enqueueAndroidDownloadManagerRequest(
            GURL url,
            String userAgent,
            String fileName,
            String mimeType,
            String cookie,
            GURL referrer) {
        DownloadInfo downloadInfo =
                new DownloadInfo.Builder()
                        .setUrl(url)
                        .setUserAgent(userAgent)
                        .setFileName(fileName)
                        .setMimeType(mimeType)
                        .setCookie(cookie)
                        .setReferrer(referrer)
                        .setIsGETRequest(true)
                        .build();
        enqueueDownloadManagerRequest(downloadInfo);
    }

    /**
     * Enqueue a request to download a file using Android DownloadManager.
     *
     * @param info Download information about the download.
     */
    static void enqueueDownloadManagerRequest(final DownloadInfo info) {
        DownloadManagerService.getDownloadManagerService()
                .enqueueNewDownload(new DownloadItem(true, info), true);
    }

    @CalledByNative
    private static void onPdfDownloadStarted(Tab tab, DownloadInfo downloadInfo) {
        if (!PdfUtils.shouldOpenPdfInline(tab.isIncognito())) {
            return;
        }
        String downloadUrl = downloadInfo.getUrl().getSpec();
        String pdfPageUrl = PdfUtils.encodePdfPageUrl(downloadUrl);
        assert pdfPageUrl != null;
        LoadUrlParams param = new LoadUrlParams(pdfPageUrl);
        // Set isPdf param so that other parts of the code can load the pdf native page instead of
        // starting a download.
        param.setIsPdf(true);
        param.setLoadType(LoadURLType.PDF_ANDROID);
        param.setVirtualUrlForSpecialCases(downloadUrl);
        // If the download url matches the tab’s url, avoid duplicate navigation entries by
        // replacing the current entry.
        param.setShouldReplaceCurrentEntry(downloadUrl.equals(tab.getUrl().getSpec()));
        tab.loadUrl(param);
        tab.addObserver(
                new EmptyTabObserver() {
                    @Override
                    public void onDestroyed(Tab tab) {
                        DownloadControllerJni.get()
                                .cancelDownload(tab.getProfile(), downloadInfo.getDownloadGuid());
                    }
                });
    }

    @NativeMethods
    interface Natives {
        void onAcquirePermissionResult(
                long callbackId,
                boolean granted,
                @JniType("std::string") String permissionToUpdate);

        void downloadUrl(@JniType("std::string") String url, WebContents webContents);

        void cancelDownload(
                @JniType("Profile*") Profile profile, @JniType("std::string") String downloadGuid);
    }
}