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