tdesktop/Telegram/SourceFiles/data/data_download_manager.cpp

1208 lines
33 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_document_media.h"
#include "data/data_web_page.h"
#include "data/data_changes.h"
#include "data/data_user.h"
#include "data/data_channel.h"
#include "data/data_file_origin.h"
#include "base/unixtime.h"
#include "base/random.h"
#include "main/main_session.h"
#include "main/main_account.h"
#include "storage/storage_account.h"
#include "history/history.h"
#include "history/history_item.h"
#include "history/history_item_helpers.h"
#include "core/application.h"
#include "core/mime_type.h"
#include "ui/controls/download_bar.h"
#include "ui/text/format_song_document_name.h"
#include "ui/layers/generic_box.h"
#include "storage/serialize_common.h"
#include "window/window_controller.h"
#include "window/window_session_controller.h"
#include "apiwrap.h"
#include "styles/style_layers.h"
namespace Data {
namespace {
constexpr auto kClearLoadingTimeout = 5 * crl::time(1000);
constexpr auto kMaxFileSize = 4000 * int64(1024 * 1024);
constexpr auto kMaxResolvePerAttempt = 100;
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.get() : nullptr;
}
};
constexpr auto ByDocument = [](const auto &entry) {
return entry.object.document;
};
[[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;
}
[[nodiscard]] bool ItemContainsMedia(const DownloadObject &object) {
if (const auto photo = object.photo) {
if (const auto media = object.item->media()) {
if (const auto page = media->webpage()) {
if (page->photo == photo) {
return true;
}
for (const auto &item : page->collage.items) {
if (const auto v = std::get_if<PhotoData*>(&item)) {
if ((*v) == photo) {
return true;
}
}
}
} else {
return (media->photo() == photo);
}
}
} else if (const auto document = object.document) {
if (const auto media = object.item->media()) {
if (const auto page = media->webpage()) {
if (page->document == document) {
return true;
}
for (const auto &item : page->collage.items) {
if (const auto v = std::get_if<DocumentData*>(&item)) {
if ((*v) == document) {
return true;
}
}
}
} else {
return (media->document() == document);
}
}
}
return false;
}
struct DocumentDescriptor {
uint64 sessionUniqueId = 0;
DocumentId documentId = 0;
FullMsgId itemId;
};
} // namespace
struct DownloadManager::DeleteFilesDescriptor {
base::flat_set<not_null<Main::Session*>> sessions;
base::flat_map<QString, DocumentDescriptor> files;
};
DownloadManager::DownloadManager()
: _clearLoadingTimer([=] { clearLoading(); }) {
}
DownloadManager::~DownloadManager() = default;
void DownloadManager::trackSession(not_null<Main::Session*> session) {
auto &data = _sessions.emplace(session, SessionData()).first->second;
data.downloaded = deserialize(session);
data.resolveNeeded = data.downloaded.size();
session->data().documentLoadProgress(
) | rpl::filter([=](not_null<DocumentData*> document) {
return _loadingDocuments.contains(document);
}) | rpl::start_with_next([=](not_null<DocumentData*> document) {
check(document);
}, 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::itemVisibilitiesUpdated(
not_null<Main::Session*> session) {
const auto i = _sessions.find(session);
if (i == end(_sessions)
|| i->second.downloading.empty()
|| !i->second.downloading.front().hiddenByView) {
return;
}
for (const auto &id : i->second.downloading) {
if (!id.done
&& !session->data().queryItemVisibility(id.object.item)) {
for (auto &id : i->second.downloading) {
id.hiddenByView = false;
}
_loadingListChanges.fire({});
return;
}
}
}
int64 DownloadManager::computeNextStartDate() {
const auto now = base::unixtime::now();
if (_lastStartedBase != now) {
_lastStartedBase = now;
_lastStartedAdded = 0;
} else {
++_lastStartedAdded;
}
return int64(_lastStartedBase) * 1000 + _lastStartedAdded;
}
void DownloadManager::addLoading(DownloadObject object) {
Expects(object.item != nullptr);
Expects(object.document != nullptr);
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->size;
const auto path = object.document->loadingFilePath();
if (path.isEmpty()) {
return;
}
const auto shownExists = !data.downloading.empty()
&& !data.downloading.front().hiddenByView;
data.downloading.push_back({
.object = object,
.started = computeNextStartDate(),
.path = path,
.total = size,
.hiddenByView = (!shownExists
&& item->history()->owner().queryItemVisibility(item)),
});
_loading.emplace(item);
_loadingDocuments.emplace(object.document);
_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));
check(data, i);
}
void DownloadManager::check(not_null<DocumentData*> document) {
auto &data = sessionData(document);
const auto i = ranges::find(
data.downloading,
document.get(),
ByDocument);
Assert(i != end(data.downloading));
check(data, i);
}
void DownloadManager::check(
SessionData &data,
std::vector<DownloadingId>::iterator i) {
auto &entry = *i;
if (!ItemContainsMedia(entry.object)) {
cancel(data, i);
return;
}
const auto document = entry.object.document;
// Load with progress only documents for now.
Assert(document != nullptr);
const auto path = document->filepath(true);
if (!path.isEmpty()) {
if (_loading.contains(entry.object.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 size = QFileInfo(path).size();
if (size <= 0 || size > kMaxFileSize) {
return;
}
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,
.size = size,
.itemId = item->fullId(),
.peerAccessHash = PeerAccessHash(item->history()->peer),
.object = std::make_unique<DownloadObject>(object),
});
_loaded.emplace(item);
_loadedAdded.fire(&data.downloaded.back());
writePostponed(&item->history()->session());
const auto i = ranges::find(data.downloading, item, ByItem);
if (i != end(data.downloading)) {
auto &entry = *i;
const auto document = entry.object.document;
if (document) {
_loadingDocuments.remove(document);
}
const auto j = _loading.find(entry.object.item);
if (j == end(_loading)) {
return;
}
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,
};
_loadingListChanges.fire({});
if (_loading.empty()) {
_clearLoadingTimer.callOnce(kClearLoadingTimeout);
}
}
}
void DownloadManager::clearIfFinished() {
if (_clearLoadingTimer.isActive()) {
_clearLoadingTimer.cancel();
clearLoading();
}
}
void DownloadManager::deleteFiles(const std::vector<GlobalMsgId> &ids) {
auto descriptor = DeleteFilesDescriptor();
for (const auto &id : ids) {
if (const auto item = MessageByGlobalId(id)) {
const auto session = &item->history()->session();
const auto i = _sessions.find(session);
if (i == end(_sessions)) {
continue;
}
auto &data = i->second;
const auto j = ranges::find(
data.downloading,
not_null{ item },
ByItem);
if (j != end(data.downloading)) {
cancel(data, j);
}
const auto k = ranges::find(data.downloaded, item, ByItem);
if (k != end(data.downloaded)) {
const auto document = k->object->document;
descriptor.files.emplace(k->path, DocumentDescriptor{
.sessionUniqueId = id.sessionUniqueId,
.documentId = document ? document->id : DocumentId(),
.itemId = id.itemId,
});
_loaded.remove(item);
_generated.remove(item);
if (document) {
_generatedDocuments.remove(document);
}
data.downloaded.erase(k);
_loadedRemoved.fire_copy(item);
descriptor.sessions.emplace(session);
}
}
}
finishFilesDelete(std::move(descriptor));
}
void DownloadManager::deleteAll() {
auto descriptor = DeleteFilesDescriptor();
for (auto &[session, data] : _sessions) {
if (!data.downloaded.empty()) {
descriptor.sessions.emplace(session);
} else if (data.downloading.empty()) {
continue;
}
const auto sessionUniqueId = session->uniqueId();
while (!data.downloading.empty()) {
cancel(data, data.downloading.end() - 1);
}
for (auto &id : base::take(data.downloaded)) {
const auto object = id.object.get();
const auto document = object ? object->document : nullptr;
descriptor.files.emplace(id.path, DocumentDescriptor{
.sessionUniqueId = sessionUniqueId,
.documentId = document ? document->id : DocumentId(),
.itemId = id.itemId,
});
if (document) {
_generatedDocuments.remove(document);
}
if (const auto item = object ? object->item.get() : nullptr) {
_loaded.remove(item);
_generated.remove(item);
_loadedRemoved.fire_copy(item);
}
}
}
for (const auto &session : descriptor.sessions) {
writePostponed(session);
}
finishFilesDelete(std::move(descriptor));
}
void DownloadManager::finishFilesDelete(DeleteFilesDescriptor &&descriptor) {
for (const auto &session : descriptor.sessions) {
writePostponed(session);
}
crl::async([files = std::move(descriptor.files)]{
for (const auto &file : files) {
QFile(file.first).remove();
crl::on_main([descriptor = file.second] {
if (const auto session = SessionByUniqueId(
descriptor.sessionUniqueId)) {
if (const auto id = descriptor.documentId) {
[[maybe_unused]] const auto location
= session->data().document(id)->location(true);
}
const auto itemId = descriptor.itemId;
if (const auto item = session->data().message(itemId)) {
session->data().requestItemRepaint(item);
}
}
});
}
});
}
bool DownloadManager::loadedHasNonCloudFile() const {
for (const auto &[session, data] : _sessions) {
for (const auto &id : data.downloaded) {
if (const auto object = id.object.get()) {
if (!object->item->isHistoryEntry()) {
return true;
}
}
}
}
return false;
}
auto DownloadManager::loadingList() const
-> ranges::any_view<const DownloadingId*, ranges::category::input> {
return ranges::views::all(
_sessions
) | ranges::views::transform([=](const auto &pair) {
return ranges::views::all(
pair.second.downloading
) | ranges::views::transform([](const DownloadingId &id) {
return &id;
});
}) | 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();
}
bool DownloadManager::loadingInProgress(Main::Session *onlyInSession) const {
return lookupLoadingItem(onlyInSession) != nullptr;
}
HistoryItem *DownloadManager::lookupLoadingItem(
Main::Session *onlyInSession) const {
constexpr auto find = [](const SessionData &data) {
constexpr auto proj = &DownloadingId::done;
const auto i = ranges::find(data.downloading, false, proj);
return (i != end(data.downloading)) ? i->object.item.get() : nullptr;
};
if (onlyInSession) {
const auto i = _sessions.find(onlyInSession);
return (i != end(_sessions)) ? find(i->second) : nullptr;
} else {
for (const auto &[session, data] : _sessions) {
if (const auto result = find(data)) {
return result;
}
}
}
return nullptr;
}
void DownloadManager::loadingStopWithConfirmation(
Fn<void()> callback,
Main::Session *onlyInSession) {
const auto item = lookupLoadingItem(onlyInSession);
if (!item) {
return;
}
const auto window = Core::App().windowFor(
&item->history()->session().account());
if (!window) {
return;
}
const auto weak = base::make_weak(&item->history()->session());
const auto id = item->fullId();
auto box = Box([=](not_null<Ui::GenericBox*> box) {
box->addRow(
object_ptr<Ui::FlatLabel>(
box.get(),
tr::lng_download_sure_stop(),
st::boxLabel),
st::boxPadding + QMargins(0, 0, 0, st::boxPadding.bottom()));
box->setStyle(st::defaultBox);
box->addButton(tr::lng_selected_upload_stop(), [=] {
box->closeBox();
if (!onlyInSession || weak.get()) {
loadingStop(onlyInSession);
}
if (callback) {
callback();
}
}, st::attentionBoxButton);
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
box->addLeftButton(tr::lng_upload_show_file(), [=] {
box->closeBox();
if (const auto strong = weak.get()) {
if (const auto item = strong->data().message(id)) {
if (const auto window = strong->tryResolveWindow()) {
window->showMessage(item);
}
}
}
});
});
window->show(std::move(box));
window->activate();
}
void DownloadManager::loadingStop(Main::Session *onlyInSession) {
const auto stopInSession = [&](SessionData &data) {
while (!data.downloading.empty()) {
cancel(data, data.downloading.end() - 1);
}
};
if (onlyInSession) {
const auto i = _sessions.find(onlyInSession);
if (i != end(_sessions)) {
stopInSession(i->second);
}
} else {
for (auto &[session, data] : _sessions) {
stopInSession(data);
}
}
}
void DownloadManager::clearLoading() {
Expects(_loading.empty());
for (auto &[session, data] : _sessions) {
while (!data.downloading.empty()) {
remove(data, data.downloading.end() - 1);
}
}
}
auto DownloadManager::loadedList()
-> ranges::any_view<const DownloadedId*, ranges::category::input> {
for (auto &[session, data] : _sessions) {
resolve(session, data);
}
return ranges::views::all(
_sessions
) | ranges::views::transform([=](const auto &pair) {
return ranges::views::all(
pair.second.downloaded
) | ranges::views::filter([](const DownloadedId &id) {
return (id.object != nullptr);
}) | ranges::views::transform([](const DownloadedId &id) {
return &id;
});
}) | ranges::views::join;
}
rpl::producer<> DownloadManager::loadedResolveDone() const {
using namespace rpl::mappers;
return _loadedResolveDone.value() | rpl::filter(_1) | rpl::to_empty;
}
void DownloadManager::resolve(
not_null<Main::Session*> session,
SessionData &data) {
const auto guard = gsl::finally([&] {
checkFullResolveDone();
});
if (data.resolveSentTotal >= data.resolveNeeded
|| data.resolveSentTotal >= kMaxResolvePerAttempt) {
return;
}
struct Prepared {
uint64 peerAccessHash = 0;
QVector<MTPInputMessage> ids;
};
auto &owner = session->data();
auto prepared = base::flat_map<PeerId, Prepared>();
auto last = begin(data.downloaded);
auto from = last + (data.resolveNeeded - data.resolveSentTotal);
for (auto i = from; i != last;) {
auto &id = *--i;
const auto msgId = id.itemId.msg;
const auto info = QFileInfo(id.path);
if (!info.exists() || info.size() != id.size) {
// Mark as deleted.
id.path = QString();
} else if (!owner.message(id.itemId) && IsServerMsgId(msgId)) {
const auto groupByPeer = peerIsChannel(id.itemId.peer)
? id.itemId.peer
: session->userPeerId();
auto &perPeer = prepared[groupByPeer];
if (peerIsChannel(id.itemId.peer) && !perPeer.peerAccessHash) {
perPeer.peerAccessHash = id.peerAccessHash;
}
perPeer.ids.push_back(MTP_inputMessageID(MTP_int(msgId.bare)));
}
if (++data.resolveSentTotal >= kMaxResolvePerAttempt) {
break;
}
}
const auto check = [=] {
auto &data = sessionData(session);
if (!data.resolveSentRequests) {
resolveRequestsFinished(session, data);
}
};
const auto requestFinished = [=] {
--sessionData(session).resolveSentRequests;
check();
};
for (auto &[peer, perPeer] : prepared) {
if (const auto channelId = peerToChannel(peer)) {
session->api().request(MTPchannels_GetMessages(
MTP_inputChannel(
MTP_long(channelId.bare),
MTP_long(perPeer.peerAccessHash)),
MTP_vector<MTPInputMessage>(perPeer.ids)
)).done([=](const MTPmessages_Messages &result) {
session->data().processExistingMessages(
session->data().channelLoaded(channelId),
result);
requestFinished();
}).fail(requestFinished).send();
} else {
session->api().request(MTPmessages_GetMessages(
MTP_vector<MTPInputMessage>(perPeer.ids)
)).done([=](const MTPmessages_Messages &result) {
session->data().processExistingMessages(nullptr, result);
requestFinished();
}).fail(requestFinished).send();
}
}
data.resolveSentRequests += prepared.size();
check();
}
void DownloadManager::resolveRequestsFinished(
not_null<Main::Session*> session,
SessionData &data) {
auto &owner = session->data();
for (; data.resolveSentTotal > 0; --data.resolveSentTotal) {
const auto i = begin(data.downloaded) + (--data.resolveNeeded);
if (i->path.isEmpty()) {
data.downloaded.erase(i);
continue;
}
const auto item = owner.message(i->itemId);
const auto media = item ? item->media() : nullptr;
const auto document = media ? media->document() : nullptr;
const auto photo = media ? media->photo() : nullptr;
if (i->download.type == DownloadType::Document
&& (!document || document->id != i->download.objectId)) {
generateEntry(session, *i);
} else if (i->download.type == DownloadType::Photo
&& (!photo || photo->id != i->download.objectId)) {
generateEntry(session, *i);
} else {
i->object = std::make_unique<DownloadObject>(DownloadObject{
.item = item,
.document = document,
.photo = photo,
});
_loaded.emplace(item);
}
_loadedAdded.fire(&*i);
}
crl::on_main(session, [=] {
resolve(session, sessionData(session));
});
}
void DownloadManager::checkFullResolveDone() {
if (_loadedResolveDone.current()) {
return;
}
for (const auto &[session, data] : _sessions) {
if (data.resolveSentTotal < data.resolveNeeded
|| data.resolveSentRequests > 0) {
return;
}
}
_loadedResolveDone = true;
}
void DownloadManager::generateEntry(
not_null<Main::Session*> session,
DownloadedId &id) {
Expects(!id.object);
const auto info = QFileInfo(id.path);
const auto document = session->data().document(
base::RandomValue<DocumentId>(),
0, // accessHash
QByteArray(), // fileReference
TimeId(id.started / 1000),
QVector<MTPDocumentAttribute>(
1,
MTP_documentAttributeFilename(
MTP_string(info.fileName()))),
Core::MimeTypeForFile(info).name(),
InlineImageLocation(), // inlineThumbnail
ImageWithLocation(), // thumbnail
ImageWithLocation(), // videoThumbnail
false, // isPremiumSticker
0, // dc
id.size);
document->setLocation(Core::FileLocation(info));
_generatedDocuments.emplace(document);
id.object = std::make_unique<DownloadObject>(DownloadObject{
.item = generateFakeItem(document),
.document = document,
});
_loaded.emplace(id.object->item);
}
auto DownloadManager::loadedAdded() const
-> rpl::producer<not_null<const DownloadedId*>> {
return _loadedAdded.events();
}
auto DownloadManager::loadedRemoved() const
-> rpl::producer<not_null<const HistoryItem*>> {
return _loadedRemoved.events();
}
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);
if (const auto document = i->object.document) {
_loadingDocuments.remove(document);
}
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;
const auto item = object.item;
remove(data, i);
if (!item->isAdminLogEntry()) {
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.get(), 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) {
detach(*i);
}
}
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.get(), ByItem);
Assert(i != end(data.downloaded));
detach(*i);
}
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));
// 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);
}
}
not_null<HistoryItem*> DownloadManager::regenerateItem(
const DownloadObject &previous) {
return generateItem(previous.item, previous.document, previous.photo);
}
not_null<HistoryItem*> DownloadManager::generateFakeItem(
not_null<DocumentData*> document) {
return generateItem(nullptr, document, nullptr);
}
not_null<HistoryItem*> DownloadManager::generateItem(
HistoryItem *previousItem,
DocumentData *document,
PhotoData *photo) {
Expects(document || photo);
const auto session = document
? &document->session()
: &photo->session();
const auto fromId = previousItem
? previousItem->from()->id
: session->userPeerId();
const auto history = previousItem
? previousItem->history()
: session->data().history(session->user());
const auto flags = MessageFlag::FakeHistoryItem;
const auto replyTo = FullReplyTo();
const auto viaBotId = UserId();
const auto date = base::unixtime::now();
const auto postAuthor = QString();
const auto caption = TextWithEntities();
const auto make = [&](const auto media) {
return history->makeMessage(
history->nextNonHistoryEntryId(),
flags,
replyTo,
viaBotId,
date,
fromId,
QString(),
media,
caption,
HistoryMessageMarkupData());
};
const auto result = document ? make(document) : make(photo);
_generated.emplace(result);
return result;
}
void DownloadManager::detach(DownloadedId &id) {
Expects(id.object != nullptr);
Expects(_loaded.contains(id.object->item));
Expects(!_generated.contains(id.object->item));
// Maybe generate new document?
const auto was = id.object->item;
const auto now = regenerateItem(*id.object);
_loaded.remove(was);
_loaded.emplace(now);
id.object->item = now;
_loadedRemoved.fire_copy(was);
_loadedAdded.fire_copy(&id);
}
DownloadManager::SessionData &DownloadManager::sessionData(
not_null<Main::Session*> session) {
const auto i = _sessions.find(session);
Assert(i != end(_sessions));
return i->second;
}
const DownloadManager::SessionData &DownloadManager::sessionData(
not_null<Main::Session*> session) const {
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());
}
DownloadManager::SessionData &DownloadManager::sessionData(
not_null<DocumentData*> document) {
return sessionData(&document->session());
}
void DownloadManager::writePostponed(not_null<Main::Session*> session) {
session->account().local().updateDownloads(serializator(session));
}
Fn<std::optional<QByteArray>()> DownloadManager::serializator(
not_null<Main::Session*> session) const {
return [this, weak = base::make_weak(session)]()
-> std::optional<QByteArray> {
const auto strong = weak.get();
if (!strong) {
return std::nullopt;
} else if (!_sessions.contains(strong)) {
return QByteArray();
}
auto result = QByteArray();
const auto &data = sessionData(strong);
const auto count = data.downloaded.size();
const auto constant = sizeof(quint64) // download.objectId
+ sizeof(qint32) // download.type
+ sizeof(qint64) // started
+ sizeof(quint32) // size
+ sizeof(quint64) // itemId.peer
+ sizeof(qint64) // itemId.msg
+ sizeof(quint64); // peerAccessHash
auto size = sizeof(qint32) // count
+ count * constant;
for (const auto &id : data.downloaded) {
size += Serialize::stringSize(id.path);
}
result.reserve(size);
auto stream = QDataStream(&result, QIODevice::WriteOnly);
stream.setVersion(QDataStream::Qt_5_1);
stream << qint32(count);
for (const auto &id : data.downloaded) {
stream
<< quint64(id.download.objectId)
<< qint32(id.download.type)
<< qint64(id.started)
// FileSize: Right now any file size fits 32 bit.
<< quint32(id.size)
<< quint64(id.itemId.peer.value)
<< qint64(id.itemId.msg.bare)
<< quint64(id.peerAccessHash)
<< id.path;
}
stream.device()->close();
return result;
};
}
std::vector<DownloadedId> DownloadManager::deserialize(
not_null<Main::Session*> session) const {
const auto serialized = session->account().local().downloadsSerialized();
if (serialized.isEmpty()) {
return {};
}
QDataStream stream(serialized);
stream.setVersion(QDataStream::Qt_5_1);
auto count = qint32();
stream >> count;
if (stream.status() != QDataStream::Ok || count <= 0 || count > 99'999) {
return {};
}
auto result = std::vector<DownloadedId>();
result.reserve(count);
for (auto i = 0; i != count; ++i) {
auto downloadObjectId = quint64();
auto uncheckedDownloadType = qint32();
auto started = qint64();
// FileSize: Right now any file size fits 32 bit.
auto size = quint32();
auto itemIdPeer = quint64();
auto itemIdMsg = qint64();
auto peerAccessHash = quint64();
auto path = QString();
stream
>> downloadObjectId
>> uncheckedDownloadType
>> started
>> size
>> itemIdPeer
>> itemIdMsg
>> peerAccessHash
>> path;
const auto downloadType = DownloadType(uncheckedDownloadType);
if (stream.status() != QDataStream::Ok
|| path.isEmpty()
|| size <= 0
|| size > kMaxFileSize
|| (downloadType != DownloadType::Document
&& downloadType != DownloadType::Photo)) {
return {};
}
result.push_back({
.download = {
.objectId = downloadObjectId,
.type = downloadType,
},
.started = started,
.path = path,
.size = int64(size),
.itemId = { PeerId(itemIdPeer), MsgId(itemIdMsg) },
.peerAccessHash = peerAccessHash,
});
}
return result;
}
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()) {
const auto item = resolved->item;
_loaded.remove(item);
_generated.remove(item);
if (const auto document = resolved->document) {
_generatedDocuments.remove(document);
}
}
}
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() {
return [](auto consumer) {
auto lifetime = rpl::lifetime();
struct State {
DocumentData *document = nullptr;
std::shared_ptr<Data::DocumentMedia> media;
rpl::lifetime downloadTaskLifetime;
QImage thumbnail;
base::has_weak_ptr guard;
bool scheduled = false;
Fn<void()> push;
};
const auto state = lifetime.make_state<State>();
auto &manager = Core::App().downloadManager();
const auto resolveThumbnailRecursive = [=](auto &&self) -> bool {
if (state->document && !state->document->hasThumbnail()) {
state->media = nullptr;
}
if (!state->media) {
state->downloadTaskLifetime.destroy();
if (!state->thumbnail.isNull()) {
return false;
}
state->thumbnail = QImage();
return true;
}
if (const auto image = state->media->thumbnail()) {
state->thumbnail = image->original();
state->downloadTaskLifetime.destroy();
state->media = nullptr;
return true;
} else if (const auto embed = state->media->thumbnailInline()) {
if (!state->thumbnail.isNull()) {
return false;
}
state->thumbnail = Images::Prepare(embed->original(), 0, {
.options = Images::Option::Blur,
});
}
state->document->session().downloaderTaskFinished(
) | rpl::filter([=] {
return self(self);
}) | rpl::start_with_next(
state->push,
state->downloadTaskLifetime);
return !state->thumbnail.isNull();
};
const auto resolveThumbnail = [=] {
return resolveThumbnailRecursive(resolveThumbnailRecursive);
};
const auto notify = [=, &manager] {
auto content = Ui::DownloadBarContent();
auto single = (const Data::DownloadObject*) nullptr;
for (const auto id : manager.loadingList()) {
if (id->hiddenByView) {
break;
}
if (!single) {
single = &id->object;
}
++content.count;
if (id->done) {
++content.done;
}
}
if (content.count == 1) {
const auto document = single->document;
const auto thumbnailed = (single->item
&& document->hasThumbnail())
? document
: nullptr;
if (state->document != thumbnailed) {
state->document = thumbnailed;
state->media = thumbnailed
? thumbnailed->createMediaView()
: nullptr;
if (const auto raw = state->media.get()) {
raw->thumbnailWanted(single->item->fullId());
}
state->thumbnail = QImage();
resolveThumbnail();
}
content.singleName = Ui::Text::FormatDownloadsName(
document);
content.singleThumbnail = state->thumbnail;
}
consumer.put_next(std::move(content));
};
state->push = [=] {
if (state->scheduled) {
return;
}
state->scheduled = true;
Ui::PostponeCall(&state->guard, [=] {
state->scheduled = false;
notify();
});
};
manager.loadingListChanges(
) | rpl::filter([=] {
return !state->scheduled;
}) | rpl::start_with_next(state->push, lifetime);
notify();
return lifetime;
};
}
} // namespace Data