374 lines
		
	
	
	
		
			10 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
			
		
		
	
	
			374 lines
		
	
	
	
		
			10 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
| /*
 | |
| This file is part of Telegram Desktop,
 | |
| the official desktop application for the Telegram messaging service.
 | |
| 
 | |
| For license and copyright information please follow this link:
 | |
| https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
 | |
| */
 | |
| #include "data/data_download_manager.h"
 | |
| 
 | |
| #include "data/data_session.h"
 | |
| #include "data/data_photo.h"
 | |
| #include "data/data_document.h"
 | |
| #include "data/data_changes.h"
 | |
| #include "data/data_user.h"
 | |
| #include "data/data_channel.h"
 | |
| #include "main/main_session.h"
 | |
| #include "main/main_account.h"
 | |
| #include "history/history.h"
 | |
| #include "history/history_item.h"
 | |
| #include "core/application.h"
 | |
| #include "ui/controls/download_bar.h"
 | |
| 
 | |
| namespace Data {
 | |
| namespace {
 | |
| 
 | |
| constexpr auto kClearLoadingTimeout = 5 * crl::time(1000);
 | |
| 
 | |
| constexpr auto ByItem = [](const auto &entry) {
 | |
| 	if constexpr (std::is_same_v<decltype(entry), const DownloadingId&>) {
 | |
| 		return entry.object.item;
 | |
| 	} else {
 | |
| 		const auto resolved = entry.object.get();
 | |
| 		return resolved ? resolved->item : nullptr;
 | |
| 	}
 | |
| };
 | |
| 
 | |
| [[nodiscard]] uint64 PeerAccessHash(not_null<PeerData*> peer) {
 | |
| 	if (const auto user = peer->asUser()) {
 | |
| 		return user->accessHash();
 | |
| 	} else if (const auto channel = peer->asChannel()) {
 | |
| 		return channel->access;
 | |
| 	}
 | |
| 	return 0;
 | |
| }
 | |
| 
 | |
| } // namespace
 | |
| 
 | |
| DownloadManager::DownloadManager()
 | |
| : _clearLoadingTimer([=] { clearLoading(); }) {
 | |
| }
 | |
| 
 | |
| DownloadManager::~DownloadManager() = default;
 | |
| 
 | |
| void DownloadManager::trackSession(not_null<Main::Session*> session) {
 | |
| 	auto &data = _sessions.emplace(session, SessionData()).first->second;
 | |
| 
 | |
| 	session->data().itemRepaintRequest(
 | |
| 	) | rpl::filter([=](not_null<const HistoryItem*> item) {
 | |
| 		return _loading.contains(item);
 | |
| 	}) | rpl::start_with_next([=](not_null<const HistoryItem*> item) {
 | |
| 		check(item);
 | |
| 	}, data.lifetime);
 | |
| 
 | |
| 	session->data().itemLayoutChanged(
 | |
| 	) | rpl::filter([=](not_null<const HistoryItem*> item) {
 | |
| 		return _loading.contains(item);
 | |
| 	}) | rpl::start_with_next([=](not_null<const HistoryItem*> item) {
 | |
| 		check(item);
 | |
| 	}, data.lifetime);
 | |
| 
 | |
| 	session->data().itemViewRefreshRequest(
 | |
| 	) | rpl::start_with_next([=](not_null<HistoryItem*> item) {
 | |
| 		changed(item);
 | |
| 	}, data.lifetime);
 | |
| 
 | |
| 	session->changes().messageUpdates(
 | |
| 		MessageUpdate::Flag::Destroyed
 | |
| 	) | rpl::start_with_next([=](const MessageUpdate &update) {
 | |
| 		removed(update.item);
 | |
| 	}, data.lifetime);
 | |
| 
 | |
| 	session->account().sessionChanges(
 | |
| 	) | rpl::filter(
 | |
| 		rpl::mappers::_1 != session
 | |
| 	) | rpl::take(1) | rpl::start_with_next([=] {
 | |
| 		untrack(session);
 | |
| 	}, data.lifetime);
 | |
| }
 | |
| 
 | |
| void DownloadManager::addLoading(DownloadObject object) {
 | |
| 	Expects(object.item != nullptr);
 | |
| 	Expects(object.document || object.photo);
 | |
| 
 | |
| 	const auto item = object.item;
 | |
| 	auto &data = sessionData(item);
 | |
| 
 | |
| 	const auto already = ranges::find(data.downloading, item, ByItem);
 | |
| 	if (already != end(data.downloading)) {
 | |
| 		const auto document = already->object.document;
 | |
| 		const auto photo = already->object.photo;
 | |
| 		if (document == object.document && photo == object.photo) {
 | |
| 			check(item);
 | |
| 			return;
 | |
| 		}
 | |
| 		remove(data, already);
 | |
| 	}
 | |
| 
 | |
| 	const auto size = object.document
 | |
| 		? object.document->size
 | |
| 		: object.photo->imageByteSize(PhotoSize::Large);
 | |
| 
 | |
| 	data.downloading.push_back({ .object = object, .total = size });
 | |
| 	_loading.emplace(item);
 | |
| 	_loadingProgress = DownloadProgress{
 | |
| 		.ready = _loadingProgress.current().ready,
 | |
| 		.total = _loadingProgress.current().total + size,
 | |
| 	};
 | |
| 	_loadingListChanges.fire({});
 | |
| 	_clearLoadingTimer.cancel();
 | |
| 
 | |
| 	check(item);
 | |
| }
 | |
| 
 | |
| void DownloadManager::check(not_null<const HistoryItem*> item) {
 | |
| 	auto &data = sessionData(item);
 | |
| 	const auto i = ranges::find(data.downloading, item, ByItem);
 | |
| 	Assert(i != end(data.downloading));
 | |
| 	auto &entry = *i;
 | |
| 
 | |
| 	const auto media = item->media();
 | |
| 	const auto photo = media ? media->photo() : nullptr;
 | |
| 	const auto document = media ? media->document() : nullptr;
 | |
| 	if (entry.object.photo != photo || entry.object.document != document) {
 | |
| 		cancel(data, i);
 | |
| 		return;
 | |
| 	}
 | |
| 	// Load with progress only documents for now.
 | |
| 	Assert(document != nullptr);
 | |
| 
 | |
| 	const auto path = document->filepath(true);
 | |
| 	if (!path.isEmpty()) {
 | |
| 		if (_loading.contains(item)) {
 | |
| 			addLoaded(entry.object, path, entry.started);
 | |
| 		}
 | |
| 	} else if (!document->loading()) {
 | |
| 		remove(data, i);
 | |
| 	} else {
 | |
| 		const auto totalChange = document->size - entry.total;
 | |
| 		const auto readyChange = document->loadOffset() - entry.ready;
 | |
| 		if (!readyChange && !totalChange) {
 | |
| 			return;
 | |
| 		}
 | |
| 		entry.ready += readyChange;
 | |
| 		entry.total += totalChange;
 | |
| 		_loadingProgress = DownloadProgress{
 | |
| 			.ready = _loadingProgress.current().ready + readyChange,
 | |
| 			.total = _loadingProgress.current().total + totalChange,
 | |
| 		};
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void DownloadManager::addLoaded(
 | |
| 		DownloadObject object,
 | |
| 		const QString &path,
 | |
| 		DownloadDate started) {
 | |
| 	Expects(object.item != nullptr);
 | |
| 	Expects(object.document || object.photo);
 | |
| 
 | |
| 	const auto item = object.item;
 | |
| 	auto &data = sessionData(item);
 | |
| 
 | |
| 	const auto id = object.document
 | |
| 		? DownloadId{ object.document->id, DownloadType::Document }
 | |
| 		: DownloadId{ object.photo->id, DownloadType::Photo };
 | |
| 	data.downloaded.push_back({
 | |
| 		.download = id,
 | |
| 		.started = started,
 | |
| 		.path = path,
 | |
| 		.itemId = item->fullId(),
 | |
| 		.peerAccessHash = PeerAccessHash(item->history()->peer),
 | |
| 		.object = std::make_unique<DownloadObject>(object),
 | |
| 	});
 | |
| 	_loaded.emplace(item);
 | |
| 
 | |
| 	const auto i = ranges::find(data.downloading, item, ByItem);
 | |
| 	if (i != end(data.downloading)) {
 | |
| 		auto &entry = *i;
 | |
| 		const auto j = _loading.find(entry.object.item);
 | |
| 		if (j == end(_loading)) {
 | |
| 			return;
 | |
| 		}
 | |
| 		const auto document = entry.object.document;
 | |
| 		const auto totalChange = document->size - entry.total;
 | |
| 		const auto readyChange = document->size - entry.ready;
 | |
| 		entry.ready += readyChange;
 | |
| 		entry.total += totalChange;
 | |
| 		entry.done = true;
 | |
| 		_loading.erase(j);
 | |
| 		_loadingDone.emplace(entry.object.item);
 | |
| 		_loadingProgress = DownloadProgress{
 | |
| 			.ready = _loadingProgress.current().ready + readyChange,
 | |
| 			.total = _loadingProgress.current().total + totalChange,
 | |
| 		};
 | |
| 		if (_loading.empty()) {
 | |
| 			_clearLoadingTimer.callOnce(kClearLoadingTimeout);
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| auto DownloadManager::loadingList() const
 | |
| -> ranges::any_view<DownloadingId, ranges::category::input> {
 | |
| 	return ranges::views::all(
 | |
| 		_sessions
 | |
| 	) | ranges::views::transform([=](const auto &pair) {
 | |
| 		return ranges::views::all(pair.second.downloading);
 | |
| 	}) | ranges::views::join;
 | |
| }
 | |
| 
 | |
| DownloadProgress DownloadManager::loadingProgress() const {
 | |
| 	return _loadingProgress.current();
 | |
| }
 | |
| 
 | |
| rpl::producer<> DownloadManager::loadingListChanges() const {
 | |
| 	return _loadingListChanges.events();
 | |
| }
 | |
| 
 | |
| auto DownloadManager::loadingProgressValue() const
 | |
| -> rpl::producer<DownloadProgress> {
 | |
| 	return _loadingProgress.value();
 | |
| }
 | |
| 
 | |
| void DownloadManager::clearLoading() {
 | |
| 	Expects(_loading.empty());
 | |
| 
 | |
| 	for (auto &[session, data] : _sessions) {
 | |
| 		while (!data.downloading.empty()) {
 | |
| 			remove(data, data.downloading.end() - 1);
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void DownloadManager::remove(
 | |
| 		SessionData &data,
 | |
| 		std::vector<DownloadingId>::iterator i) {
 | |
| 	const auto now = DownloadProgress{
 | |
| 		.ready = _loadingProgress.current().ready - i->ready,
 | |
| 		.total = _loadingProgress.current().total - i->total,
 | |
| 	};
 | |
| 	_loading.remove(i->object.item);
 | |
| 	_loadingDone.remove(i->object.item);
 | |
| 	data.downloading.erase(i);
 | |
| 	_loadingListChanges.fire({});
 | |
| 	_loadingProgress = now;
 | |
| 	if (_loading.empty() && !_loadingDone.empty()) {
 | |
| 		_clearLoadingTimer.callOnce(kClearLoadingTimeout);
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void DownloadManager::cancel(
 | |
| 		SessionData &data,
 | |
| 		std::vector<DownloadingId>::iterator i) {
 | |
| 	const auto object = i->object;
 | |
| 	remove(data, i);
 | |
| 	if (const auto document = object.document) {
 | |
| 		document->cancel();
 | |
| 	} else if (const auto photo = object.photo) {
 | |
| 		photo->cancel();
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void DownloadManager::changed(not_null<const HistoryItem*> item) {
 | |
| 	if (_loaded.contains(item)) {
 | |
| 		auto &data = sessionData(item);
 | |
| 		const auto i = ranges::find(data.downloaded, item, ByItem);
 | |
| 		Assert(i != end(data.downloaded));
 | |
| 
 | |
| 		const auto media = item->media();
 | |
| 		const auto photo = media ? media->photo() : nullptr;
 | |
| 		const auto document = media ? media->document() : nullptr;
 | |
| 		if (i->object->photo != photo || i->object->document != document) {
 | |
| 			*i->object = DownloadObject();
 | |
| 
 | |
| 			_loaded.remove(item);
 | |
| 		}
 | |
| 	}
 | |
| 	if (_loading.contains(item) || _loadingDone.contains(item)) {
 | |
| 		check(item);
 | |
| 	}
 | |
| }
 | |
| 
 | |
| void DownloadManager::removed(not_null<const HistoryItem*> item) {
 | |
| 	if (_loaded.contains(item)) {
 | |
| 		auto &data = sessionData(item);
 | |
| 		const auto i = ranges::find(data.downloaded, item, ByItem);
 | |
| 		Assert(i != end(data.downloaded));
 | |
| 		*i->object = DownloadObject();
 | |
| 
 | |
| 		_loaded.remove(item);
 | |
| 	}
 | |
| 	if (_loading.contains(item) || _loadingDone.contains(item)) {
 | |
| 		auto &data = sessionData(item);
 | |
| 		const auto i = ranges::find(data.downloading, item, ByItem);
 | |
| 		Assert(i != end(data.downloading));
 | |
| 		auto &entry = *i;
 | |
| 
 | |
| 		// We don't want to download files without messages.
 | |
| 		// For example, there is no way to refresh a file reference for them.
 | |
| 		//entry.object.item = nullptr;
 | |
| 		cancel(data, i);
 | |
| 	}
 | |
| }
 | |
| 
 | |
| DownloadManager::SessionData &DownloadManager::sessionData(
 | |
| 		not_null<Main::Session*> session) {
 | |
| 	const auto i = _sessions.find(session);
 | |
| 	Assert(i != end(_sessions));
 | |
| 	return i->second;
 | |
| }
 | |
| 
 | |
| DownloadManager::SessionData &DownloadManager::sessionData(
 | |
| 		not_null<const HistoryItem*> item) {
 | |
| 	return sessionData(&item->history()->session());
 | |
| }
 | |
| 
 | |
| void DownloadManager::untrack(not_null<Main::Session*> session) {
 | |
| 	const auto i = _sessions.find(session);
 | |
| 	Assert(i != end(_sessions));
 | |
| 
 | |
| 	for (const auto &entry : i->second.downloaded) {
 | |
| 		if (const auto resolved = entry.object.get()) {
 | |
| 			if (const auto item = resolved->item) {
 | |
| 				_loaded.remove(item);
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 	while (!i->second.downloading.empty()) {
 | |
| 		remove(i->second, i->second.downloading.end() - 1);
 | |
| 	}
 | |
| 	_sessions.erase(i);
 | |
| }
 | |
| 
 | |
| rpl::producer<Ui::DownloadBarProgress> MakeDownloadBarProgress() {
 | |
| 	return Core::App().downloadManager().loadingProgressValue(
 | |
| 	) | rpl::map([=](const DownloadProgress &progress) {
 | |
| 		return Ui::DownloadBarProgress{
 | |
| 			.ready = progress.ready,
 | |
| 			.total = progress.total,
 | |
| 		};
 | |
| 	});
 | |
| }
 | |
| 
 | |
| rpl::producer<Ui::DownloadBarContent> MakeDownloadBarContent() {
 | |
| 	auto &manager = Core::App().downloadManager();
 | |
| 	return rpl::single(
 | |
| 		rpl::empty_value()
 | |
| 	) | rpl::then(
 | |
| 		manager.loadingListChanges() | rpl::to_empty
 | |
| 	) | rpl::map([=, &manager] {
 | |
| 		auto result = Ui::DownloadBarContent();
 | |
| 		for (const auto &id : manager.loadingList()) {
 | |
| 			if (result.singleName.isEmpty()) {
 | |
| 				result.singleName = id.object.document->filename();
 | |
| 				result.singleThumbnail = QImage();
 | |
| 			}
 | |
| 			++result.count;
 | |
| 			if (id.done) {
 | |
| 				++result.done;
 | |
| 			}
 | |
| 		}
 | |
| 		return result;
 | |
| 	});
 | |
| }
 | |
| 
 | |
| } // namespace Data
 | 
