Fixes: NB#295890 - Facebook "large" avatar needed to be scaled to wallpaper sized
[qtcontacts-tracker:hasselmms-contactsd.git] / plugins / telepathy / cdtpavatarupdate.cpp
1 /*********************************************************************************
2  ** This file is part of QtContacts tracker storage plugin
3  **
4  ** Copyright (c) 2011 Nokia Corporation and/or its subsidiary(-ies).
5  **
6  ** Contact:  Nokia Corporation (info@qt.nokia.com)
7  **
8  ** GNU Lesser General Public License Usage
9  ** This file may be used under the terms of the GNU Lesser General Public License
10  ** version 2.1 as published by the Free Software Foundation and appearing in the
11  ** file LICENSE.LGPL included in the packaging of this file.  Please review the
12  ** following information to ensure the GNU Lesser General Public License version
13  ** 2.1 requirements will be met:
14  ** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
15  **
16  ** In addition, as a special exception, Nokia gives you certain additional rights.
17  ** These rights are described in the Nokia Qt LGPL Exception version 1.1, included
18  ** in the file LGPL_EXCEPTION.txt in this package.
19  **
20  ** Other Usage
21  ** Alternatively, this file may be used in accordance with the terms and
22  ** conditions contained in a signed written agreement between you and Nokia.
23  *********************************************************************************/
24
25
26 #include "cdtpavatarupdate.h"
27 #include "cdtpplugin.h"
28 #include "debug.h"
29
30 #include <QtGui/QApplication>
31 #include <QtGui/QDesktopWidget>
32
33 #include <linux/sched.h>
34
35 using namespace Contactsd;
36
37 const QString CDTpAvatarUpdate::Large = QLatin1String("large");
38 const QString CDTpAvatarUpdate::Square = QLatin1String("square");
39 const QString CDTpAvatarUpdate::Wallpaper = QLatin1String("wallpaper");
40
41 static bool
42 writeAvatarFile(const QFileInfo &avatarFile, const QByteArray &avatarData)
43 {
44     const QDir cacheDir = avatarFile.absolutePath();
45
46     if (not cacheDir.exists() && not QDir::root().mkpath(cacheDir.absolutePath())) {
47         warning() << "Could not create avatar cache dir:" << cacheDir.path();
48         return false;
49     }
50
51     QTemporaryFile tempFile(cacheDir.absoluteFilePath(QLatin1String("pinkpony")));
52
53     if (tempFile.open() && avatarData.count() == tempFile.write(avatarData)) {
54         tempFile.close();
55
56         if (avatarFile.exists()) {
57             QFile::remove(avatarFile.filePath());
58         }
59
60         if (not tempFile.rename(avatarFile.filePath())) {
61             warning() << "Failed to rename temporary avatar file to"
62                       << avatarFile.filePath() << ":"
63                       << tempFile.errorString();
64             return false;
65         }
66
67         tempFile.setAutoRemove(false);
68         return true;
69     }
70
71     return false;
72 }
73
74 // use private thread pool for scaling since we want to give its threads idle priority
75 Q_GLOBAL_STATIC(QThreadPool, scalerThreadPool)
76
77 CDTpAvatarScaler::CDTpAvatarScaler(const QString &contactId,
78                                    const QFileInfo &sourceFile,
79                                    const QFileInfo &targetFile,
80                                    const QSize &targetSize)
81     : mContactId(contactId)
82     , mSourceFile(sourceFile)
83     , mTargetFile(targetFile)
84     , mTargetSize(targetSize)
85 {
86 }
87
88 void
89 CDTpAvatarScaler::run()
90 {
91     // Dramatically reduce priority of the scaler thread to keep the UI fluid.
92     // Do this on each run(), since QThreadPool will "randomly" create new
93     // threads, even when setting maxThreadCount() to 1.
94     const sched_param sched_idle_param = { 0 };
95     pthread_setschedparam(pthread_self(), SCHED_IDLE, &sched_idle_param);
96
97     QImage image;
98
99     if (not image.load(mSourceFile.filePath())) {
100         warning() << "Cannot read large avatar for" << mContactId
101                   << ":" << mSourceFile.filePath();
102         emit finished();
103         return;
104     }
105
106     image = image.scaled(mTargetSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
107
108     QBuffer imageBuffer;
109
110     if (not image.save(&imageBuffer, "JPEG")) {
111         warning() << "Cannot write wallpaper avatar for" << mContactId
112                   << ":" << mTargetFile.filePath();
113         emit finished();
114         return;
115     }
116
117     if (writeAvatarFile(mTargetFile, imageBuffer.data())) {
118         emit finished(mTargetFile.path());
119     } else {
120         emit finished();
121     }
122 }
123
124 CDTpAvatarUpdate::CDTpAvatarUpdate(QNetworkReply *networkReply,
125                                    CDTpContact *contactWrapper,
126                                    const QString &avatarType,
127                                    Qt::Orientation wallpaperOrientation,
128                                    QObject *parent)
129     : QObject(parent)
130     , mNetworkReply(0)
131     , mContactWrapper(contactWrapper)
132     , mAvatarType(avatarType)
133     , mCacheDir(CDTpPlugin::cacheFileName(QLatin1String("avatars")))
134     , mWallpaperOrientation(wallpaperOrientation)
135 {
136     setNetworkReply(networkReply);
137 }
138
139 CDTpAvatarUpdate::~CDTpAvatarUpdate()
140 {
141     setNetworkReply(0);
142 }
143
144 void
145 CDTpAvatarUpdate::setNetworkReply(QNetworkReply *networkReply)
146 {
147     if (mNetworkReply) {
148         mNetworkReply->disconnect(this);
149         mNetworkReply->deleteLater();
150     }
151
152     mNetworkReply = networkReply;
153
154     if (mNetworkReply) {
155         connect(mNetworkReply, SIGNAL(finished()), this, SLOT(onRequestFinished()));
156     }
157 }
158
159 bool
160 CDTpAvatarUpdate::updateWallpaperAvatar()
161 {
162     const QDir wallpaperCacheDir = mCacheDir.absoluteFilePath(Wallpaper);
163     const QFileInfo wallpaperFile = wallpaperCacheDir.absoluteFilePath(mAvatarFile.fileName());
164
165     if (wallpaperFile.exists() && wallpaperFile.lastModified() >= mAvatarFile.lastModified()) {
166         return false; // assume nothing to do
167     }
168
169     // apply out desired wall paper orientation
170     QSize wallpaperSize = QApplication::desktop()->size();
171
172     if ((mWallpaperOrientation == Qt::Horizontal &&
173          wallpaperSize.width() < wallpaperSize.height()) ||
174         (mWallpaperOrientation == Qt::Vertical &&
175          wallpaperSize.height() < wallpaperSize.width())) {
176         wallpaperSize = QSize(wallpaperSize.height(), wallpaperSize.width());
177     }
178
179     // create image scaler thread
180     QScopedPointer<CDTpAvatarScaler> scaler
181             (new CDTpAvatarScaler(mContactWrapper->contact()->id(),
182                                   mAvatarFile, wallpaperFile, wallpaperSize));
183
184     connect(scaler.data(), SIGNAL(finished(QString)),
185             this, SLOT(onScalerFinished(QString)));
186     scalerThreadPool()->start(scaler.take());
187
188     return true;
189 }
190
191 static bool
192 acceptFileSize(qint64 actualFileSize, qint64 expectedFileSize)
193 {
194     if (expectedFileSize > 0) {
195         return (actualFileSize == expectedFileSize);
196     }
197
198     return actualFileSize > 0;
199 }
200
201 void
202 CDTpAvatarUpdate::onRequestFinished()
203 {
204     if (mNetworkReply.isNull() || mNetworkReply->error() != QNetworkReply::NoError) {
205         mAvatarFile = QString();
206         setNetworkReply(0);
207         emit finished();
208         return;
209     }
210
211     // Build filename from the image URL's SHA1 hash.
212     const QUrl redirectionTarget = mNetworkReply->attribute(QNetworkRequest::RedirectionTargetAttribute).toUrl();
213     const QString avatarUrl = (not redirectionTarget.isEmpty() ? mNetworkReply->url().resolved(redirectionTarget)
214                                                                : mNetworkReply->url()).toString();
215
216     QByteArray avatarHash = QCryptographicHash::hash(avatarUrl.toUtf8(), QCryptographicHash::Sha1);
217     const QString avatarFileName = QString::fromAscii(avatarHash.toHex());
218     const QDir avatarCacheDir = mCacheDir.absoluteFilePath(mAvatarType);
219     QFileInfo avatarFile(avatarCacheDir.absoluteFilePath(avatarFileName));
220
221     // Check for existing avatar file and its size to see if we need to fetch from network.
222     const qint64 contentLength = mNetworkReply->header(QNetworkRequest::ContentLengthHeader).toLongLong();
223
224     if (avatarFile.exists() && acceptFileSize(avatarFile.size(), contentLength)) {
225         // Seems we can reuse the existing avatar file.
226         mAvatarFile = avatarFile;
227     } else {
228         // Follow redirections as done by Facebook's graph API.
229         if (not redirectionTarget.isEmpty()) {
230             setNetworkReply(mNetworkReply->manager()->get(QNetworkRequest(redirectionTarget)));
231             return;
232         }
233
234         // Facebook delivers a distinct gif image if no avatar is set. Ignore that bugger.
235         const QString contentType = mNetworkReply->header(QNetworkRequest::ContentTypeHeader).toString();
236
237         static const QLatin1String contentTypeImageGif = QLatin1String("image/gif");
238         static const QLatin1String contentTypeImage = QLatin1String("image/");
239
240         if (contentType.startsWith(contentTypeImage) && contentType != contentTypeImageGif) {
241             if (writeAvatarFile(avatarFile, mNetworkReply->readAll())) {
242                 mAvatarFile = avatarFile;
243             }
244         }
245     }
246
247     setNetworkReply(0);
248
249     // Update the contact if a new avatar is available.
250     bool havePendingTasks = false;
251
252     if (mAvatarFile.isFile() && not mContactWrapper.isNull()) {
253         if (mAvatarType == Square) {
254             mContactWrapper->setSquareAvatarPath(mAvatarFile.filePath());
255         } else if (mAvatarType == Large) {
256             mContactWrapper->setLargeAvatarPath(mAvatarFile.filePath());
257             havePendingTasks |= updateWallpaperAvatar();
258         }
259     }
260
261     if (not havePendingTasks) {
262         emit finished();
263     }
264 }
265
266 void
267 CDTpAvatarUpdate::onScalerFinished(const QString &avatarPath)
268 {
269     if (not avatarPath.isEmpty()) {
270         mContactWrapper->setWallpaperAvatarPath(avatarPath);
271     }
272
273     emit finished();
274 }