// 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.
#include "chrome/browser/offline_pages/android/downloads/offline_page_download_bridge.h"
#include <memory>
#include <string>
#include <utility>
#include <vector>
#include "base/android/jni_string.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/logging.h"
#include "base/memory/ptr_util.h"
#include "base/memory/raw_ptr.h"
#include "base/uuid.h"
#include "chrome/browser/android/profile_key_util.h"
#include "chrome/browser/android/tab_android.h"
#include "chrome/browser/download/android/download_controller_base.h"
#include "chrome/browser/download/android/download_dialog_utils.h"
#include "chrome/browser/download/android/duplicate_download_dialog_bridge.h"
#include "chrome/browser/flags/android/chrome_feature_list.h"
#include "chrome/browser/image_fetcher/image_decoder_impl.h"
#include "chrome/browser/offline_items_collection/offline_content_aggregator_factory.h"
#include "chrome/browser/offline_pages/android/downloads/offline_page_infobar_delegate.h"
#include "chrome/browser/offline_pages/android/downloads/offline_page_share_helper.h"
#include "chrome/browser/offline_pages/offline_page_mhtml_archiver.h"
#include "chrome/browser/offline_pages/offline_page_model_factory.h"
#include "chrome/browser/offline_pages/offline_page_utils.h"
#include "chrome/browser/offline_pages/recent_tab_helper.h"
#include "chrome/browser/offline_pages/request_coordinator_factory.h"
#include "chrome/browser/offline_pages/visuals_decoder_impl.h"
#include "chrome/browser/profiles/incognito_helpers.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profile_key.h"
#include "chrome/browser/transition_manager/full_browser_transition_manager.h"
#include "components/download/public/common/download_url_parameters.h"
#include "components/offline_items_collection/core/offline_content_aggregator.h"
#include "components/offline_items_collection/core/offline_content_provider.h"
#include "components/offline_pages/core/background/request_coordinator.h"
#include "components/offline_pages/core/client_namespace_constants.h"
#include "components/offline_pages/core/downloads/download_ui_adapter.h"
#include "components/offline_pages/core/offline_page_client_policy.h"
#include "components/offline_pages/core/offline_page_feature.h"
#include "components/offline_pages/core/offline_page_item_utils.h"
#include "components/offline_pages/core/offline_page_model.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/download_manager.h"
#include "content/public/browser/download_request_utils.h"
#include "content/public/browser/navigation_entry.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/browser/web_contents.h"
#include "net/base/filename_util.h"
#include "net/traffic_annotation/network_traffic_annotation.h"
#include "url/gurl.h"
// Must come after all headers that specialize FromJniType() / ToJniType().
#include "chrome/android/chrome_jni_headers/OfflinePageDownloadBridge_jni.h"
using base::android::AttachCurrentThread;
using base::android::ConvertJavaStringToUTF8;
using base::android::ConvertUTF8ToJavaString;
using base::android::ConvertUTF16ToJavaString;
using base::android::JavaParamRef;
using base::android::JavaRef;
using base::android::ScopedJavaGlobalRef;
using base::android::ScopedJavaLocalRef;
using ShareCallback =
offline_items_collection::OfflineContentProvider::ShareCallback;
namespace offline_pages {
namespace android {
namespace {
void OnDuplicateDialogConfirmed(base::OnceClosure callback, bool accepted) {
if (accepted)
std::move(callback).Run();
}
void OnShareInfoRetrieved(std::unique_ptr<OfflinePageShareHelper>,
ShareCallback share_callback,
ShareResult result,
const ContentId& id,
std::unique_ptr<OfflineItemShareInfo> info) {
// When |info| is null, the page URL will be used in sharing.
if (result != ShareResult::kFileAccessPermissionDenied)
std::move(share_callback).Run(id, std::move(info));
// TODO(jianli, xingliu): When the permission request was denied by the user
// and "Never ask again" was checked, we'd better show the permission update
// infobar to remind the user. Currently the infobar only works for
// ChromeActivities. We need to investigate how to make it work for other
// activities.
}
class DownloadUIAdapterDelegate : public DownloadUIAdapter::Delegate {
public:
explicit DownloadUIAdapterDelegate(OfflinePageModel* model);
// DownloadUIAdapter::Delegate
bool IsVisibleInUI(const ClientId& client_id) override;
void SetUIAdapter(DownloadUIAdapter* ui_adapter) override;
void OpenItem(
const OfflineItem& item,
int64_t offline_id,
const offline_items_collection::OpenParams& open_params) override;
void GetShareInfoForItem(const ContentId& id,
ShareCallback share_callback) override;
private:
// Not owned, cached service pointer.
raw_ptr<OfflinePageModel> model_;
};
DownloadUIAdapterDelegate::DownloadUIAdapterDelegate(OfflinePageModel* model)
: model_(model) {}
bool DownloadUIAdapterDelegate::IsVisibleInUI(const ClientId& client_id) {
const std::string& name_space = client_id.name_space;
return GetPolicy(name_space).is_supported_by_download &&
base::Uuid::ParseCaseInsensitive(client_id.id).is_valid();
}
void DownloadUIAdapterDelegate::SetUIAdapter(DownloadUIAdapter* ui_adapter) {}
void DownloadUIAdapterDelegate::OpenItem(
const OfflineItem& item,
int64_t offline_id,
const offline_items_collection::OpenParams& open_params) {
JNIEnv* env = AttachCurrentThread();
Java_OfflinePageDownloadBridge_openItem(
env, item.url.spec(), offline_id,
static_cast<int>(open_params.launch_location),
open_params.open_in_incognito,
offline_pages::ShouldOfflinePagesInDownloadHomeOpenInCct());
}
void DownloadUIAdapterDelegate::GetShareInfoForItem(
const ContentId& id,
ShareCallback share_callback) {
auto share_helper = std::make_unique<OfflinePageShareHelper>(model_);
auto* const share_helper_ptr = share_helper.get();
share_helper_ptr->GetShareInfo(
id, base::BindOnce(&OnShareInfoRetrieved, std::move(share_helper),
std::move(share_callback)));
}
// TODO(dewittj): Move to Download UI Adapter.
content::WebContents* GetWebContentsFromJavaTab(
const ScopedJavaGlobalRef<jobject>& j_tab_ref) {
JNIEnv* env = AttachCurrentThread();
TabAndroid* tab = TabAndroid::GetNativeTab(env, j_tab_ref);
if (!tab)
return nullptr;
return tab->web_contents();
}
void SavePageIfNotNavigatedAway(const GURL& url,
const GURL& original_url,
const ScopedJavaGlobalRef<jobject>& j_tab_ref,
const std::string& origin) {
content::WebContents* web_contents = GetWebContentsFromJavaTab(j_tab_ref);
if (!web_contents)
return;
// This ignores fragment differences in URLs, bails out only if tab has
// navigated away and not just scrolled to a fragment.
GURL current_url = web_contents->GetLastCommittedURL();
if (!EqualsIgnoringFragment(current_url, url))
return;
offline_pages::ClientId client_id;
client_id.name_space = offline_pages::kDownloadNamespace;
client_id.id = base::Uuid::GenerateRandomV4().AsLowercaseString();
int64_t request_id = OfflinePageModel::kInvalidOfflineId;
// Post disabled request before passing the download task to the tab helper.
// This will keep the request persisted in case Chrome is evicted from RAM
// or closed by the user.
// Note: the 'disabled' status is not persisted (stored in memory) so it
// automatically resets if Chrome is re-started.
offline_pages::RequestCoordinator* request_coordinator =
offline_pages::RequestCoordinatorFactory::GetForBrowserContext(
web_contents->GetBrowserContext());
if (request_coordinator) {
offline_pages::RequestCoordinator::SavePageLaterParams params;
params.url = current_url;
params.client_id = client_id;
params.availability =
RequestCoordinator::RequestAvailability::DISABLED_FOR_OFFLINER;
params.original_url = original_url;
params.request_origin = origin;
request_id = request_coordinator->SavePageLater(params, base::DoNothing());
} else {
DVLOG(1) << "SavePageIfNotNavigatedAway has no valid coordinator.";
}
// Pass request_id to the current tab's helper to attempt download right from
// the tab. If unsuccessful, it'll enable the already-queued request for
// background offliner. Same will happen if Chrome is terminated since
// 'disabled' status of the request is RAM-stored info.
offline_pages::RecentTabHelper* tab_helper =
RecentTabHelper::FromWebContents(web_contents);
if (!tab_helper) {
if (request_id != OfflinePageModel::kInvalidOfflineId &&
request_coordinator) {
request_coordinator->EnableForOffliner(request_id, client_id);
}
return;
}
tab_helper->ObserveAndDownloadCurrentPage(client_id, request_id, origin);
OfflinePageDownloadBridge::ShowDownloadingToast();
}
void DuplicateCheckDone(const GURL& url,
const GURL& original_url,
const ScopedJavaGlobalRef<jobject>& j_tab_ref,
const std::string& origin,
OfflinePageUtils::DuplicateCheckResult result) {
if (result == OfflinePageUtils::DuplicateCheckResult::NOT_FOUND) {
SavePageIfNotNavigatedAway(url, original_url, j_tab_ref, origin);
return;
}
content::WebContents* web_contents = GetWebContentsFromJavaTab(j_tab_ref);
if (!web_contents)
return;
bool duplicate_request_exists =
result == OfflinePageUtils::DuplicateCheckResult::DUPLICATE_REQUEST_FOUND;
base::OnceClosure callback = base::BindOnce(&SavePageIfNotNavigatedAway, url,
original_url, j_tab_ref, origin);
DuplicateDownloadDialogBridge::GetInstance()->Show(
url.spec(), DownloadDialogUtils::GetDisplayURLForPageURL(url),
-1 /*total_bytes*/, duplicate_request_exists, web_contents,
base::BindOnce(&OnDuplicateDialogConfirmed, std::move(callback)));
}
content::WebContents* GetWebContentsByFrameID(int render_process_id,
int render_frame_id) {
content::RenderFrameHost* render_frame_host =
content::RenderFrameHost::FromID(render_process_id, render_frame_id);
if (!render_frame_host)
return NULL;
return content::WebContents::FromRenderFrameHost(render_frame_host);
}
content::WebContents::Getter GetWebContentsGetter(
content::WebContents* web_contents) {
// The FrameTreeNode ID should be used to access the WebContents.
int frame_tree_node_id =
web_contents->GetPrimaryMainFrame()->GetFrameTreeNodeId();
if (frame_tree_node_id != content::RenderFrameHost::kNoFrameTreeNodeId) {
return base::BindRepeating(content::WebContents::FromFrameTreeNodeId,
frame_tree_node_id);
}
// In other cases, use the RenderProcessHost ID + RenderFrameHost ID to get
// the WebContents.
return base::BindRepeating(
&GetWebContentsByFrameID,
web_contents->GetPrimaryMainFrame()->GetProcess()->GetID(),
web_contents->GetPrimaryMainFrame()->GetRoutingID());
}
void DownloadAsFile(content::WebContents* web_contents, const GURL& url) {
content::DownloadManager* dlm =
web_contents->GetBrowserContext()->GetDownloadManager();
std::unique_ptr<download::DownloadUrlParameters> dl_params(
content::DownloadRequestUtils::CreateDownloadForWebContentsMainFrame(
web_contents, url,
TRAFFIC_ANNOTATION_WITHOUT_PROTO("Offline pages download file")));
content::NavigationEntry* entry =
web_contents->GetController().GetLastCommittedEntry();
// |entry| should not be null since otherwise an empty URL is returned from
// calling GetLastCommittedURL and we should bail out earlier.
DCHECK(entry);
content::Referrer referrer =
content::Referrer::SanitizeForRequest(url, entry->GetReferrer());
dl_params->set_referrer(referrer.url);
dl_params->set_referrer_policy(
content::Referrer::ReferrerPolicyForUrlRequest(referrer.policy));
dl_params->set_prompt(false);
dl_params->set_download_source(download::DownloadSource::OFFLINE_PAGE);
dlm->DownloadUrl(std::move(dl_params));
}
void OnOfflinePageAcquireFileAccessPermissionDone(
const content::WebContents::Getter& web_contents_getter,
const ScopedJavaGlobalRef<jobject>& j_tab_ref,
const std::string& origin,
bool granted) {
if (!granted)
return;
content::WebContents* web_contents = web_contents_getter.Run();
if (!web_contents)
return;
GURL url = web_contents->GetLastCommittedURL();
if (url.is_empty())
return;
// If the page is not a HTML page, route to DownloadManager.
if (!offline_pages::OfflinePageUtils::CanDownloadAsOfflinePage(
url, web_contents->GetContentsMimeType())) {
DownloadAsFile(web_contents, url);
return;
}
// Otherwise, save the HTML page as archive.
GURL original_url =
offline_pages::OfflinePageUtils::GetOriginalURLFromWebContents(
web_contents);
OfflinePageUtils::CheckDuplicateDownloads(
chrome::GetBrowserContextRedirectedInIncognito(
web_contents->GetBrowserContext()),
url,
base::BindOnce(&DuplicateCheckDone, url, original_url, j_tab_ref,
origin));
}
void InitializeBackendOnProfileCreated(Profile* profile) {
// Even if |profile| is incognito, use the regular one since downloads are
// shared between them.
profile = profile->GetOriginalProfile();
OfflinePageModel* offline_page_model =
OfflinePageModelFactory::GetForBrowserContext(profile);
DCHECK(offline_page_model);
DownloadUIAdapter* adapter =
DownloadUIAdapter::FromOfflinePageModel(offline_page_model);
if (!adapter) {
RequestCoordinator* request_coordinator =
RequestCoordinatorFactory::GetForBrowserContext(profile);
DCHECK(request_coordinator);
offline_items_collection::OfflineContentAggregator* aggregator =
OfflineContentAggregatorFactory::GetForKey(profile->GetProfileKey());
DCHECK(aggregator);
adapter = new DownloadUIAdapter(
aggregator, offline_page_model, request_coordinator,
std::make_unique<VisualsDecoderImpl>(
std::make_unique<ImageDecoderImpl>()),
std::make_unique<DownloadUIAdapterDelegate>(offline_page_model));
DownloadUIAdapter::AttachToOfflinePageModel(base::WrapUnique(adapter),
offline_page_model);
}
}
} // namespace
OfflinePageDownloadBridge::OfflinePageDownloadBridge(
JNIEnv* env,
const JavaParamRef<jobject>& obj)
: weak_java_ref_(env, obj) {}
OfflinePageDownloadBridge::~OfflinePageDownloadBridge() {}
void OfflinePageDownloadBridge::Destroy(JNIEnv* env,
const JavaParamRef<jobject>&) {
delete this;
}
void JNI_OfflinePageDownloadBridge_StartDownload(
JNIEnv* env,
const JavaParamRef<jobject>& j_tab,
std::string& origin) {
TabAndroid* tab = TabAndroid::GetNativeTab(env, j_tab);
if (!tab)
return;
content::WebContents* web_contents = tab->web_contents();
if (!web_contents)
return;
ScopedJavaGlobalRef<jobject> j_tab_ref(env, j_tab);
// Ensure that the storage permission is granted since the target file
// is going to be placed in the public directory.
content::WebContents::Getter web_contents_getter =
GetWebContentsGetter(web_contents);
DownloadControllerBase::Get()->AcquireFileAccessPermission(
web_contents_getter,
base::BindOnce(&OnOfflinePageAcquireFileAccessPermissionDone,
web_contents_getter, j_tab_ref, origin));
}
static jlong JNI_OfflinePageDownloadBridge_Init(
JNIEnv* env,
const JavaParamRef<jobject>& obj) {
ProfileKey* key = ::android::GetLastUsedRegularProfileKey();
FullBrowserTransitionManager::Get()->RegisterCallbackOnProfileCreation(
key, base::BindOnce(&InitializeBackendOnProfileCreated));
return reinterpret_cast<jlong>(new OfflinePageDownloadBridge(env, obj));
}
// static
void OfflinePageDownloadBridge::ShowDownloadingToast() {
JNIEnv* env = AttachCurrentThread();
Java_OfflinePageDownloadBridge_showDownloadingToast(env);
}
} // namespace android
} // namespace offline_pages