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

// Copyright 2015 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 android.app.DownloadManager;
import android.content.pm.PackageManager;
import android.os.Environment;
import android.text.TextUtils;
import android.util.Pair;
import android.webkit.URLUtil;

import androidx.annotation.VisibleForTesting;

import org.chromium.base.Log;
import org.chromium.base.ThreadUtils;
import org.chromium.base.UserData;
import org.chromium.base.UserDataHost;
import org.chromium.base.task.AsyncTask;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.components.embedder_support.util.UrlUtilities;
import org.chromium.ui.base.WindowAndroid;
import org.chromium.ui.permissions.PermissionCallback;
import org.chromium.url.GURL;

import java.io.File;

/**
 * Chrome implementation of the ContentViewDownloadDelegate interface.
 *
 * Listens to POST and GET download events. GET download requests are passed along to the
 * Android Download Manager. POST downloads are expected to be handled natively and listener
 * is responsible for adding the completed download to the download manager.
 *
 * Prompts the user when a dangerous file is downloaded. Auto-opens PDFs after downloading.
 */
public class ChromeDownloadDelegate implements UserData {
    private static final String TAG = "Download";

    private static final Class<ChromeDownloadDelegate> USER_DATA_KEY = ChromeDownloadDelegate.class;
    private Tab mTab;

    public static ChromeDownloadDelegate from(Tab tab) {
        UserDataHost host = tab.getUserDataHost();
        ChromeDownloadDelegate controller = host.getUserData(USER_DATA_KEY);
        return controller == null
                ? host.setUserData(USER_DATA_KEY, new ChromeDownloadDelegate(tab))
                : controller;
    }

    /**
     * Creates ChromeDownloadDelegate.
     * @param tab The corresponding tab instance.
     */
    @VisibleForTesting
    ChromeDownloadDelegate(Tab tab) {
        mTab = tab;
    }

    @Override
    public void destroy() {
        mTab = null;
    }

    /**
     * Notify the host application a download should be done, even if there is a
     * streaming viewer available for this type.
     *
     * @param downloadInfo Information about the download.
     */
    protected void onDownloadStartNoStream(final DownloadInfo downloadInfo) {
        final String fileName = downloadInfo.getFileName();
        assert !TextUtils.isEmpty(fileName);
        final String newMimeType =
                MimeUtils.remapGenericMimeType(
                        downloadInfo.getMimeType(), downloadInfo.getUrl().getSpec(), fileName);
        new AsyncTask<Pair<String, File>>() {
            @Override
            protected Pair<String, File> doInBackground() {
                // Check to see if we have an SDCard.
                String status = Environment.getExternalStorageState();
                File fullDirPath = getDownloadDirectoryFullPath();
                return new Pair<String, File>(status, fullDirPath);
            }

            @Override
            protected void onPostExecute(Pair<String, File> result) {
                String externalStorageState = result.first;
                File fullDirPath = result.second;
                if (!checkExternalStorageAndNotify(
                        downloadInfo, fullDirPath, externalStorageState)) {
                    return;
                }
                GURL url = sanitizeDownloadUrl(downloadInfo);
                if (url == null) return;
                DownloadInfo newInfo =
                        DownloadInfo.Builder.fromDownloadInfo(downloadInfo)
                                .setUrl(url)
                                .setMimeType(newMimeType)
                                .setDescription(url.getSpec())
                                .setFileName(fileName)
                                .setIsGETRequest(true)
                                .build();
                DownloadController.enqueueDownloadManagerRequest(newInfo);
                // TODO(shaktisahu): Verify if we still need to close an empty tab for OMA download.
            }
        }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
    }

    /**
     * Sanitize the URL for the download item.
     *
     * @param downloadInfo Information about the download.
     */
    protected GURL sanitizeDownloadUrl(DownloadInfo downloadInfo) {
        return downloadInfo.getUrl();
    }

    /**
     * Return the full path of the download directory.
     *
     * @return File object containing the path to the download directory.
     */
    private static File getDownloadDirectoryFullPath() {
        assert !ThreadUtils.runningOnUiThread();
        File dir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
        if (!dir.mkdir() && !dir.isDirectory()) return null;
        return dir;
    }

    private static boolean checkFileExists(File dirPath, final String fileName) {
        assert !ThreadUtils.runningOnUiThread();
        final File file = new File(dirPath, fileName);
        return file != null && file.exists();
    }

    private static void deleteFileForOverwrite(DownloadInfo info) {
        assert !ThreadUtils.runningOnUiThread();
        File dir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
        if (!dir.isDirectory()) return;
        final File file = new File(dir, info.getFileName());
        if (!file.delete()) {
            Log.e(TAG, "Failed to delete a file: " + info.getFileName());
        }
    }

    /**
     * Check the external storage and notify user on error.
     *
     * @param fullDirPath The dir path to download a file. Normally this is external storage.
     * @param externalStorageStatus The status of the external storage.
     * @return Whether external storage is ok for downloading.
     */
    private boolean checkExternalStorageAndNotify(
            DownloadInfo downloadInfo, File fullDirPath, String externalStorageStatus) {
        if (fullDirPath == null) {
            Log.e(TAG, "Download failed: no SD card");
            alertDownloadFailure(downloadInfo, DownloadManager.ERROR_DEVICE_NOT_FOUND);
            return false;
        }
        if (!externalStorageStatus.equals(Environment.MEDIA_MOUNTED)) {
            int reason = DownloadManager.ERROR_DEVICE_NOT_FOUND;
            // Check to see if the SDCard is busy, same as the music app
            if (externalStorageStatus.equals(Environment.MEDIA_SHARED)) {
                Log.e(TAG, "Download failed: SD card unavailable");
                reason = DownloadManager.ERROR_FILE_ERROR;
            } else {
                Log.e(TAG, "Download failed: no SD card");
            }
            alertDownloadFailure(downloadInfo, reason);
            return false;
        }
        return true;
    }

    /**
     * Alerts user of download failure.
     *
     * @param downloadInfo The associated download.
     * @param reason Reason of failure defined in {@link DownloadManager}
     */
    private void alertDownloadFailure(DownloadInfo downloadInfo, int reason) {
        DownloadItem downloadItem = new DownloadItem(false, downloadInfo);
        DownloadManagerService.getDownloadManagerService().onDownloadFailed(downloadItem, reason);
    }

    /**
     * For certain download types(OMA for example), android DownloadManager should
     * handle them. Call this function to intercept those downloads.
     *
     * @param url URL to be downloaded.
     * @return whether the DownloadManager should intercept the download.
     */
    public boolean shouldInterceptContextMenuDownload(GURL url) {
        if (!UrlUtilities.isHttpOrHttps(url)) return false;
        String path = url.getPath();
        if (!OMADownloadHandler.isOMAFile(path)) return false;
        if (mTab == null) return true;
        String fileName =
                URLUtil.guessFileName(url.getSpec(), null, MimeUtils.OMA_DRM_MESSAGE_MIME);
        final DownloadInfo downloadInfo =
                new DownloadInfo.Builder().setUrl(url).setFileName(fileName).build();
        WindowAndroid window = mTab.getWindowAndroid();
        if (window.hasPermission(permission.WRITE_EXTERNAL_STORAGE)) {
            onDownloadStartNoStream(downloadInfo);
        } else if (window.canRequestPermission(permission.WRITE_EXTERNAL_STORAGE)) {
            PermissionCallback permissionCallback =
                    (permissions, grantResults) -> {
                        if (grantResults.length > 0
                                && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                            onDownloadStartNoStream(downloadInfo);
                        }
                    };
            window.requestPermissions(
                    new String[] {permission.WRITE_EXTERNAL_STORAGE}, permissionCallback);
        }
        return true;
    }
}