diff options
Diffstat (limited to 'src/plugins/platforms/ios/qiosfileengineassetslibrary.mm')
-rw-r--r-- | src/plugins/platforms/ios/qiosfileengineassetslibrary.mm | 320 |
1 files changed, 261 insertions, 59 deletions
diff --git a/src/plugins/platforms/ios/qiosfileengineassetslibrary.mm b/src/plugins/platforms/ios/qiosfileengineassetslibrary.mm index 73bfc2a87f..44a7901160 100644 --- a/src/plugins/platforms/ios/qiosfileengineassetslibrary.mm +++ b/src/plugins/platforms/ios/qiosfileengineassetslibrary.mm @@ -38,6 +38,145 @@ #include <QtCore/QTimer> #include <QtCore/private/qcoreapplication_p.h> +#include <QtCore/qurl.h> +#include <QtCore/qset.h> +#include <QtCore/qthreadstorage.h> + +static QThreadStorage<QString> g_iteratorCurrentUrl; +static QThreadStorage<QPointer<QIOSAssetData> > g_assetDataCache; + +static const int kBufferSize = 10; +static ALAsset *kNoAsset = 0; + +static void ensureAuthorizationDialogNotBlocked() +{ + if ([ALAssetsLibrary authorizationStatus] != ALAuthorizationStatusNotDetermined) + return; + if (static_cast<QCoreApplicationPrivate *>(QObjectPrivate::get(qApp))->in_exec) + return; + + // Since authorization status has not been determined, the user will be asked + // to authorize the app. But since main has not finished, the dialog will be held + // back until the launch completes. To avoid a dead-lock below, we start an event + // loop to complete the launch. + QEventLoop loop; + QTimer::singleShot(1, &loop, &QEventLoop::quit); + loop.exec(); +} + +// ------------------------------------------------------------------------- + +class QIOSAssetEnumerator +{ +public: + QIOSAssetEnumerator(ALAssetsLibrary *assetsLibrary, ALAssetsGroupType type) + : m_semWriteAsset(dispatch_semaphore_create(kBufferSize)) + , m_semReadAsset(dispatch_semaphore_create(0)) + , m_stop(false) + , m_assetsLibrary([assetsLibrary retain]) + , m_type(type) + , m_buffer(QVector<ALAsset *>(kBufferSize)) + , m_readIndex(0) + , m_writeIndex(0) + , m_nextAssetReady(false) + { + ensureAuthorizationDialogNotBlocked(); + startEnumerate(); + } + + ~QIOSAssetEnumerator() + { + m_stop = true; + + // Flush and autorelease remaining assets in the buffer + while (hasNext()) + next(); + + // Documentation states that we need to balance out calls to 'wait' + // and 'signal'. Since the enumeration function always will be one 'wait' + // ahead, we need to signal m_semProceedToNextAsset one last time. + dispatch_semaphore_signal(m_semWriteAsset); + dispatch_release(m_semReadAsset); + dispatch_release(m_semWriteAsset); + + [m_assetsLibrary autorelease]; + } + + bool hasNext() + { + if (!m_nextAssetReady) { + dispatch_semaphore_wait(m_semReadAsset, DISPATCH_TIME_FOREVER); + m_nextAssetReady = true; + } + return m_buffer[m_readIndex] != kNoAsset; + } + + ALAsset *next() + { + Q_ASSERT(m_nextAssetReady); + Q_ASSERT(m_buffer[m_readIndex]); + + ALAsset *asset = [m_buffer[m_readIndex] autorelease]; + dispatch_semaphore_signal(m_semWriteAsset); + + m_readIndex = (m_readIndex + 1) % kBufferSize; + m_nextAssetReady = false; + return asset; + } + +private: + dispatch_semaphore_t m_semWriteAsset; + dispatch_semaphore_t m_semReadAsset; + std::atomic_bool m_stop; + + ALAssetsLibrary *m_assetsLibrary; + ALAssetsGroupType m_type; + QVector<ALAsset *> m_buffer; + int m_readIndex; + int m_writeIndex; + bool m_nextAssetReady; + + void writeAsset(ALAsset *asset) + { + dispatch_semaphore_wait(m_semWriteAsset, DISPATCH_TIME_FOREVER); + m_buffer[m_writeIndex] = [asset retain]; + dispatch_semaphore_signal(m_semReadAsset); + m_writeIndex = (m_writeIndex + 1) % kBufferSize; + } + + void startEnumerate() + { + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + [m_assetsLibrary enumerateGroupsWithTypes:m_type usingBlock:^(ALAssetsGroup *group, BOOL *stopEnumerate) { + + if (!group) { + writeAsset(kNoAsset); + return; + } + + if (m_stop) { + *stopEnumerate = true; + return; + } + + [group enumerateAssetsUsingBlock:^(ALAsset *asset, NSUInteger index, BOOL *stopEnumerate) { + Q_UNUSED(index); + if (!asset || ![[asset valueForProperty:ALAssetPropertyType] isEqual:ALAssetTypePhoto]) + return; + + writeAsset(asset); + *stopEnumerate = m_stop; + }]; + } failureBlock:^(NSError *error) { + NSLog(@"QIOSFileEngine: %@", error); + writeAsset(kNoAsset); + }]; + }); + } + +}; + +// ------------------------------------------------------------------------- class QIOSAssetData : public QObject { @@ -47,35 +186,17 @@ public: , m_assetUrl(assetUrl) , m_assetLibrary(0) { - switch ([ALAssetsLibrary authorizationStatus]) { - case ALAuthorizationStatusRestricted: - case ALAuthorizationStatusDenied: - engine->setError(QFile::PermissionsError, QLatin1String("Unauthorized access")); - return; - case ALAuthorizationStatusNotDetermined: - if (!static_cast<QCoreApplicationPrivate *>(QObjectPrivate::get(qApp))->in_exec) { - // Since authorization status has not been determined, the user will be asked - // to authorize the app. But since main has not finished, the dialog will be held - // back until the launch completes. To avoid a dead-lock below, we start an event - // loop to complete the launch. - QEventLoop loop; - QTimer::singleShot(1, &loop, &QEventLoop::quit); - loop.exec(); - } - break; - default: - if (g_currentAssetData) { - // It's a common pattern that QFiles pointing to the same path are created and destroyed - // several times during a single event loop cycle. To avoid loading the same asset - // over and over, we check if the last loaded asset has not been destroyed yet, and try to - // reuse its data. Since QFile is (mostly) reentrant, we need to protect m_currentAssetData - // from being modified by several threads at the same time. - QMutexLocker lock(&g_mutex); - if (g_currentAssetData && g_currentAssetData->m_assetUrl == assetUrl) { - m_assetLibrary = [g_currentAssetData->m_assetLibrary retain]; - m_asset = [g_currentAssetData->m_asset retain]; - return; - } + ensureAuthorizationDialogNotBlocked(); + + if (QIOSAssetData *assetData = g_assetDataCache.localData()) { + // It's a common pattern that QFiles pointing to the same path are created and destroyed + // several times during a single event loop cycle. To avoid loading the same asset + // over and over, we check if the last loaded asset has not been destroyed yet, and try to + // reuse its data. + if (assetData->m_assetUrl == assetUrl) { + m_assetLibrary = [assetData->m_assetLibrary retain]; + m_asset = [assetData->m_asset retain]; + return; } } @@ -90,6 +211,26 @@ public: NSURL *url = [NSURL URLWithString:assetUrl.toNSString()]; m_assetLibrary = [[ALAssetsLibrary alloc] init]; [m_assetLibrary assetForURL:url resultBlock:^(ALAsset *asset) { + + if (!asset) { + // When an asset couldn't be loaded, chances are that it belongs to ALAssetsGroupPhotoStream. + // Such assets can be stored in the cloud and might need to be downloaded first. Unfortunately, + // forcing that to happen is hidden behind private APIs ([ALAsset requestDefaultRepresentation]). + // As a work-around, we search for it instead, since that will give us a pointer to the asset. + QIOSAssetEnumerator e(m_assetLibrary, ALAssetsGroupPhotoStream); + while (e.hasNext()) { + ALAsset *a = e.next(); + QString url = QUrl::fromNSURL([a valueForProperty:ALAssetPropertyAssetURL]).toString(); + if (url == assetUrl) { + asset = a; + break; + } + } + } + + if (!asset) + engine->setError(QFile::OpenError, QLatin1String("could not open image")); + m_asset = [asset retain]; dispatch_semaphore_signal(semaphore); } failureBlock:^(NSError *error) { @@ -101,17 +242,15 @@ public: dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); dispatch_release(semaphore); - QMutexLocker lock(&g_mutex); - g_currentAssetData = this; + g_assetDataCache.setLocalData(this); } ~QIOSAssetData() { - QMutexLocker lock(&g_mutex); [m_assetLibrary release]; [m_asset release]; - if (this == g_currentAssetData) - g_currentAssetData = 0; + if (g_assetDataCache.localData() == this) + g_assetDataCache.setLocalData(0); } ALAsset *m_asset; @@ -119,21 +258,67 @@ public: private: QString m_assetUrl; ALAssetsLibrary *m_assetLibrary; +}; + +// ------------------------------------------------------------------------- - static QBasicMutex g_mutex; - static QPointer<QIOSAssetData> g_currentAssetData; +#ifndef QT_NO_FILESYSTEMITERATOR + +class QIOSFileEngineIteratorAssetsLibrary : public QAbstractFileEngineIterator +{ +public: + QIOSAssetEnumerator *m_enumerator; + + QIOSFileEngineIteratorAssetsLibrary( + QDir::Filters filters, const QStringList &nameFilters) + : QAbstractFileEngineIterator(filters, nameFilters) + , m_enumerator(new QIOSAssetEnumerator([[[ALAssetsLibrary alloc] init] autorelease], ALAssetsGroupAll)) + { + } + + ~QIOSFileEngineIteratorAssetsLibrary() + { + delete m_enumerator; + g_iteratorCurrentUrl.setLocalData(QString()); + } + + QString next() Q_DECL_OVERRIDE + { + // Cache the URL that we are about to return, since QDir will immediately create a + // new file engine on the file and ask if it exists. Unless we do this, we end up + // creating a new ALAsset just to verify its existence, which will be especially + // costly for assets belonging to ALAssetsGroupPhotoStream. + ALAsset *asset = m_enumerator->next(); + QString url = QUrl::fromNSURL([asset valueForProperty:ALAssetPropertyAssetURL]).toString(); + g_iteratorCurrentUrl.setLocalData(url); + return url; + } + + bool hasNext() const Q_DECL_OVERRIDE + { + return m_enumerator->hasNext(); + } + + QString currentFileName() const Q_DECL_OVERRIDE + { + return g_iteratorCurrentUrl.localData(); + } + + QFileInfo currentFileInfo() const + { + return QFileInfo(currentFileName()); + } }; -QBasicMutex QIOSAssetData::g_mutex; -QPointer<QIOSAssetData> QIOSAssetData::g_currentAssetData = 0; +#endif // ------------------------------------------------------------------------- QIOSFileEngineAssetsLibrary::QIOSFileEngineAssetsLibrary(const QString &fileName) - : m_fileName(fileName) - , m_offset(0) + : m_offset(0) , m_data(0) { + setFileName(fileName); } QIOSFileEngineAssetsLibrary::~QIOSFileEngineAssetsLibrary() @@ -143,18 +328,8 @@ QIOSFileEngineAssetsLibrary::~QIOSFileEngineAssetsLibrary() ALAsset *QIOSFileEngineAssetsLibrary::loadAsset() const { - if (!m_data) { - // QUrl::fromLocalFile() will remove double slashes. Since the asset url is passed around as a file - // name in the app (and converted to/from a file url, e.g in QFileDialog), we need to check if we still - // have two leading slashes after the scheme, and restore the second slash if not. - QString assetUrl = m_fileName; - const int index = 16; // "assets-library://" - if (assetUrl[index] != QLatin1Char('/')) - assetUrl.insert(index, '/'); - - m_data = new QIOSAssetData(assetUrl, const_cast<QIOSFileEngineAssetsLibrary *>(this)); - } - + if (!m_data) + m_data = new QIOSAssetData(m_assetUrl, const_cast<QIOSFileEngineAssetsLibrary *>(this)); return m_data->m_asset; } @@ -179,15 +354,21 @@ bool QIOSFileEngineAssetsLibrary::close() QAbstractFileEngine::FileFlags QIOSFileEngineAssetsLibrary::fileFlags(QAbstractFileEngine::FileFlags type) const { QAbstractFileEngine::FileFlags flags = 0; - if (!loadAsset()) + const bool isDir = (m_assetUrl == QLatin1String("assets-library://")); + const bool exists = isDir || m_assetUrl == g_iteratorCurrentUrl.localData() || loadAsset(); + + if (!exists) return flags; if (type & FlagsMask) flags |= ExistsFlag; - if (type & PermsMask) - flags |= ReadOwnerPerm | ReadUserPerm | ReadGroupPerm | ReadOtherPerm; + if (type & PermsMask) { + ALAuthorizationStatus status = [ALAssetsLibrary authorizationStatus]; + if (status != ALAuthorizationStatusRestricted && status != ALAuthorizationStatusDenied) + flags |= ReadOwnerPerm | ReadUserPerm | ReadGroupPerm | ReadOtherPerm; + } if (type & TypesMask) - flags |= FileType; + flags |= isDir ? DirectoryType : FileType; return flags; } @@ -245,11 +426,32 @@ void QIOSFileEngineAssetsLibrary::setFileName(const QString &file) if (m_data) close(); m_fileName = file; + // QUrl::fromLocalFile() will remove double slashes. Since the asset url is + // passed around as a file name in the app (and converted to/from a file url, e.g + // in QFileDialog), we need to ensure that m_assetUrl ends up being valid. + int index = file.indexOf(QLatin1String("asset.JPG?")); + if (index == -1) + m_assetUrl = QLatin1String("assets-library://"); + else + m_assetUrl = QLatin1String("assets-library://asset/") + file.mid(index); } QStringList QIOSFileEngineAssetsLibrary::entryList(QDir::Filters filters, const QStringList &filterNames) const { - Q_UNUSED(filters); - Q_UNUSED(filterNames); - return QStringList(); + return QAbstractFileEngine::entryList(filters, filterNames); +} + +#ifndef QT_NO_FILESYSTEMITERATOR + +QAbstractFileEngine::Iterator *QIOSFileEngineAssetsLibrary::beginEntryList( + QDir::Filters filters, const QStringList &filterNames) +{ + return new QIOSFileEngineIteratorAssetsLibrary(filters, filterNames); } + +QAbstractFileEngine::Iterator *QIOSFileEngineAssetsLibrary::endEntryList() +{ + return 0; +} + +#endif |