1 /**************************************************************************
2 * *
3 * Copyright (C) 2016 Felix Rohrbach <kde@fxrh.de> *
4 * *
5 * This program is free software; you can redistribute it and/or *
6 * modify it under the terms of the GNU General Public License *
7 * as published by the Free Software Foundation; either version 3 *
8 * of the License, or (at your option) any later version. *
9 * *
10 * This program is distributed in the hope that it will be useful, *
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of *
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
13 * GNU General Public License for more details. *
14 * *
15 * You should have received a copy of the GNU General Public License *
16 * along with this program. If not, see <http://www.gnu.org/licenses/>. *
17 * *
18 **************************************************************************/
19
20 #include "imageprovider.h"
21
22 #include <connection.h>
23 #include <jobs/mediathumbnailjob.h>
24
25 #include <QtCore/QReadWriteLock>
26 #include <QtCore/QThread>
27 #include <QtCore/QDebug>
28
29 using Quotient::Connection;
30 using Quotient::BaseJob;
31
32 class ThumbnailResponse : public QQuickImageResponse
33 {
34 Q_OBJECT
35 public:
ThumbnailResponse(Connection * c,QString id,QSize size)36 ThumbnailResponse(Connection* c, QString id, QSize size)
37 : c(c), mediaId(std::move(id)), requestedSize(size)
38 {
39 if (!c)
40 errorStr = tr("No connection to perform image request");
41 else if (mediaId.count('/') != 1)
42 errorStr =
43 tr("Media id '%1' doesn't follow server/mediaId pattern")
44 .arg(mediaId);
45 else if (requestedSize.isEmpty()) {
46 qDebug() << "ThumbnailResponse: returning an empty image for"
47 << mediaId << "due to empty" << requestedSize;
48 image = {requestedSize, QImage::Format_Invalid};
49 }
50 if (!errorStr.isEmpty() || requestedSize.isEmpty()) {
51 emit finished();
52 return;
53 }
54 // We are good to go
55 qDebug().nospace() << "ThumbnailResponse: requesting " << mediaId
56 << ", " << size;
57 errorStr = tr("Image request is pending");
58
59 // Execute a request on the main thread asynchronously
60 moveToThread(c->thread());
61 QMetaObject::invokeMethod(this,
62 #if (QT_VERSION >= QT_VERSION_CHECK(5, 10, 0))
63 &ThumbnailResponse::startRequest
64 #else
65 "startRequest"
66 #endif
67 );
68 }
69 ~ThumbnailResponse() override = default;
70
71 private slots:
72 // All these run in the main thread, not QML thread
73
startRequest()74 void startRequest()
75 {
76 Q_ASSERT(QThread::currentThread() == c->thread());
77
78 job = c->getThumbnail(mediaId, requestedSize);
79 // Connect to any possible outcome including abandonment
80 // to make sure the QML thread is not left stuck forever.
81 connect(job, &BaseJob::finished,
82 this, &ThumbnailResponse::prepareResult);
83 }
84
prepareResult()85 void prepareResult()
86 {
87 Q_ASSERT(QThread::currentThread() == job->thread());
88 Q_ASSERT(job->error() != BaseJob::Pending);
89 {
90 QWriteLocker _(&lock);
91 if (job->error() == BaseJob::Success)
92 {
93 image = job->thumbnail();
94 errorStr.clear();
95 qDebug().nospace() << "ThumbnailResponse: image ready for "
96 << mediaId << ", " << image.size();
97 } else if (job->error() == BaseJob::Abandoned) {
98 errorStr = tr("Image request has been cancelled");
99 qDebug() << "ThumbnailResponse: cancelled for" << mediaId;
100 } else {
101 errorStr = job->errorString();
102 qWarning() << "ThumbnailResponse: no valid image for"
103 << mediaId << "-" << errorStr;
104 }
105 }
106 job = nullptr;
107 emit finished();
108 }
109
doCancel()110 void doCancel()
111 {
112 if (job)
113 {
114 Q_ASSERT(QThread::currentThread() == job->thread());
115 job->abandon();
116 }
117 }
118
119 private:
120 Connection* c;
121 const QString mediaId;
122 const QSize requestedSize;
123 Quotient::MediaThumbnailJob* job = nullptr;
124
125 QImage image;
126 QString errorStr;
127 mutable QReadWriteLock lock; // Guards ONLY these two above
128
129 // The following overrides run in QML thread
130
textureFactory() const131 QQuickTextureFactory *textureFactory() const override
132 {
133 QReadLocker _(&lock);
134 return QQuickTextureFactory::textureFactoryForImage(image);
135 }
136
errorString() const137 QString errorString() const override
138 {
139 QReadLocker _(&lock);
140 return errorStr;
141 }
142
cancel()143 void cancel() override
144 {
145 // Flip from QML thread to the main thread
146 QMetaObject::invokeMethod(this,
147 #if (QT_VERSION >= QT_VERSION_CHECK(5, 10, 0))
148 &ThumbnailResponse::doCancel
149 #else
150 "doCancel"
151 #endif
152 );
153 }
154 };
155
156 #include "imageprovider.moc" // Because we define a Q_OBJECT in the cpp file
157
ImageProvider(Connection * connection)158 ImageProvider::ImageProvider(Connection* connection)
159 : m_connection(connection)
160 { }
161
162 #if QT_VERSION < QT_VERSION_CHECK(5, 14, 0)
163 # define LOAD_ATOMIC(Ptr) Ptr.load()
164 # define STORE_ATOMIC(Ptr, NewValue) Ptr.store(NewValue)
165 #else
166 # define LOAD_ATOMIC(Ptr) Ptr.loadRelaxed()
167 # define STORE_ATOMIC(Ptr, NewValue) Ptr.storeRelaxed(NewValue)
168 #endif
169
requestImageResponse(const QString & id,const QSize & requestedSize)170 QQuickImageResponse* ImageProvider::requestImageResponse(
171 const QString& id, const QSize& requestedSize)
172 {
173 auto size = requestedSize;
174 // Force integer overflow if the value is -1 - may cause issues when
175 // screens resolution becomes 100K+ each dimension :-D
176 if (size.width() == -1)
177 size.setWidth(ushort(-1));
178 if (size.height() == -1)
179 size.setHeight(ushort(-1));
180 return new ThumbnailResponse(LOAD_ATOMIC(m_connection), id, size);
181 }
182
setConnection(Connection * connection)183 void ImageProvider::setConnection(Connection* connection)
184 {
185 STORE_ATOMIC(m_connection, connection);
186 }
187