/* 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) { return entry.object.item; } else { const auto resolved = entry.object.get(); return resolved ? resolved->item : nullptr; } }; [[nodiscard]] uint64 PeerAccessHash(not_null 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 session) { auto &data = _sessions.emplace(session, SessionData()).first->second; session->data().itemRepaintRequest( ) | rpl::filter([=](not_null item) { return _loading.contains(item); }) | rpl::start_with_next([=](not_null item) { check(item); }, data.lifetime); session->data().itemLayoutChanged( ) | rpl::filter([=](not_null item) { return _loading.contains(item); }) | rpl::start_with_next([=](not_null item) { check(item); }, data.lifetime); session->data().itemViewRefreshRequest( ) | rpl::start_with_next([=](not_null 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 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(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 { 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 { 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::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::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 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 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 session) { const auto i = _sessions.find(session); Assert(i != end(_sessions)); return i->second; } DownloadManager::SessionData &DownloadManager::sessionData( not_null item) { return sessionData(&item->history()->session()); } void DownloadManager::untrack(not_null 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 MakeDownloadBarProgress() { return Core::App().downloadManager().loadingProgressValue( ) | rpl::map([=](const DownloadProgress &progress) { return Ui::DownloadBarProgress{ .ready = progress.ready, .total = progress.total, }; }); } rpl::producer 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