1/****************************************************************************
2**
3** Copyright (C) 2016 The Qt Company Ltd.
4** Contact: https://www.qt.io/licensing/
5**
6** This file is part of the plugins of the Qt Toolkit.
7**
8** $QT_BEGIN_LICENSE:LGPL$
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 https://www.qt.io/terms-conditions. For further
15** information use the contact form at https://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 3 as published by the Free Software
20** Foundation and appearing in the file LICENSE.LGPL3 included in the
21** packaging of this file. Please review the following information to
22** ensure the GNU Lesser General Public License version 3 requirements
23** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
24**
25** GNU General Public License Usage
26** Alternatively, this file may be used under the terms of the GNU
27** General Public License version 2.0 or (at your option) the GNU General
28** Public license version 3 or any later version approved by the KDE Free
29** Qt Foundation. The licenses are as published by the Free Software
30** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
31** included in the packaging of this file. Please review the following
32** information to ensure the GNU General Public License requirements will
33** be met: https://www.gnu.org/licenses/gpl-2.0.html and
34** https://www.gnu.org/licenses/gpl-3.0.html.
35**
36** $QT_END_LICENSE$
37**
38****************************************************************************/
39
40#include "qiosfileengineassetslibrary.h"
41
42#import <UIKit/UIKit.h>
43#import <AssetsLibrary/AssetsLibrary.h>
44
45#include <QtCore/QTimer>
46#include <QtCore/private/qcoreapplication_p.h>
47#include <QtCore/qurl.h>
48#include <QtCore/qset.h>
49#include <QtCore/qthreadstorage.h>
50
51QT_BEGIN_NAMESPACE
52
53static QThreadStorage<QString> g_iteratorCurrentUrl;
54static QThreadStorage<QPointer<QIOSAssetData> > g_assetDataCache;
55
56static const int kBufferSize = 10;
57static ALAsset *kNoAsset = 0;
58
59static bool ensureAuthorizationDialogNotBlocked()
60{
61    if ([ALAssetsLibrary authorizationStatus] != ALAuthorizationStatusNotDetermined)
62        return true;
63
64    if (static_cast<QCoreApplicationPrivate *>(QObjectPrivate::get(qApp))->in_exec)
65        return true;
66
67    if ([NSThread isMainThread]) {
68        // The dialog is about to show, but since main has not finished, the dialog will be held
69        // back until the launch completes. This is problematic since we cannot successfully return
70        // back to the caller before the asset is ready, which also includes showing the dialog. To
71        // work around this, we create an event loop to that will complete the launch (return from the
72        // applicationDidFinishLaunching callback). But this will only work if we're on the main thread.
73        QEventLoop loop;
74        QTimer::singleShot(1, &loop, &QEventLoop::quit);
75        loop.exec();
76    } else {
77        NSLog(@"QIOSFileEngine: unable to show assets authorization dialog from non-gui thread before QApplication is executing.");
78        return false;
79    }
80
81    return true;
82}
83
84// -------------------------------------------------------------------------
85
86class QIOSAssetEnumerator
87{
88public:
89    QIOSAssetEnumerator(ALAssetsLibrary *assetsLibrary, ALAssetsGroupType type)
90        : m_semWriteAsset(dispatch_semaphore_create(kBufferSize))
91        , m_semReadAsset(dispatch_semaphore_create(0))
92        , m_stop(false)
93        , m_assetsLibrary([assetsLibrary retain])
94        , m_type(type)
95        , m_buffer(QVector<ALAsset *>(kBufferSize))
96        , m_readIndex(0)
97        , m_writeIndex(0)
98        , m_nextAssetReady(false)
99    {
100        if (!ensureAuthorizationDialogNotBlocked())
101            writeAsset(kNoAsset);
102        else
103            startEnumerate();
104    }
105
106    ~QIOSAssetEnumerator()
107    {
108        m_stop = true;
109
110        // Flush and autorelease remaining assets in the buffer
111        while (hasNext())
112            next();
113
114        // Documentation states that we need to balance out calls to 'wait'
115        // and 'signal'. Since the enumeration function always will be one 'wait'
116        // ahead, we need to signal m_semProceedToNextAsset one last time.
117        dispatch_semaphore_signal(m_semWriteAsset);
118        dispatch_release(m_semReadAsset);
119        dispatch_release(m_semWriteAsset);
120
121        [m_assetsLibrary autorelease];
122    }
123
124    bool hasNext()
125    {
126        if (!m_nextAssetReady) {
127            dispatch_semaphore_wait(m_semReadAsset, DISPATCH_TIME_FOREVER);
128            m_nextAssetReady = true;
129        }
130        return m_buffer[m_readIndex] != kNoAsset;
131    }
132
133    ALAsset *next()
134    {
135        Q_ASSERT(m_nextAssetReady);
136        Q_ASSERT(m_buffer[m_readIndex]);
137
138        ALAsset *asset = [m_buffer[m_readIndex] autorelease];
139        dispatch_semaphore_signal(m_semWriteAsset);
140
141        m_readIndex = (m_readIndex + 1) % kBufferSize;
142        m_nextAssetReady = false;
143        return asset;
144    }
145
146private:
147    dispatch_semaphore_t m_semWriteAsset;
148    dispatch_semaphore_t m_semReadAsset;
149    std::atomic_bool m_stop;
150
151    ALAssetsLibrary *m_assetsLibrary;
152    ALAssetsGroupType m_type;
153    QVector<ALAsset *> m_buffer;
154    int m_readIndex;
155    int m_writeIndex;
156    bool m_nextAssetReady;
157
158    void writeAsset(ALAsset *asset)
159    {
160        dispatch_semaphore_wait(m_semWriteAsset, DISPATCH_TIME_FOREVER);
161        m_buffer[m_writeIndex] = [asset retain];
162        dispatch_semaphore_signal(m_semReadAsset);
163        m_writeIndex = (m_writeIndex + 1) % kBufferSize;
164    }
165
166    void startEnumerate()
167    {
168        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
169            [m_assetsLibrary enumerateGroupsWithTypes:m_type usingBlock:^(ALAssetsGroup *group, BOOL *stopEnumerate) {
170
171                if (!group) {
172                    writeAsset(kNoAsset);
173                    return;
174                }
175
176                if (m_stop) {
177                    *stopEnumerate = true;
178                    return;
179                }
180
181                [group enumerateAssetsUsingBlock:^(ALAsset *asset, NSUInteger index, BOOL *stopEnumerate) {
182                    Q_UNUSED(index);
183                    if (!asset || ![[asset valueForProperty:ALAssetPropertyType] isEqual:ALAssetTypePhoto])
184                       return;
185
186                    writeAsset(asset);
187                    *stopEnumerate = m_stop;
188                }];
189            } failureBlock:^(NSError *error) {
190                NSLog(@"QIOSFileEngine: %@", error);
191                writeAsset(kNoAsset);
192            }];
193        });
194    }
195
196};
197
198// -------------------------------------------------------------------------
199
200class QIOSAssetData : public QObject
201{
202public:
203    QIOSAssetData(const QString &assetUrl, QIOSFileEngineAssetsLibrary *engine)
204        : m_asset(0)
205        , m_assetUrl(assetUrl)
206        , m_assetLibrary(0)
207    {
208        if (!ensureAuthorizationDialogNotBlocked())
209            return;
210
211        if (QIOSAssetData *assetData = g_assetDataCache.localData()) {
212            // It's a common pattern that QFiles pointing to the same path are created and destroyed
213            // several times during a single event loop cycle. To avoid loading the same asset
214            // over and over, we check if the last loaded asset has not been destroyed yet, and try to
215            // reuse its data.
216            if (assetData->m_assetUrl == assetUrl) {
217                m_assetLibrary = [assetData->m_assetLibrary retain];
218                m_asset = [assetData->m_asset retain];
219                return;
220            }
221        }
222
223        // We can only load images from the asset library async. And this might take time, since it
224        // involves showing the authorization dialog. But the QFile API is synchronuous, so we need to
225        // wait until we have access to the data. [ALAssetLibrary assetForUrl:] will shedule a block on
226        // the current thread. But instead of spinning the event loop to force the block to execute, we
227        // wrap the call inside a synchronuous dispatch queue so that it executes on another thread.
228        dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
229
230        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
231            NSURL *url = [NSURL URLWithString:assetUrl.toNSString()];
232            m_assetLibrary = [[ALAssetsLibrary alloc] init];
233            [m_assetLibrary assetForURL:url resultBlock:^(ALAsset *asset) {
234
235                if (!asset) {
236                    // When an asset couldn't be loaded, chances are that it belongs to ALAssetsGroupPhotoStream.
237                    // Such assets can be stored in the cloud and might need to be downloaded first. Unfortunately,
238                    // forcing that to happen is hidden behind private APIs ([ALAsset requestDefaultRepresentation]).
239                    // As a work-around, we search for it instead, since that will give us a pointer to the asset.
240                    QIOSAssetEnumerator e(m_assetLibrary, ALAssetsGroupPhotoStream);
241                    while (e.hasNext()) {
242                        ALAsset *a = e.next();
243                        QString url = QUrl::fromNSURL([a valueForProperty:ALAssetPropertyAssetURL]).toString();
244                        if (url == assetUrl) {
245                            asset = a;
246                            break;
247                        }
248                    }
249                }
250
251                if (!asset)
252                    engine->setError(QFile::OpenError, QLatin1String("could not open image"));
253
254                m_asset = [asset retain];
255                dispatch_semaphore_signal(semaphore);
256            } failureBlock:^(NSError *error) {
257                engine->setError(QFile::OpenError, QString::fromNSString(error.localizedDescription));
258                dispatch_semaphore_signal(semaphore);
259            }];
260        });
261
262        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
263        dispatch_release(semaphore);
264
265        g_assetDataCache.setLocalData(this);
266    }
267
268    ~QIOSAssetData()
269    {
270        [m_assetLibrary release];
271        [m_asset release];
272        if (g_assetDataCache.localData() == this)
273            g_assetDataCache.setLocalData(0);
274    }
275
276    ALAsset *m_asset;
277
278private:
279    QString m_assetUrl;
280    ALAssetsLibrary *m_assetLibrary;
281};
282
283// -------------------------------------------------------------------------
284
285#ifndef QT_NO_FILESYSTEMITERATOR
286
287class QIOSFileEngineIteratorAssetsLibrary : public QAbstractFileEngineIterator
288{
289public:
290    QIOSAssetEnumerator *m_enumerator;
291
292    QIOSFileEngineIteratorAssetsLibrary(
293            QDir::Filters filters, const QStringList &nameFilters)
294        : QAbstractFileEngineIterator(filters, nameFilters)
295        , m_enumerator(new QIOSAssetEnumerator([[[ALAssetsLibrary alloc] init] autorelease], ALAssetsGroupAll))
296    {
297    }
298
299    ~QIOSFileEngineIteratorAssetsLibrary()
300    {
301        delete m_enumerator;
302        g_iteratorCurrentUrl.setLocalData(QString());
303    }
304
305    QString next() override
306    {
307        // Cache the URL that we are about to return, since QDir will immediately create a
308        // new file engine on the file and ask if it exists. Unless we do this, we end up
309        // creating a new ALAsset just to verify its existence, which will be especially
310        // costly for assets belonging to ALAssetsGroupPhotoStream.
311        ALAsset *asset = m_enumerator->next();
312        QString url = QUrl::fromNSURL([asset valueForProperty:ALAssetPropertyAssetURL]).toString();
313        g_iteratorCurrentUrl.setLocalData(url);
314        return url;
315    }
316
317    bool hasNext() const override
318    {
319        return m_enumerator->hasNext();
320    }
321
322    QString currentFileName() const override
323    {
324        return g_iteratorCurrentUrl.localData();
325    }
326
327    QFileInfo currentFileInfo() const override
328    {
329        return QFileInfo(currentFileName());
330    }
331};
332
333#endif
334
335// -------------------------------------------------------------------------
336
337QIOSFileEngineAssetsLibrary::QIOSFileEngineAssetsLibrary(const QString &fileName)
338    : m_offset(0)
339    , m_data(0)
340{
341    setFileName(fileName);
342}
343
344QIOSFileEngineAssetsLibrary::~QIOSFileEngineAssetsLibrary()
345{
346    close();
347}
348
349ALAsset *QIOSFileEngineAssetsLibrary::loadAsset() const
350{
351    if (!m_data)
352        m_data = new QIOSAssetData(m_assetUrl, const_cast<QIOSFileEngineAssetsLibrary *>(this));
353    return m_data->m_asset;
354}
355
356bool QIOSFileEngineAssetsLibrary::open(QIODevice::OpenMode openMode)
357{
358    if (openMode & (QIODevice::WriteOnly | QIODevice::Text))
359        return false;
360    return loadAsset();
361}
362
363bool QIOSFileEngineAssetsLibrary::close()
364{
365    if (m_data) {
366        // Delete later, so that we can reuse the asset if a QFile is
367        // opened with the same path during the same event loop cycle.
368        m_data->deleteLater();
369        m_data = 0;
370    }
371    return true;
372}
373
374QAbstractFileEngine::FileFlags QIOSFileEngineAssetsLibrary::fileFlags(QAbstractFileEngine::FileFlags type) const
375{
376    QAbstractFileEngine::FileFlags flags;
377    const bool isDir = (m_assetUrl == QLatin1String("assets-library://"));
378    const bool exists = isDir || m_assetUrl == g_iteratorCurrentUrl.localData() || loadAsset();
379
380    if (!exists)
381        return flags;
382
383    if (type & FlagsMask)
384        flags |= ExistsFlag;
385    if (type & PermsMask) {
386        ALAuthorizationStatus status = [ALAssetsLibrary authorizationStatus];
387        if (status != ALAuthorizationStatusRestricted && status != ALAuthorizationStatusDenied)
388            flags |= ReadOwnerPerm | ReadUserPerm | ReadGroupPerm | ReadOtherPerm;
389    }
390    if (type & TypesMask)
391        flags |= isDir ? DirectoryType : FileType;
392
393    return flags;
394}
395
396qint64 QIOSFileEngineAssetsLibrary::size() const
397{
398    if (ALAsset *asset = loadAsset())
399        return [[asset defaultRepresentation] size];
400    return 0;
401}
402
403qint64 QIOSFileEngineAssetsLibrary::read(char *data, qint64 maxlen)
404{
405    ALAsset *asset = loadAsset();
406    if (!asset)
407        return -1;
408
409    qint64 bytesRead = qMin(maxlen, size() - m_offset);
410    if (!bytesRead)
411        return 0;
412
413    NSError *error = 0;
414    [[asset defaultRepresentation] getBytes:(uint8_t *)data fromOffset:m_offset length:bytesRead error:&error];
415
416    if (error) {
417        setError(QFile::ReadError, QString::fromNSString(error.localizedDescription));
418        return -1;
419    }
420
421    m_offset += bytesRead;
422    return bytesRead;
423}
424
425qint64 QIOSFileEngineAssetsLibrary::pos() const
426{
427    return m_offset;
428}
429
430bool QIOSFileEngineAssetsLibrary::seek(qint64 pos)
431{
432    if (pos >= size())
433        return false;
434    m_offset = pos;
435    return true;
436}
437
438QString QIOSFileEngineAssetsLibrary::fileName(FileName file) const
439{
440    Q_UNUSED(file);
441    return m_fileName;
442}
443
444void QIOSFileEngineAssetsLibrary::setFileName(const QString &file)
445{
446    if (m_data)
447        close();
448    m_fileName = file;
449    // QUrl::fromLocalFile() will remove double slashes. Since the asset url is
450    // passed around as a file name in the app (and converted to/from a file url, e.g
451    // in QFileDialog), we need to ensure that m_assetUrl ends up being valid.
452    int index = file.indexOf(QLatin1String("/asset"));
453    if (index == -1)
454        m_assetUrl = QLatin1String("assets-library://");
455    else
456        m_assetUrl = QLatin1String("assets-library:/") + file.mid(index);
457}
458
459QStringList QIOSFileEngineAssetsLibrary::entryList(QDir::Filters filters, const QStringList &filterNames) const
460{
461    return QAbstractFileEngine::entryList(filters, filterNames);
462}
463
464#ifndef QT_NO_FILESYSTEMITERATOR
465
466QAbstractFileEngine::Iterator *QIOSFileEngineAssetsLibrary::beginEntryList(
467        QDir::Filters filters, const QStringList &filterNames)
468{
469    return new QIOSFileEngineIteratorAssetsLibrary(filters, filterNames);
470}
471
472QAbstractFileEngine::Iterator *QIOSFileEngineAssetsLibrary::endEntryList()
473{
474    return 0;
475}
476
477QT_END_NAMESPACE
478
479#endif
480