1 /****************************************************************************
2 **
3 ** Copyright (C) 2015 The Qt Company Ltd.
4 ** Contact: http://www.qt.io/licensing/
5 **
6 ** This file is part of the demonstration applications of the Qt Toolkit.
7 **
8 ** $QT_BEGIN_LICENSE:LGPL21$
9 ** Commercial License Usage
10 ** Licensees holding valid commercial Qt licenses may use this file in
11 ** accordance with the commercial license agreement provided with the
12 ** Software or, alternatively, in accordance with the terms contained in
13 ** a written agreement between you and The Qt Company. For licensing terms
14 ** and conditions see http://www.qt.io/terms-conditions. For further
15 ** information use the contact form at http://www.qt.io/contact-us.
16 **
17 ** GNU Lesser General Public License Usage
18 ** Alternatively, this file may be used under the terms of the GNU Lesser
19 ** General Public License version 2.1 or version 3 as published by the Free
20 ** Software Foundation and appearing in the file LICENSE.LGPLv21 and
21 ** LICENSE.LGPLv3 included in the packaging of this file. Please review the
22 ** following information to ensure the GNU Lesser General Public License
23 ** requirements will be met: https://www.gnu.org/licenses/lgpl.html and
24 ** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
25 **
26 ** As a special exception, The Qt Company gives you certain additional
27 ** rights. These rights are described in The Qt Company LGPL Exception
28 ** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
29 **
30 ** $QT_END_LICENSE$
31 **
32 ****************************************************************************/
33
34 #include "downloadmanager.h"
35
36 #include "autosaver.h"
37 #include "browserapplication.h"
38 #include "networkaccessmanager.h"
39
40 #include <math.h>
41
42 #include <QtCore/QMetaEnum>
43 #include <QtCore/QSettings>
44
45 #include <QtGui/QDesktopServices>
46 #include <QtWidgets/QFileDialog>
47 #include <QtWidgets/QHeaderView>
48 #include <QtWidgets/QFileIconProvider>
49
50 #include <QtCore/QDebug>
51
52 #include <QWebSettings>
53
54 /*!
55 DownloadItem is a widget that is displayed in the download manager list.
56 It moves the data from the QNetworkReply into the QFile as well
57 as update the information/progressbar and report errors.
58 */
DownloadItem(QNetworkReply * reply,bool requestFileName,QWidget * parent)59 DownloadItem::DownloadItem(QNetworkReply *reply, bool requestFileName, QWidget *parent)
60 : QWidget(parent)
61 , m_reply(reply)
62 , m_requestFileName(requestFileName)
63 , m_bytesReceived(0)
64 {
65 setupUi(this);
66 QPalette p = downloadInfoLabel->palette();
67 p.setColor(QPalette::Text, Qt::darkGray);
68 downloadInfoLabel->setPalette(p);
69 progressBar->setMaximum(0);
70 tryAgainButton->hide();
71 connect(stopButton, SIGNAL(clicked()), this, SLOT(stop()));
72 connect(openButton, SIGNAL(clicked()), this, SLOT(open()));
73 connect(tryAgainButton, SIGNAL(clicked()), this, SLOT(tryAgain()));
74
75 init();
76 }
77
init()78 void DownloadItem::init()
79 {
80 if (!m_reply)
81 return;
82
83 // attach to the m_reply
84 m_url = m_reply->url();
85 m_reply->setParent(this);
86 connect(m_reply, SIGNAL(readyRead()), this, SLOT(downloadReadyRead()));
87 connect(m_reply, SIGNAL(error(QNetworkReply::NetworkError)),
88 this, SLOT(error(QNetworkReply::NetworkError)));
89 connect(m_reply, SIGNAL(downloadProgress(qint64,qint64)),
90 this, SLOT(downloadProgress(qint64,qint64)));
91 connect(m_reply, SIGNAL(metaDataChanged()),
92 this, SLOT(metaDataChanged()));
93 connect(m_reply, SIGNAL(finished()),
94 this, SLOT(finished()));
95
96 // reset info
97 downloadInfoLabel->clear();
98 progressBar->setValue(0);
99 getFileName();
100
101 // start timer for the download estimation
102 m_downloadTime.start();
103
104 if (m_reply->error() != QNetworkReply::NoError) {
105 error(m_reply->error());
106 finished();
107 }
108 }
109
getFileName()110 void DownloadItem::getFileName()
111 {
112 QSettings settings;
113 settings.beginGroup(QLatin1String("downloadmanager"));
114 QString defaultLocation = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation);
115 QString downloadDirectory = settings.value(QLatin1String("downloadDirectory"), defaultLocation).toString();
116 if (!downloadDirectory.isEmpty())
117 downloadDirectory += QLatin1Char('/');
118
119 QString defaultFileName = saveFileName(downloadDirectory);
120 QString fileName = defaultFileName;
121 if (m_requestFileName) {
122 fileName = QFileDialog::getSaveFileName(this, tr("Save File"), defaultFileName);
123 if (fileName.isEmpty()) {
124 m_reply->close();
125 fileNameLabel->setText(tr("Download canceled: %1").arg(QFileInfo(defaultFileName).fileName()));
126 return;
127 }
128 }
129 m_output.setFileName(fileName);
130 fileNameLabel->setText(QFileInfo(m_output.fileName()).fileName());
131 if (m_requestFileName)
132 downloadReadyRead();
133 }
134
saveFileName(const QString & directory) const135 QString DownloadItem::saveFileName(const QString &directory) const
136 {
137 // Move this function into QNetworkReply to also get file name sent from the server
138 QString path = m_url.path();
139 QFileInfo info(path);
140 QString baseName = info.completeBaseName();
141 QString endName = info.suffix();
142
143 if (baseName.isEmpty()) {
144 baseName = QLatin1String("unnamed_download");
145 qDebug() << "DownloadManager:: downloading unknown file:" << m_url;
146 }
147 QString name = directory + baseName + QLatin1Char('.') + endName;
148 if (QFile::exists(name)) {
149 // already exists, don't overwrite
150 int i = 1;
151 do {
152 name = directory + baseName + QLatin1Char('-') + QString::number(i++) + QLatin1Char('.') + endName;
153 } while (QFile::exists(name));
154 }
155 return name;
156 }
157
158
stop()159 void DownloadItem::stop()
160 {
161 setUpdatesEnabled(false);
162 stopButton->setEnabled(false);
163 stopButton->hide();
164 tryAgainButton->setEnabled(true);
165 tryAgainButton->show();
166 setUpdatesEnabled(true);
167 m_reply->abort();
168 }
169
open()170 void DownloadItem::open()
171 {
172 QFileInfo info(m_output);
173 QUrl url = QUrl::fromLocalFile(info.absolutePath());
174 QDesktopServices::openUrl(url);
175 }
176
tryAgain()177 void DownloadItem::tryAgain()
178 {
179 if (!tryAgainButton->isEnabled())
180 return;
181
182 tryAgainButton->setEnabled(false);
183 tryAgainButton->setVisible(false);
184 stopButton->setEnabled(true);
185 stopButton->setVisible(true);
186 progressBar->setVisible(true);
187
188 QNetworkReply *r = BrowserApplication::networkAccessManager()->get(QNetworkRequest(m_url));
189 if (m_reply)
190 m_reply->deleteLater();
191 if (m_output.exists())
192 m_output.remove();
193 m_reply = r;
194 init();
195 emit statusChanged();
196 }
197
downloadReadyRead()198 void DownloadItem::downloadReadyRead()
199 {
200 if (m_requestFileName && m_output.fileName().isEmpty())
201 return;
202 if (!m_output.isOpen()) {
203 // in case someone else has already put a file there
204 if (!m_requestFileName)
205 getFileName();
206 if (!m_output.open(QIODevice::WriteOnly)) {
207 downloadInfoLabel->setText(tr("Error opening save file: %1")
208 .arg(m_output.errorString()));
209 stopButton->click();
210 emit statusChanged();
211 return;
212 }
213 emit statusChanged();
214 }
215 if (-1 == m_output.write(m_reply->readAll())) {
216 downloadInfoLabel->setText(tr("Error saving: %1")
217 .arg(m_output.errorString()));
218 stopButton->click();
219 }
220 }
221
error(QNetworkReply::NetworkError)222 void DownloadItem::error(QNetworkReply::NetworkError)
223 {
224 qDebug() << "DownloadItem::error" << m_reply->errorString() << m_url;
225 downloadInfoLabel->setText(tr("Network Error: %1").arg(m_reply->errorString()));
226 tryAgainButton->setEnabled(true);
227 tryAgainButton->setVisible(true);
228 }
229
metaDataChanged()230 void DownloadItem::metaDataChanged()
231 {
232 qDebug() << "DownloadItem::metaDataChanged: not handled.";
233 }
234
downloadProgress(qint64 bytesReceived,qint64 bytesTotal)235 void DownloadItem::downloadProgress(qint64 bytesReceived, qint64 bytesTotal)
236 {
237 m_bytesReceived = bytesReceived;
238 if (bytesTotal == -1) {
239 progressBar->setValue(0);
240 progressBar->setMaximum(0);
241 } else {
242 progressBar->setValue(bytesReceived);
243 progressBar->setMaximum(bytesTotal);
244 }
245 updateInfoLabel();
246 }
247
updateInfoLabel()248 void DownloadItem::updateInfoLabel()
249 {
250 if (m_reply->error() == QNetworkReply::NoError)
251 return;
252
253 qint64 bytesTotal = progressBar->maximum();
254 bool running = !downloadedSuccessfully();
255
256 // update info label
257 double speed = m_bytesReceived * 1000.0 / m_downloadTime.elapsed();
258 double timeRemaining = ((double)(bytesTotal - m_bytesReceived)) / speed;
259 QString timeRemainingString = tr("seconds");
260 if (timeRemaining > 60) {
261 timeRemaining = timeRemaining / 60;
262 timeRemainingString = tr("minutes");
263 }
264 timeRemaining = floor(timeRemaining);
265
266 // When downloading the eta should never be 0
267 if (timeRemaining == 0)
268 timeRemaining = 1;
269
270 QString info;
271 if (running) {
272 QString remaining;
273 if (bytesTotal != 0)
274 remaining = tr("- %4 %5 remaining")
275 .arg(timeRemaining)
276 .arg(timeRemainingString);
277 info = tr("%1 of %2 (%3/sec) %4")
278 .arg(dataString(m_bytesReceived))
279 .arg(bytesTotal == 0 ? tr("?") : dataString(bytesTotal))
280 .arg(dataString((int)speed))
281 .arg(remaining);
282 } else {
283 if (m_bytesReceived == bytesTotal)
284 info = dataString(m_output.size());
285 else
286 info = tr("%1 of %2 - Stopped")
287 .arg(dataString(m_bytesReceived))
288 .arg(dataString(bytesTotal));
289 }
290 downloadInfoLabel->setText(info);
291 }
292
dataString(int size) const293 QString DownloadItem::dataString(int size) const
294 {
295 QString unit;
296 if (size < 1024) {
297 unit = tr("bytes");
298 } else if (size < 1024*1024) {
299 size /= 1024;
300 unit = tr("kB");
301 } else {
302 size /= 1024*1024;
303 unit = tr("MB");
304 }
305 return QString(QLatin1String("%1 %2")).arg(size).arg(unit);
306 }
307
downloading() const308 bool DownloadItem::downloading() const
309 {
310 return (progressBar->isVisible());
311 }
312
downloadedSuccessfully() const313 bool DownloadItem::downloadedSuccessfully() const
314 {
315 return (stopButton->isHidden() && tryAgainButton->isHidden());
316 }
317
finished()318 void DownloadItem::finished()
319 {
320 progressBar->hide();
321 stopButton->setEnabled(false);
322 stopButton->hide();
323 m_output.close();
324 updateInfoLabel();
325 emit statusChanged();
326 }
327
328 /*!
329 DownloadManager is a Dialog that contains a list of DownloadItems
330
331 It is a basic download manager. It only downloads the file, doesn't do BitTorrent,
332 extract zipped files or anything fancy.
333 */
DownloadManager(QWidget * parent)334 DownloadManager::DownloadManager(QWidget *parent)
335 : QDialog(parent)
336 , m_autoSaver(new AutoSaver(this))
337 , m_manager(BrowserApplication::networkAccessManager())
338 , m_iconProvider(0)
339 , m_removePolicy(Never)
340 {
341 setupUi(this);
342 downloadsView->setShowGrid(false);
343 downloadsView->verticalHeader()->hide();
344 downloadsView->horizontalHeader()->hide();
345 downloadsView->setAlternatingRowColors(true);
346 downloadsView->horizontalHeader()->setStretchLastSection(true);
347 m_model = new DownloadModel(this);
348 downloadsView->setModel(m_model);
349 connect(cleanupButton, SIGNAL(clicked()), this, SLOT(cleanup()));
350 load();
351 }
352
~DownloadManager()353 DownloadManager::~DownloadManager()
354 {
355 m_autoSaver->changeOccurred();
356 m_autoSaver->saveIfNeccessary();
357 if (m_iconProvider)
358 delete m_iconProvider;
359 }
360
activeDownloads() const361 int DownloadManager::activeDownloads() const
362 {
363 int count = 0;
364 for (int i = 0; i < m_downloads.count(); ++i) {
365 if (m_downloads.at(i)->stopButton->isEnabled())
366 ++count;
367 }
368 return count;
369 }
370
download(const QNetworkRequest & request,bool requestFileName)371 void DownloadManager::download(const QNetworkRequest &request, bool requestFileName)
372 {
373 if (request.url().isEmpty())
374 return;
375 handleUnsupportedContent(m_manager->get(request), requestFileName);
376 }
377
handleUnsupportedContent(QNetworkReply * reply,bool requestFileName)378 void DownloadManager::handleUnsupportedContent(QNetworkReply *reply, bool requestFileName)
379 {
380 if (!reply || reply->url().isEmpty())
381 return;
382 QVariant header = reply->header(QNetworkRequest::ContentLengthHeader);
383 bool ok;
384 int size = header.toInt(&ok);
385 if (ok && size == 0)
386 return;
387
388 qDebug() << "DownloadManager::handleUnsupportedContent" << reply->url() << "requestFileName" << requestFileName;
389 DownloadItem *item = new DownloadItem(reply, requestFileName, this);
390 addItem(item);
391 }
392
addItem(DownloadItem * item)393 void DownloadManager::addItem(DownloadItem *item)
394 {
395 connect(item, SIGNAL(statusChanged()), this, SLOT(updateRow()));
396 int row = m_downloads.count();
397 m_model->beginInsertRows(QModelIndex(), row, row);
398 m_downloads.append(item);
399 m_model->endInsertRows();
400 updateItemCount();
401 if (row == 0)
402 show();
403 downloadsView->setIndexWidget(m_model->index(row, 0), item);
404 QIcon icon = style()->standardIcon(QStyle::SP_FileIcon);
405 item->fileIcon->setPixmap(icon.pixmap(48, 48));
406 downloadsView->setRowHeight(row, item->sizeHint().height());
407 }
408
updateRow()409 void DownloadManager::updateRow()
410 {
411 DownloadItem *item = qobject_cast<DownloadItem*>(sender());
412 int row = m_downloads.indexOf(item);
413 if (-1 == row)
414 return;
415 if (!m_iconProvider)
416 m_iconProvider = new QFileIconProvider();
417 QIcon icon = m_iconProvider->icon(item->m_output.fileName());
418 if (icon.isNull())
419 icon = style()->standardIcon(QStyle::SP_FileIcon);
420 item->fileIcon->setPixmap(icon.pixmap(48, 48));
421 downloadsView->setRowHeight(row, item->minimumSizeHint().height());
422
423 bool remove = false;
424 QWebSettings *globalSettings = QWebSettings::globalSettings();
425 if (!item->downloading()
426 && globalSettings->testAttribute(QWebSettings::PrivateBrowsingEnabled))
427 remove = true;
428
429 if (item->downloadedSuccessfully()
430 && removePolicy() == DownloadManager::SuccessFullDownload) {
431 remove = true;
432 }
433 if (remove)
434 m_model->removeRow(row);
435
436 cleanupButton->setEnabled(m_downloads.count() - activeDownloads() > 0);
437 }
438
removePolicy() const439 DownloadManager::RemovePolicy DownloadManager::removePolicy() const
440 {
441 return m_removePolicy;
442 }
443
setRemovePolicy(RemovePolicy policy)444 void DownloadManager::setRemovePolicy(RemovePolicy policy)
445 {
446 if (policy == m_removePolicy)
447 return;
448 m_removePolicy = policy;
449 m_autoSaver->changeOccurred();
450 }
451
save() const452 void DownloadManager::save() const
453 {
454 QSettings settings;
455 settings.beginGroup(QLatin1String("downloadmanager"));
456 QMetaEnum removePolicyEnum = staticMetaObject.enumerator(staticMetaObject.indexOfEnumerator("RemovePolicy"));
457 settings.setValue(QLatin1String("removeDownloadsPolicy"), QLatin1String(removePolicyEnum.valueToKey(m_removePolicy)));
458 settings.setValue(QLatin1String("size"), size());
459 if (m_removePolicy == Exit)
460 return;
461
462 for (int i = 0; i < m_downloads.count(); ++i) {
463 QString key = QString(QLatin1String("download_%1_")).arg(i);
464 settings.setValue(key + QLatin1String("url"), m_downloads[i]->m_url);
465 settings.setValue(key + QLatin1String("location"), QFileInfo(m_downloads[i]->m_output).filePath());
466 settings.setValue(key + QLatin1String("done"), m_downloads[i]->downloadedSuccessfully());
467 }
468 int i = m_downloads.count();
469 QString key = QString(QLatin1String("download_%1_")).arg(i);
470 while (settings.contains(key + QLatin1String("url"))) {
471 settings.remove(key + QLatin1String("url"));
472 settings.remove(key + QLatin1String("location"));
473 settings.remove(key + QLatin1String("done"));
474 key = QString(QLatin1String("download_%1_")).arg(++i);
475 }
476 }
477
load()478 void DownloadManager::load()
479 {
480 QSettings settings;
481 settings.beginGroup(QLatin1String("downloadmanager"));
482 QSize size = settings.value(QLatin1String("size")).toSize();
483 if (size.isValid())
484 resize(size);
485 QByteArray value = settings.value(QLatin1String("removeDownloadsPolicy"), QLatin1String("Never")).toByteArray();
486 QMetaEnum removePolicyEnum = staticMetaObject.enumerator(staticMetaObject.indexOfEnumerator("RemovePolicy"));
487 m_removePolicy = removePolicyEnum.keyToValue(value) == -1 ?
488 Never :
489 static_cast<RemovePolicy>(removePolicyEnum.keyToValue(value));
490
491 int i = 0;
492 QString key = QString(QLatin1String("download_%1_")).arg(i);
493 while (settings.contains(key + QLatin1String("url"))) {
494 QUrl url = settings.value(key + QLatin1String("url")).toUrl();
495 QString fileName = settings.value(key + QLatin1String("location")).toString();
496 bool done = settings.value(key + QLatin1String("done"), true).toBool();
497 if (!url.isEmpty() && !fileName.isEmpty()) {
498 DownloadItem *item = new DownloadItem(0, this);
499 item->m_output.setFileName(fileName);
500 item->fileNameLabel->setText(QFileInfo(item->m_output.fileName()).fileName());
501 item->m_url = url;
502 item->stopButton->setVisible(false);
503 item->stopButton->setEnabled(false);
504 item->tryAgainButton->setVisible(!done);
505 item->tryAgainButton->setEnabled(!done);
506 item->progressBar->setVisible(!done);
507 addItem(item);
508 }
509 key = QString(QLatin1String("download_%1_")).arg(++i);
510 }
511 cleanupButton->setEnabled(m_downloads.count() - activeDownloads() > 0);
512 }
513
cleanup()514 void DownloadManager::cleanup()
515 {
516 if (m_downloads.isEmpty())
517 return;
518 m_model->removeRows(0, m_downloads.count());
519 updateItemCount();
520 if (m_downloads.isEmpty() && m_iconProvider) {
521 delete m_iconProvider;
522 m_iconProvider = 0;
523 }
524 m_autoSaver->changeOccurred();
525 }
526
updateItemCount()527 void DownloadManager::updateItemCount()
528 {
529 int count = m_downloads.count();
530 itemCount->setText(count == 1 ? tr("1 Download") : tr("%1 Downloads").arg(count));
531 }
532
DownloadModel(DownloadManager * downloadManager,QObject * parent)533 DownloadModel::DownloadModel(DownloadManager *downloadManager, QObject *parent)
534 : QAbstractListModel(parent)
535 , m_downloadManager(downloadManager)
536 {
537 }
538
data(const QModelIndex & index,int role) const539 QVariant DownloadModel::data(const QModelIndex &index, int role) const
540 {
541 if (index.row() < 0 || index.row() >= rowCount(index.parent()))
542 return QVariant();
543 if (role == Qt::ToolTipRole)
544 if (!m_downloadManager->m_downloads.at(index.row())->downloadedSuccessfully())
545 return m_downloadManager->m_downloads.at(index.row())->downloadInfoLabel->text();
546 return QVariant();
547 }
548
rowCount(const QModelIndex & parent) const549 int DownloadModel::rowCount(const QModelIndex &parent) const
550 {
551 return (parent.isValid()) ? 0 : m_downloadManager->m_downloads.count();
552 }
553
removeRows(int row,int count,const QModelIndex & parent)554 bool DownloadModel::removeRows(int row, int count, const QModelIndex &parent)
555 {
556 if (parent.isValid())
557 return false;
558
559 int lastRow = row + count - 1;
560 for (int i = lastRow; i >= row; --i) {
561 if (m_downloadManager->m_downloads.at(i)->downloadedSuccessfully()
562 || m_downloadManager->m_downloads.at(i)->tryAgainButton->isEnabled()) {
563 beginRemoveRows(parent, i, i);
564 m_downloadManager->m_downloads.takeAt(i)->deleteLater();
565 endRemoveRows();
566 }
567 }
568 m_downloadManager->m_autoSaver->changeOccurred();
569 return true;
570 }
571
572