2024-04-26 21:21:33 +02:00
|
|
|
// Copyright (c) 2012 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.
|
|
|
|
|
2024-04-30 17:45:07 +02:00
|
|
|
#include "cef/libcef/browser/download_manager_delegate_impl.h"
|
2024-04-26 21:21:33 +02:00
|
|
|
|
|
|
|
#include <tuple>
|
|
|
|
|
|
|
|
#include "base/files/file_util.h"
|
|
|
|
#include "base/functional/bind.h"
|
|
|
|
#include "base/logging.h"
|
|
|
|
#include "base/path_service.h"
|
|
|
|
#include "base/strings/string_util.h"
|
|
|
|
#include "base/strings/utf_string_conversions.h"
|
2024-04-30 17:45:07 +02:00
|
|
|
#include "cef/include/cef_download_handler.h"
|
|
|
|
#include "cef/libcef/browser/browser_host_base.h"
|
|
|
|
#include "cef/libcef/browser/context.h"
|
|
|
|
#include "cef/libcef/browser/download_item_impl.h"
|
|
|
|
#include "cef/libcef/browser/thread_util.h"
|
2024-04-26 21:21:33 +02:00
|
|
|
#include "content/public/browser/browser_context.h"
|
|
|
|
#include "content/public/browser/download_item_utils.h"
|
|
|
|
#include "content/public/browser/web_contents.h"
|
|
|
|
#include "net/base/filename_util.h"
|
|
|
|
#include "third_party/blink/public/mojom/choosers/file_chooser.mojom.h"
|
|
|
|
|
|
|
|
using content::DownloadManager;
|
|
|
|
using content::WebContents;
|
|
|
|
using download::DownloadItem;
|
|
|
|
|
|
|
|
namespace {
|
|
|
|
|
|
|
|
// Helper function to retrieve the CefDownloadHandler.
|
|
|
|
CefRefPtr<CefDownloadHandler> GetDownloadHandler(
|
|
|
|
CefRefPtr<CefBrowserHostBase> browser) {
|
|
|
|
CefRefPtr<CefClient> client = browser->GetClient();
|
|
|
|
if (client.get()) {
|
|
|
|
return client->GetDownloadHandler();
|
|
|
|
}
|
|
|
|
return nullptr;
|
|
|
|
}
|
|
|
|
|
|
|
|
void RunDownloadTargetCallback(download::DownloadTargetCallback callback,
|
|
|
|
const base::FilePath& path) {
|
|
|
|
download::DownloadTargetInfo target_info;
|
|
|
|
target_info.target_path = path;
|
|
|
|
target_info.intermediate_path = path;
|
|
|
|
std::move(callback).Run(std::move(target_info));
|
|
|
|
}
|
|
|
|
|
|
|
|
// CefBeforeDownloadCallback implementation.
|
|
|
|
class CefBeforeDownloadCallbackImpl : public CefBeforeDownloadCallback {
|
|
|
|
public:
|
|
|
|
CefBeforeDownloadCallbackImpl(const base::WeakPtr<DownloadManager>& manager,
|
|
|
|
uint32_t download_id,
|
|
|
|
const base::FilePath& suggested_name,
|
|
|
|
download::DownloadTargetCallback callback)
|
|
|
|
: manager_(manager),
|
|
|
|
download_id_(download_id),
|
|
|
|
suggested_name_(suggested_name),
|
|
|
|
callback_(std::move(callback)) {}
|
|
|
|
|
|
|
|
CefBeforeDownloadCallbackImpl(const CefBeforeDownloadCallbackImpl&) = delete;
|
|
|
|
CefBeforeDownloadCallbackImpl& operator=(
|
|
|
|
const CefBeforeDownloadCallbackImpl&) = delete;
|
|
|
|
|
|
|
|
void Continue(const CefString& download_path, bool show_dialog) override {
|
|
|
|
if (CEF_CURRENTLY_ON_UIT()) {
|
|
|
|
if (download_id_ <= 0) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (manager_) {
|
|
|
|
base::FilePath path = base::FilePath(download_path);
|
|
|
|
CEF_POST_USER_VISIBLE_TASK(
|
|
|
|
base::BindOnce(&CefBeforeDownloadCallbackImpl::GenerateFilename,
|
|
|
|
manager_, download_id_, suggested_name_, path,
|
|
|
|
show_dialog, std::move(callback_)));
|
|
|
|
}
|
|
|
|
|
|
|
|
download_id_ = 0;
|
|
|
|
} else {
|
|
|
|
CEF_POST_TASK(CEF_UIT,
|
|
|
|
base::BindOnce(&CefBeforeDownloadCallbackImpl::Continue,
|
|
|
|
this, download_path, show_dialog));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
bool IsDetached() const { return callback_.is_null(); }
|
|
|
|
[[nodiscard]] download::DownloadTargetCallback Detach() {
|
|
|
|
return std::move(callback_);
|
|
|
|
}
|
|
|
|
|
|
|
|
private:
|
|
|
|
static void GenerateFilename(base::WeakPtr<DownloadManager> manager,
|
|
|
|
uint32_t download_id,
|
|
|
|
const base::FilePath& suggested_name,
|
|
|
|
const base::FilePath& download_path,
|
|
|
|
bool show_dialog,
|
|
|
|
download::DownloadTargetCallback callback) {
|
|
|
|
CEF_REQUIRE_BLOCKING();
|
|
|
|
|
|
|
|
base::FilePath suggested_path = download_path;
|
|
|
|
if (!suggested_path.empty()) {
|
|
|
|
// Create the directory if necessary.
|
|
|
|
base::FilePath dir_path = suggested_path.DirName();
|
|
|
|
if (!base::DirectoryExists(dir_path) &&
|
|
|
|
!base::CreateDirectory(dir_path)) {
|
|
|
|
DCHECK(false) << "failed to create the download directory";
|
|
|
|
suggested_path.clear();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (suggested_path.empty()) {
|
|
|
|
if (base::PathService::Get(base::DIR_TEMP, &suggested_path)) {
|
|
|
|
// Use the temp directory.
|
|
|
|
suggested_path = suggested_path.Append(suggested_name);
|
|
|
|
} else {
|
|
|
|
// Use the current working directory.
|
|
|
|
suggested_path = suggested_name;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
CEF_POST_TASK(
|
|
|
|
CEF_UIT,
|
|
|
|
base::BindOnce(&CefBeforeDownloadCallbackImpl::ChooseDownloadPath,
|
|
|
|
manager, download_id, suggested_path, show_dialog,
|
|
|
|
std::move(callback)));
|
|
|
|
}
|
|
|
|
|
|
|
|
static void ChooseDownloadPath(base::WeakPtr<DownloadManager> manager,
|
|
|
|
uint32_t download_id,
|
|
|
|
const base::FilePath& suggested_path,
|
|
|
|
bool show_dialog,
|
|
|
|
download::DownloadTargetCallback callback) {
|
|
|
|
if (!manager) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
DownloadItem* item = manager->GetDownload(download_id);
|
|
|
|
if (!item || item->GetState() != DownloadItem::IN_PROGRESS) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool handled = false;
|
|
|
|
|
|
|
|
if (show_dialog) {
|
|
|
|
WebContents* web_contents =
|
|
|
|
content::DownloadItemUtils::GetWebContents(item);
|
|
|
|
CefRefPtr<CefBrowserHostBase> browser =
|
|
|
|
CefBrowserHostBase::GetBrowserForContents(web_contents);
|
|
|
|
if (browser.get()) {
|
|
|
|
handled = true;
|
|
|
|
|
|
|
|
blink::mojom::FileChooserParams params;
|
|
|
|
params.mode = blink::mojom::FileChooserParams::Mode::kSave;
|
|
|
|
if (!suggested_path.empty()) {
|
|
|
|
params.default_file_name = suggested_path;
|
|
|
|
if (!suggested_path.Extension().empty()) {
|
|
|
|
params.accept_types.push_back(
|
|
|
|
CefString(suggested_path.Extension()));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
browser->RunFileChooserForBrowser(
|
|
|
|
params,
|
|
|
|
base::BindOnce(
|
|
|
|
&CefBeforeDownloadCallbackImpl::ChooseDownloadPathCallback,
|
|
|
|
std::move(callback)));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!handled) {
|
|
|
|
RunDownloadTargetCallback(std::move(callback), suggested_path);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
static void ChooseDownloadPathCallback(
|
|
|
|
download::DownloadTargetCallback callback,
|
|
|
|
const std::vector<base::FilePath>& file_paths) {
|
|
|
|
DCHECK_LE(file_paths.size(), (size_t)1);
|
|
|
|
|
|
|
|
base::FilePath path;
|
|
|
|
if (file_paths.size() > 0) {
|
|
|
|
path = file_paths.front();
|
|
|
|
}
|
|
|
|
|
|
|
|
// The download will be cancelled if |path| is empty.
|
|
|
|
RunDownloadTargetCallback(std::move(callback), path);
|
|
|
|
}
|
|
|
|
|
|
|
|
base::WeakPtr<DownloadManager> manager_;
|
|
|
|
uint32_t download_id_;
|
|
|
|
base::FilePath suggested_name_;
|
|
|
|
download::DownloadTargetCallback callback_;
|
|
|
|
|
|
|
|
IMPLEMENT_REFCOUNTING(CefBeforeDownloadCallbackImpl);
|
|
|
|
};
|
|
|
|
|
|
|
|
// CefDownloadItemCallback implementation.
|
|
|
|
class CefDownloadItemCallbackImpl : public CefDownloadItemCallback {
|
|
|
|
public:
|
|
|
|
explicit CefDownloadItemCallbackImpl(
|
|
|
|
const base::WeakPtr<DownloadManager>& manager,
|
|
|
|
uint32_t download_id)
|
|
|
|
: manager_(manager), download_id_(download_id) {}
|
|
|
|
|
|
|
|
CefDownloadItemCallbackImpl(const CefDownloadItemCallbackImpl&) = delete;
|
|
|
|
CefDownloadItemCallbackImpl& operator=(const CefDownloadItemCallbackImpl&) =
|
|
|
|
delete;
|
|
|
|
|
|
|
|
void Cancel() override {
|
|
|
|
CEF_POST_TASK(CEF_UIT,
|
|
|
|
base::BindOnce(&CefDownloadItemCallbackImpl::DoCancel, this));
|
|
|
|
}
|
|
|
|
|
|
|
|
void Pause() override {
|
|
|
|
CEF_POST_TASK(CEF_UIT,
|
|
|
|
base::BindOnce(&CefDownloadItemCallbackImpl::DoPause, this));
|
|
|
|
}
|
|
|
|
|
|
|
|
void Resume() override {
|
|
|
|
CEF_POST_TASK(CEF_UIT,
|
|
|
|
base::BindOnce(&CefDownloadItemCallbackImpl::DoResume, this));
|
|
|
|
}
|
|
|
|
|
|
|
|
private:
|
|
|
|
void DoCancel() {
|
|
|
|
if (download_id_ <= 0) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (manager_) {
|
|
|
|
DownloadItem* item = manager_->GetDownload(download_id_);
|
|
|
|
if (item && item->GetState() == DownloadItem::IN_PROGRESS) {
|
|
|
|
item->Cancel(true);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
download_id_ = 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
void DoPause() {
|
|
|
|
if (download_id_ <= 0) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (manager_) {
|
|
|
|
DownloadItem* item = manager_->GetDownload(download_id_);
|
|
|
|
if (item && item->GetState() == DownloadItem::IN_PROGRESS) {
|
|
|
|
item->Pause();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void DoResume() {
|
|
|
|
if (download_id_ <= 0) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (manager_) {
|
|
|
|
DownloadItem* item = manager_->GetDownload(download_id_);
|
|
|
|
if (item && item->CanResume()) {
|
|
|
|
item->Resume(true);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
base::WeakPtr<DownloadManager> manager_;
|
|
|
|
uint32_t download_id_;
|
|
|
|
|
|
|
|
IMPLEMENT_REFCOUNTING(CefDownloadItemCallbackImpl);
|
|
|
|
};
|
|
|
|
|
|
|
|
} // namespace
|
|
|
|
|
|
|
|
CefDownloadManagerDelegateImpl::CefDownloadManagerDelegateImpl(
|
|
|
|
DownloadManager* manager,
|
|
|
|
bool alloy_bootstrap)
|
|
|
|
: manager_(manager),
|
|
|
|
manager_ptr_factory_(manager),
|
|
|
|
alloy_bootstrap_(alloy_bootstrap) {
|
|
|
|
DCHECK(manager);
|
|
|
|
manager->AddObserver(this);
|
|
|
|
|
|
|
|
DownloadManager::DownloadVector items;
|
|
|
|
manager->GetAllDownloads(&items);
|
|
|
|
DownloadManager::DownloadVector::const_iterator it = items.begin();
|
|
|
|
for (; it != items.end(); ++it) {
|
|
|
|
OnDownloadCreated(manager, *it);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
CefDownloadManagerDelegateImpl::~CefDownloadManagerDelegateImpl() {
|
|
|
|
ResetManager();
|
|
|
|
}
|
|
|
|
|
|
|
|
void CefDownloadManagerDelegateImpl::OnDownloadUpdated(DownloadItem* download) {
|
|
|
|
CefRefPtr<CefBrowserHostBase> browser = GetBrowser(download);
|
|
|
|
CefRefPtr<CefDownloadHandler> handler;
|
|
|
|
if (browser.get()) {
|
|
|
|
handler = GetDownloadHandler(browser);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (handler.get()) {
|
|
|
|
CefRefPtr<CefDownloadItemImpl> download_item(
|
|
|
|
new CefDownloadItemImpl(download));
|
|
|
|
CefRefPtr<CefDownloadItemCallback> callback(new CefDownloadItemCallbackImpl(
|
|
|
|
manager_ptr_factory_.GetWeakPtr(), download->GetId()));
|
|
|
|
|
|
|
|
handler->OnDownloadUpdated(browser.get(), download_item.get(), callback);
|
|
|
|
|
|
|
|
std::ignore = download_item->Detach(nullptr);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void CefDownloadManagerDelegateImpl::OnDownloadDestroyed(DownloadItem* item) {
|
|
|
|
item->RemoveObserver(this);
|
|
|
|
|
|
|
|
CefBrowserHostBase* browser = nullptr;
|
|
|
|
|
|
|
|
ItemBrowserMap::iterator it = item_browser_map_.find(item);
|
|
|
|
DCHECK(it != item_browser_map_.end());
|
|
|
|
if (it != item_browser_map_.end()) {
|
|
|
|
browser = it->second;
|
|
|
|
item_browser_map_.erase(it);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (browser) {
|
|
|
|
// Determine if any remaining DownloadItems are associated with the same
|
|
|
|
// browser. If not, then unregister as an observer.
|
|
|
|
bool has_remaining = false;
|
|
|
|
ItemBrowserMap::const_iterator it2 = item_browser_map_.begin();
|
|
|
|
for (; it2 != item_browser_map_.end(); ++it2) {
|
|
|
|
if (it2->second == browser) {
|
|
|
|
has_remaining = true;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!has_remaining) {
|
|
|
|
browser->RemoveObserver(this);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void CefDownloadManagerDelegateImpl::OnDownloadCreated(DownloadManager* manager,
|
|
|
|
DownloadItem* item) {
|
|
|
|
// This callback may arrive after DetermineDownloadTarget, so we allow
|
|
|
|
// association from either method.
|
|
|
|
CefRefPtr<CefBrowserHostBase> browser = GetOrAssociateBrowser(item);
|
|
|
|
if (!browser) {
|
|
|
|
// If the download is rejected (e.g. ALT+click on an invalid protocol link)
|
|
|
|
// then an "interrupted" download will be started via DownloadManagerImpl::
|
|
|
|
// StartDownloadWithId (originating from CreateInterruptedDownload) with no
|
|
|
|
// associated WebContents and consequently no associated CEF browser. In
|
|
|
|
// that case DetermineDownloadTarget will be called before this method.
|
|
|
|
// TODO(cef): Figure out how to expose this via a client callback.
|
|
|
|
const std::vector<GURL>& url_chain = item->GetUrlChain();
|
|
|
|
if (!url_chain.empty()) {
|
|
|
|
LOG(INFO) << "Rejected download of " << url_chain.back().spec();
|
|
|
|
}
|
|
|
|
item->Cancel(true);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void CefDownloadManagerDelegateImpl::ManagerGoingDown(
|
|
|
|
DownloadManager* manager) {
|
|
|
|
DCHECK_EQ(manager, manager_);
|
|
|
|
ResetManager();
|
|
|
|
}
|
|
|
|
|
|
|
|
bool CefDownloadManagerDelegateImpl::DetermineDownloadTarget(
|
|
|
|
DownloadItem* item,
|
|
|
|
download::DownloadTargetCallback* callback) {
|
|
|
|
if (alloy_bootstrap_) {
|
|
|
|
const auto& forced_path = item->GetForcedFilePath();
|
|
|
|
if (!forced_path.empty()) {
|
|
|
|
RunDownloadTargetCallback(std::move(*callback), forced_path);
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// This callback may arrive before OnDownloadCreated, so we allow association
|
|
|
|
// from either method.
|
|
|
|
CefRefPtr<CefBrowserHostBase> browser = GetOrAssociateBrowser(item);
|
|
|
|
if (!browser) {
|
|
|
|
// Cancel by default with Alloy bootstrap.
|
|
|
|
return alloy_bootstrap_;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool handled = false;
|
|
|
|
CefRefPtr<CefDownloadHandler> handler = GetDownloadHandler(browser);
|
|
|
|
if (handler) {
|
|
|
|
base::FilePath suggested_name = net::GenerateFileName(
|
|
|
|
item->GetURL(), item->GetContentDisposition(), std::string(),
|
|
|
|
item->GetSuggestedFilename(), item->GetMimeType(), "download");
|
|
|
|
|
|
|
|
CefRefPtr<CefDownloadItemImpl> download_item(new CefDownloadItemImpl(item));
|
|
|
|
CefRefPtr<CefBeforeDownloadCallbackImpl> callbackObj(
|
|
|
|
new CefBeforeDownloadCallbackImpl(manager_ptr_factory_.GetWeakPtr(),
|
|
|
|
item->GetId(), suggested_name,
|
|
|
|
std::move(*callback)));
|
|
|
|
|
|
|
|
handled =
|
|
|
|
handler->OnBeforeDownload(browser.get(), download_item.get(),
|
|
|
|
suggested_name.value(), callbackObj.get());
|
|
|
|
if (!handled && callbackObj->IsDetached()) {
|
|
|
|
LOG(ERROR) << "Should return true from OnBeforeDownload when executing "
|
|
|
|
"the callback";
|
|
|
|
handled = true;
|
|
|
|
}
|
|
|
|
if (!handled) {
|
|
|
|
*callback = callbackObj->Detach();
|
|
|
|
}
|
|
|
|
|
|
|
|
std::ignore = download_item->Detach(nullptr);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Cancel by default with Alloy style.
|
|
|
|
return handled ? true : alloy_bootstrap_;
|
|
|
|
}
|
|
|
|
|
|
|
|
void CefDownloadManagerDelegateImpl::OnBrowserDestroyed(
|
|
|
|
CefBrowserHostBase* browser) {
|
|
|
|
ItemBrowserMap::iterator it = item_browser_map_.begin();
|
|
|
|
for (; it != item_browser_map_.end(); ++it) {
|
|
|
|
if (it->second == browser) {
|
|
|
|
// Don't call back into browsers that have been destroyed. We're not
|
|
|
|
// canceling the download so it will continue silently until it completes
|
|
|
|
// or until the associated browser context is destroyed.
|
|
|
|
it->second = nullptr;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
CefRefPtr<CefBrowserHostBase>
|
|
|
|
CefDownloadManagerDelegateImpl::GetOrAssociateBrowser(
|
|
|
|
download::DownloadItem* item) {
|
|
|
|
ItemBrowserMap::const_iterator it = item_browser_map_.find(item);
|
|
|
|
if (it != item_browser_map_.end()) {
|
2024-05-13 23:36:09 +02:00
|
|
|
return it->second.get();
|
2024-04-26 21:21:33 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
CefRefPtr<CefBrowserHostBase> browser;
|
|
|
|
content::WebContents* contents =
|
|
|
|
content::DownloadItemUtils::GetWebContents(item);
|
|
|
|
if (contents) {
|
|
|
|
browser = CefBrowserHostBase::GetBrowserForContents(contents);
|
|
|
|
LOG_IF(WARNING, !browser) << "No CefBrowser for download item";
|
|
|
|
}
|
|
|
|
if (!browser) {
|
|
|
|
return nullptr;
|
|
|
|
}
|
|
|
|
|
|
|
|
item->AddObserver(this);
|
|
|
|
|
|
|
|
item_browser_map_.insert(std::make_pair(item, browser.get()));
|
|
|
|
|
|
|
|
// Register as an observer so that we can cancel associated DownloadItems when
|
|
|
|
// the browser is destroyed.
|
|
|
|
if (!browser->HasObserver(this)) {
|
|
|
|
browser->AddObserver(this);
|
|
|
|
}
|
|
|
|
|
|
|
|
return browser;
|
|
|
|
}
|
|
|
|
|
|
|
|
CefRefPtr<CefBrowserHostBase> CefDownloadManagerDelegateImpl::GetBrowser(
|
|
|
|
DownloadItem* item) {
|
|
|
|
ItemBrowserMap::const_iterator it = item_browser_map_.find(item);
|
|
|
|
if (it != item_browser_map_.end()) {
|
2024-05-13 23:36:09 +02:00
|
|
|
return it->second.get();
|
2024-04-26 21:21:33 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// If the download is rejected (e.g. ALT+click on an invalid protocol link)
|
|
|
|
// then an "interrupted" download will be started via DownloadManagerImpl::
|
|
|
|
// StartDownloadWithId (originating from CreateInterruptedDownload) with no
|
|
|
|
// associated WebContents and consequently no associated CEF browser. In that
|
|
|
|
// case DetermineDownloadTarget will be called before OnDownloadCreated.
|
|
|
|
DCHECK(!content::DownloadItemUtils::GetWebContents(item));
|
|
|
|
return nullptr;
|
|
|
|
}
|
|
|
|
|
|
|
|
void CefDownloadManagerDelegateImpl::ResetManager() {
|
|
|
|
if (manager_) {
|
|
|
|
if (alloy_bootstrap_) {
|
|
|
|
manager_->SetDelegate(nullptr);
|
|
|
|
}
|
|
|
|
manager_->RemoveObserver(this);
|
|
|
|
manager_ptr_factory_.InvalidateWeakPtrs();
|
|
|
|
manager_ = nullptr;
|
|
|
|
}
|
|
|
|
|
|
|
|
while (!item_browser_map_.empty()) {
|
|
|
|
OnDownloadDestroyed(item_browser_map_.begin()->first);
|
|
|
|
}
|
|
|
|
}
|