blob: f569dcd97fcbfa669226f759d57605769555afcc [file] [log] [blame]
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/android/download/download_controller.h"
#include <memory>
#include <vector>
#include "base/android/jni_android.h"
#include "base/android/jni_string.h"
#include "base/bind.h"
#include "base/feature_list.h"
#include "base/lazy_instance.h"
#include "base/logging.h"
#include "base/metrics/field_trial_params.h"
#include "base/metrics/histogram_macros.h"
#include "base/synchronization/lock.h"
#include "base/task/post_task.h"
#include "chrome/browser/android/chrome_feature_list.h"
#include "chrome/browser/android/download/dangerous_download_infobar_delegate.h"
#include "chrome/browser/android/download/download_manager_service.h"
#include "chrome/browser/android/download/download_utils.h"
#include "chrome/browser/android/tab_android.h"
#include "chrome/browser/download/download_stats.h"
#include "chrome/browser/infobars/infobar_service.h"
#include "chrome/browser/offline_pages/android/offline_page_bridge.h"
#include "chrome/browser/permissions/permission_update_infobar_delegate_android.h"
#include "chrome/browser/ui/android/view_android_helper.h"
#include "chrome/browser/vr/vr_tab_helper.h"
#include "chrome/grit/chromium_strings.h"
#include "components/download/public/common/auto_resumption_handler.h"
#include "components/download/public/common/download_url_parameters.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/download_item_utils.h"
#include "content/public/browser/download_manager.h"
#include "content/public/browser/download_request_utils.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/browser/render_view_host.h"
#include "content/public/common/referrer.h"
#include "jni/DownloadController_jni.h"
#include "net/base/filename_util.h"
#include "net/traffic_annotation/network_traffic_annotation.h"
#include "ui/android/view_android.h"
#include "ui/android/window_android.h"
#include "ui/base/page_transition_types.h"
using base::android::ConvertUTF8ToJavaString;
using base::android::JavaParamRef;
using base::android::ScopedJavaLocalRef;
using content::BrowserContext;
using content::BrowserThread;
using content::ContextMenuParams;
using download::DownloadItem;
using content::DownloadManager;
using content::WebContents;
namespace {
// Guards download_controller_
base::LazyInstance<base::Lock>::DestructorAtExit g_download_controller_lock_;
void CreateContextMenuDownload(
const content::ResourceRequestInfo::WebContentsGetter& wc_getter,
const content::ContextMenuParams& params,
bool is_link,
const std::string& extra_headers,
bool granted) {
content::WebContents* web_contents = wc_getter.Run();
if (!granted)
return;
if (!web_contents) {
DownloadController::RecordStoragePermission(
DownloadController::StoragePermissionType::
STORAGE_PERMISSION_NO_WEB_CONTENTS);
return;
}
const GURL& url = is_link ? params.link_url : params.src_url;
const GURL& referring_url =
params.frame_url.is_empty() ? params.page_url : params.frame_url;
content::DownloadManager* dlm =
content::BrowserContext::GetDownloadManager(
web_contents->GetBrowserContext());
std::unique_ptr<download::DownloadUrlParameters> dl_params(
content::DownloadRequestUtils::CreateDownloadForWebContentsMainFrame(
web_contents, url,
TRAFFIC_ANNOTATION_WITHOUT_PROTO("Download via context menu")));
content::Referrer referrer = content::Referrer::SanitizeForRequest(
url,
content::Referrer(referring_url.GetAsReferrer(), params.referrer_policy));
dl_params->set_referrer(referrer.url);
dl_params->set_referrer_policy(
content::Referrer::ReferrerPolicyForUrlRequest(referrer.policy));
if (is_link)
dl_params->set_referrer_encoding(params.frame_charset);
net::HttpRequestHeaders headers;
headers.AddHeadersFromString(extra_headers);
for (net::HttpRequestHeaders::Iterator it(headers); it.GetNext();)
dl_params->add_request_header(it.name(), it.value());
if (!is_link && extra_headers.empty())
dl_params->set_prefer_cache(true);
dl_params->set_prompt(false);
dl_params->set_request_origin(
offline_pages::android::OfflinePageBridge::GetEncodedOriginApp(
web_contents));
dl_params->set_suggested_name(params.suggested_filename);
RecordDownloadSource(DOWNLOAD_INITIATED_BY_CONTEXT_MENU);
dl_params->set_download_source(download::DownloadSource::CONTEXT_MENU);
dlm->DownloadUrl(std::move(dl_params));
}
// Helper class for retrieving a DownloadManager.
class DownloadManagerGetter : public DownloadManager::Observer {
public:
explicit DownloadManagerGetter(DownloadManager* manager) : manager_(manager) {
manager_->AddObserver(this);
}
~DownloadManagerGetter() override {
if (manager_)
manager_->RemoveObserver(this);
}
void ManagerGoingDown(DownloadManager* manager) override {
manager_ = nullptr;
}
DownloadManager* manager() { return manager_; }
private:
DownloadManager* manager_;
DISALLOW_COPY_AND_ASSIGN(DownloadManagerGetter);
};
void RemoveDownloadItem(std::unique_ptr<DownloadManagerGetter> getter,
const std::string& guid) {
if (!getter->manager())
return;
DownloadItem* item = getter->manager()->GetDownloadByGuid(guid);
if (item)
item->Remove();
}
void OnRequestFileAccessResult(
const content::ResourceRequestInfo::WebContentsGetter& web_contents_getter,
DownloadControllerBase::AcquireFileAccessPermissionCallback cb,
bool granted,
const std::string& permission_to_update) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
if (!granted && !permission_to_update.empty() && web_contents_getter.Run()) {
WebContents* web_contents = web_contents_getter.Run();
std::vector<std::string> permissions;
permissions.push_back(permission_to_update);
PermissionUpdateInfoBarDelegate::Create(
web_contents, permissions,
IDS_MISSING_STORAGE_PERMISSION_DOWNLOAD_EDUCATION_TEXT, std::move(cb));
return;
}
if (!granted) {
DownloadController::RecordDownloadCancelReason(
DownloadController::CANCEL_REASON_NO_STORAGE_PERMISSION);
}
std::move(cb).Run(granted);
}
void OnStoragePermissionDecided(
DownloadControllerBase::AcquireFileAccessPermissionCallback cb,
bool granted) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
if (granted) {
DownloadController::RecordStoragePermission(
DownloadController::StoragePermissionType::STORAGE_PERMISSION_GRANTED);
} else {
DownloadController::RecordStoragePermission(
DownloadController::StoragePermissionType::STORAGE_PERMISSION_DENIED);
}
std::move(cb).Run(granted);
}
} // namespace
static void JNI_DownloadController_OnAcquirePermissionResult(
JNIEnv* env,
jlong callback_id,
jboolean granted,
const JavaParamRef<jstring>& jpermission_to_update) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
DCHECK(callback_id);
std::string permission_to_update;
if (jpermission_to_update) {
permission_to_update =
base::android::ConvertJavaStringToUTF8(env, jpermission_to_update);
}
// Convert java long long int to c++ pointer, take ownership.
std::unique_ptr<DownloadController::AcquirePermissionCallback> cb(
reinterpret_cast<DownloadController::AcquirePermissionCallback*>(
callback_id));
std::move(*cb).Run(granted, permission_to_update);
}
// static
DownloadControllerBase* DownloadControllerBase::Get() {
base::AutoLock lock(g_download_controller_lock_.Get());
if (!DownloadControllerBase::download_controller_)
download_controller_ = DownloadController::GetInstance();
return DownloadControllerBase::download_controller_;
}
// static
void DownloadControllerBase::SetDownloadControllerBase(
DownloadControllerBase* download_controller) {
base::AutoLock lock(g_download_controller_lock_.Get());
DownloadControllerBase::download_controller_ = download_controller;
}
// static
void DownloadController::RecordDownloadCancelReason(
DownloadCancelReason reason) {
UMA_HISTOGRAM_ENUMERATION(
"MobileDownload.CancelReason", reason, CANCEL_REASON_MAX);
}
// static
void DownloadController::RecordStoragePermission(StoragePermissionType type) {
UMA_HISTOGRAM_ENUMERATION("MobileDownload.StoragePermission", type,
STORAGE_PERMISSION_MAX);
}
// static
DownloadController* DownloadController::GetInstance() {
return base::Singleton<DownloadController>::get();
}
DownloadController::DownloadController() = default;
DownloadController::~DownloadController() = default;
void DownloadController::AcquireFileAccessPermission(
const content::ResourceRequestInfo::WebContentsGetter& web_contents_getter,
DownloadControllerBase::AcquireFileAccessPermissionCallback cb) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
WebContents* web_contents = web_contents_getter.Run();
if (HasFileAccessPermission()) {
RecordStoragePermission(
StoragePermissionType::STORAGE_PERMISSION_REQUESTED);
RecordStoragePermission(
StoragePermissionType::STORAGE_PERMISSION_NO_ACTION_NEEDED);
base::PostTaskWithTraits(FROM_HERE, {BrowserThread::UI},
base::BindOnce(std::move(cb), true));
return;
} else if (vr::VrTabHelper::IsUiSuppressedInVr(
web_contents,
vr::UiSuppressedElement::kFileAccessPermission)) {
base::PostTaskWithTraits(FROM_HERE, {BrowserThread::UI},
base::BindOnce(std::move(cb), false));
return;
}
RecordStoragePermission(StoragePermissionType::STORAGE_PERMISSION_REQUESTED);
AcquirePermissionCallback callback(base::BindOnce(
&OnRequestFileAccessResult, web_contents_getter,
base::BindOnce(&OnStoragePermissionDecided, base::Passed(&cb))));
// Make copy on the heap so we can pass the pointer through JNI.
intptr_t callback_id = reinterpret_cast<intptr_t>(
new AcquirePermissionCallback(std::move(callback)));
JNIEnv* env = base::android::AttachCurrentThread();
Java_DownloadController_requestFileAccess(env, callback_id);
}
void DownloadController::CreateAndroidDownload(
const content::ResourceRequestInfo::WebContentsGetter& wc_getter,
const DownloadInfo& info) {
base::PostTaskWithTraits(FROM_HERE, {BrowserThread::UI},
base::Bind(&DownloadController::StartAndroidDownload,
base::Unretained(this), wc_getter, info));
}
void DownloadController::AboutToResumeDownload(DownloadItem* download_item) {
download_item->RemoveObserver(this);
download_item->AddObserver(this);
// If a download is resumed from an interrupted state, record its strong
// validators so we know whether the resumption causes a restart.
if (download_item->GetState() == DownloadItem::IN_PROGRESS ||
download_item->GetLastReason() ==
download::DOWNLOAD_INTERRUPT_REASON_NONE) {
return;
}
if (download_item->GetETag().empty() &&
download_item->GetLastModifiedTime().empty()) {
return;
}
strong_validators_map_.emplace(
download_item->GetGuid(),
std::make_pair(download_item->GetETag(),
download_item->GetLastModifiedTime()));
}
void DownloadController::StartAndroidDownload(
const content::ResourceRequestInfo::WebContentsGetter& wc_getter,
const DownloadInfo& info) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
AcquireFileAccessPermission(
wc_getter, base::Bind(&DownloadController::StartAndroidDownloadInternal,
base::Unretained(this), wc_getter, info));
}
void DownloadController::StartAndroidDownloadInternal(
const content::ResourceRequestInfo::WebContentsGetter& wc_getter,
const DownloadInfo& info, bool allowed) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
if (!allowed)
return;
JNIEnv* env = base::android::AttachCurrentThread();
base::string16 file_name =
net::GetSuggestedFilename(info.url, info.content_disposition,
std::string(), // referrer_charset
std::string(), // suggested_name
info.original_mime_type, default_file_name_);
ScopedJavaLocalRef<jstring> jurl =
ConvertUTF8ToJavaString(env, info.url.spec());
ScopedJavaLocalRef<jstring> juser_agent =
ConvertUTF8ToJavaString(env, info.user_agent);
ScopedJavaLocalRef<jstring> jmime_type =
ConvertUTF8ToJavaString(env, info.original_mime_type);
ScopedJavaLocalRef<jstring> jcookie =
ConvertUTF8ToJavaString(env, info.cookie);
ScopedJavaLocalRef<jstring> jreferer =
ConvertUTF8ToJavaString(env, info.referer);
ScopedJavaLocalRef<jstring> jfile_name =
base::android::ConvertUTF16ToJavaString(env, file_name);
Java_DownloadController_enqueueAndroidDownloadManagerRequest(
env, jurl, juser_agent, jfile_name, jmime_type, jcookie, jreferer);
WebContents* web_contents = wc_getter.Run();
if (web_contents) {
TabAndroid* tab = TabAndroid::FromWebContents(web_contents);
if (tab && !tab->GetJavaObject().is_null())
Java_DownloadController_closeTabIfBlank(env, tab->GetJavaObject());
}
}
bool DownloadController::HasFileAccessPermission() {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
JNIEnv* env = base::android::AttachCurrentThread();
return Java_DownloadController_hasFileAccess(env);
}
void DownloadController::OnDownloadStarted(
DownloadItem* download_item) {
// For dangerous item, we need to show the dangerous infobar before the
// download can start.
JNIEnv* env = base::android::AttachCurrentThread();
if (!download_item->IsDangerous())
Java_DownloadController_onDownloadStarted(env);
WebContents* web_contents =
content::DownloadItemUtils::GetWebContents(download_item);
if (web_contents) {
TabAndroid* tab = TabAndroid::FromWebContents(web_contents);
if (tab && !tab->GetJavaObject().is_null()) {
Java_DownloadController_closeTabIfBlank(env, tab->GetJavaObject());
}
}
// Register for updates to the DownloadItem.
download_item->RemoveObserver(this);
download_item->AddObserver(this);
download::AutoResumptionHandler::Get()->OnDownloadStarted(download_item);
OnDownloadUpdated(download_item);
}
void DownloadController::OnDownloadUpdated(DownloadItem* item) {
if (item->IsDangerous() && (item->GetState() != DownloadItem::CANCELLED)) {
// Dont't show notification for a dangerous download, as user can resume
// the download after browser crash through notification.
OnDangerousDownload(item);
return;
}
JNIEnv* env = base::android::AttachCurrentThread();
ScopedJavaLocalRef<jobject> j_item =
DownloadManagerService::CreateJavaDownloadInfo(env, item);
switch (item->GetState()) {
case DownloadItem::IN_PROGRESS: {
Java_DownloadController_onDownloadUpdated(env, j_item);
break;
}
case DownloadItem::COMPLETE:
strong_validators_map_.erase(item->GetGuid());
// Multiple OnDownloadUpdated() notifications may be issued while the
// download is in the COMPLETE state. Only handle one.
item->RemoveObserver(this);
// Call onDownloadCompleted
Java_DownloadController_onDownloadCompleted(env, j_item);
DownloadController::RecordDownloadCancelReason(
DownloadController::CANCEL_REASON_NOT_CANCELED);
break;
case DownloadItem::CANCELLED:
strong_validators_map_.erase(item->GetGuid());
Java_DownloadController_onDownloadCancelled(env, j_item);
DownloadController::RecordDownloadCancelReason(
DownloadController::CANCEL_REASON_OTHER_NATIVE_RESONS);
break;
case DownloadItem::INTERRUPTED:
// When device loses/changes network, we get a NETWORK_TIMEOUT,
// NETWORK_FAILED or NETWORK_DISCONNECTED error. Download should auto
// resume in this case.
Java_DownloadController_onDownloadInterrupted(env, j_item,
IsInterruptedDownloadAutoResumable(item));
break;
case DownloadItem::MAX_DOWNLOAD_STATE:
NOTREACHED();
}
}
void DownloadController::OnDangerousDownload(DownloadItem* item) {
WebContents* web_contents = content::DownloadItemUtils::GetWebContents(item);
if (!web_contents) {
auto download_manager_getter = std::make_unique<DownloadManagerGetter>(
BrowserContext::GetDownloadManager(
content::DownloadItemUtils::GetBrowserContext(item)));
base::PostTaskWithTraits(
FROM_HERE, {BrowserThread::UI},
base::BindOnce(&RemoveDownloadItem, std::move(download_manager_getter),
item->GetGuid()));
item->RemoveObserver(this);
return;
}
DangerousDownloadInfoBarDelegate::Create(
InfoBarService::FromWebContents(web_contents), item);
}
void DownloadController::StartContextMenuDownload(
const ContextMenuParams& params, WebContents* web_contents, bool is_link,
const std::string& extra_headers) {
int process_id = web_contents->GetRenderViewHost()->GetProcess()->GetID();
int routing_id = web_contents->GetRenderViewHost()->GetRoutingID();
const content::ResourceRequestInfo::WebContentsGetter& wc_getter(
base::Bind(&GetWebContents, process_id, routing_id));
AcquireFileAccessPermission(
wc_getter, base::Bind(&CreateContextMenuDownload, wc_getter, params,
is_link, extra_headers));
}
bool DownloadController::IsInterruptedDownloadAutoResumable(
download::DownloadItem* download_item) {
if (!download_item->GetURL().SchemeIsHTTPOrHTTPS())
return false;
static int size_limit = DownloadUtils::GetAutoResumptionSizeLimit();
bool exceeds_size_limit = download_item->GetReceivedBytes() > size_limit;
std::string etag = download_item->GetETag();
std::string last_modified = download_item->GetLastModifiedTime();
if (exceeds_size_limit && etag.empty() && last_modified.empty())
return false;
// If the download has strong validators, but it caused a restart, stop auto
// resumption as the server may always send new strong validators on
// resumption.
auto strong_validator = strong_validators_map_.find(download_item->GetGuid());
if (strong_validator != strong_validators_map_.end()) {
if (exceeds_size_limit &&
(strong_validator->second.first != etag ||
strong_validator->second.second != last_modified)) {
return false;
}
}
int interrupt_reason = download_item->GetLastReason();
DCHECK_NE(interrupt_reason, download::DOWNLOAD_INTERRUPT_REASON_NONE);
return interrupt_reason ==
download::DOWNLOAD_INTERRUPT_REASON_NETWORK_TIMEOUT ||
interrupt_reason ==
download::DOWNLOAD_INTERRUPT_REASON_NETWORK_FAILED ||
interrupt_reason ==
download::DOWNLOAD_INTERRUPT_REASON_NETWORK_DISCONNECTED;
}