| // 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/download/android/download_controller.h" |
| |
| #include <memory> |
| #include <utility> |
| #include <vector> |
| |
| #include "base/android/jni_android.h" |
| #include "base/android/jni_string.h" |
| #include "base/bind.h" |
| #include "base/check_op.h" |
| #include "base/feature_list.h" |
| #include "base/lazy_instance.h" |
| #include "base/metrics/field_trial_params.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/notreached.h" |
| #include "base/synchronization/lock.h" |
| #include "chrome/android/chrome_jni_headers/DownloadController_jni.h" |
| #include "chrome/browser/android/profile_key_startup_accessor.h" |
| #include "chrome/browser/android/profile_key_util.h" |
| #include "chrome/browser/android/tab_android.h" |
| #include "chrome/browser/download/android/dangerous_download_infobar_delegate.h" |
| #include "chrome/browser/download/android/download_manager_service.h" |
| #include "chrome/browser/download/android/download_utils.h" |
| #include "chrome/browser/download/download_offline_content_provider.h" |
| #include "chrome/browser/download/download_offline_content_provider_factory.h" |
| #include "chrome/browser/download/download_stats.h" |
| #include "chrome/browser/flags/android/chrome_feature_list.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/tab_model/tab_model.h" |
| #include "chrome/browser/ui/android/tab_model/tab_model_list.h" |
| #include "chrome/browser/vr/vr_tab_helper.h" |
| #include "chrome/grit/chromium_strings.h" |
| #include "components/download/content/public/context_menu_download.h" |
| #include "components/download/public/common/auto_resumption_handler.h" |
| #include "components/download/public/common/download_features.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/render_process_host.h" |
| #include "content/public/browser/render_view_host.h" |
| #include "net/base/filename_util.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 content::DownloadManager; |
| using content::WebContents; |
| using download::DownloadItem; |
| |
| namespace { |
| // Guards download_controller_ |
| base::LazyInstance<base::Lock>::DestructorAtExit g_download_controller_lock_; |
| |
| void CreateContextMenuDownloadInternal( |
| const content::WebContents::Getter& wc_getter, |
| const content::ContextMenuParams& params, |
| bool is_link, |
| 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; |
| } |
| |
| RecordDownloadSource(DOWNLOAD_INITIATED_BY_CONTEXT_MENU); |
| auto origin = offline_pages::android::OfflinePageBridge::GetEncodedOriginApp( |
| web_contents); |
| download::CreateContextMenuDownload(web_contents, params, origin, is_link); |
| } |
| |
| // 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::WebContents::Getter& 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; |
| } |
| |
| 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::RecordStoragePermission(StoragePermissionType type) { |
| UMA_HISTOGRAM_ENUMERATION("MobileDownload.StoragePermission", type, |
| STORAGE_PERMISSION_MAX); |
| } |
| |
| // static |
| void DownloadController::CloseTabIfEmpty(content::WebContents* web_contents) { |
| if (!web_contents || !web_contents->GetController().IsInitialNavigation()) |
| return; |
| |
| TabModel* tab_model = TabModelList::GetTabModelForWebContents(web_contents); |
| if (!tab_model || tab_model->GetTabCount() == 1) |
| return; |
| |
| int tab_index = -1; |
| for (int index = 0; index < tab_model->GetTabCount(); ++index) { |
| if (web_contents == tab_model->GetWebContentsAt(index)) { |
| tab_index = index; |
| break; |
| } |
| } |
| |
| if (tab_index == -1) |
| return; |
| |
| tab_model->CloseTabAt(tab_index); |
| } |
| |
| // static |
| DownloadController* DownloadController::GetInstance() { |
| return base::Singleton<DownloadController>::get(); |
| } |
| |
| DownloadController::DownloadController() = default; |
| |
| DownloadController::~DownloadController() = default; |
| |
| void DownloadController::AcquireFileAccessPermission( |
| const content::WebContents::Getter& 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); |
| content::GetUIThreadTaskRunner({})->PostTask( |
| FROM_HERE, base::BindOnce(std::move(cb), true)); |
| return; |
| } else if (vr::VrTabHelper::IsUiSuppressedInVr( |
| web_contents, |
| vr::UiSuppressedElement::kFileAccessPermission)) { |
| content::GetUIThreadTaskRunner({})->PostTask( |
| FROM_HERE, base::BindOnce(std::move(cb), false)); |
| return; |
| } |
| |
| RecordStoragePermission(StoragePermissionType::STORAGE_PERMISSION_REQUESTED); |
| AcquirePermissionCallback callback(base::BindOnce( |
| &OnRequestFileAccessResult, web_contents_getter, |
| base::BindOnce(&OnStoragePermissionDecided, std::move(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::WebContents::Getter& wc_getter, |
| const DownloadInfo& info) { |
| content::GetUIThreadTaskRunner({})->PostTask( |
| FROM_HERE, base::BindOnce(&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::WebContents::Getter& wc_getter, |
| const DownloadInfo& info) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| |
| AcquireFileAccessPermission( |
| wc_getter, |
| base::BindOnce(&DownloadController::StartAndroidDownloadInternal, |
| base::Unretained(this), wc_getter, info)); |
| } |
| |
| void DownloadController::StartAndroidDownloadInternal( |
| const content::WebContents::Getter& wc_getter, |
| const DownloadInfo& info, |
| bool allowed) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| if (!allowed) |
| return; |
| |
| JNIEnv* env = base::android::AttachCurrentThread(); |
| std::u16string 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(); |
| CloseTabIfEmpty(web_contents); |
| } |
| |
| 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 downloads, 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); |
| |
| // Register for updates to the DownloadItem. |
| download_item->RemoveObserver(this); |
| download_item->AddObserver(this); |
| |
| if (download::AutoResumptionHandler::Get()) |
| download::AutoResumptionHandler::Get()->OnDownloadStarted(download_item); |
| |
| ProfileKey* profile_key = GetProfileKey(download_item); |
| if (!profile_key) |
| return; |
| |
| DownloadOfflineContentProviderFactory::GetForKey(profile_key) |
| ->OnDownloadStarted(download_item); |
| |
| OnDownloadUpdated(download_item); |
| } |
| |
| void DownloadController::OnDownloadUpdated(DownloadItem* item) { |
| if (item->IsTemporary() || item->IsTransient()) |
| return; |
| |
| 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); |
| break; |
| case DownloadItem::CANCELLED: |
| strong_validators_map_.erase(item->GetGuid()); |
| Java_DownloadController_onDownloadCancelled(env, j_item); |
| break; |
| case DownloadItem::INTERRUPTED: |
| if (item->IsDone()) |
| strong_validators_map_.erase(item->GetGuid()); |
| // 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))); |
| content::GetUIThreadTaskRunner({})->PostTask( |
| FROM_HERE, |
| 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) { |
| int process_id = web_contents->GetRenderViewHost()->GetProcess()->GetID(); |
| int routing_id = web_contents->GetRenderViewHost()->GetRoutingID(); |
| |
| const content::WebContents::Getter& wc_getter( |
| base::BindRepeating(&GetWebContents, process_id, routing_id)); |
| |
| AcquireFileAccessPermission( |
| wc_getter, base::BindOnce(&CreateContextMenuDownloadInternal, wc_getter, |
| params, is_link)); |
| } |
| |
| 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() && |
| !base::FeatureList::IsEnabled( |
| download::features:: |
| kAllowDownloadResumptionWithoutStrongValidators)) { |
| 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; |
| } |
| |
| ProfileKey* DownloadController::GetProfileKey(DownloadItem* download_item) { |
| Profile* profile = Profile::FromBrowserContext( |
| content::DownloadItemUtils::GetBrowserContext(download_item)); |
| |
| ProfileKey* profile_key; |
| if (profile) |
| profile_key = profile->GetProfileKey(); |
| else |
| profile_key = ProfileKeyStartupAccessor::GetInstance()->profile_key(); |
| |
| return profile_key; |
| } |