Qt
Internal/Contributor docs for the Qt SDK. <b>Note:</b> These are NOT official API docs; those are found <a href='https://doc.qt.io/'>here</a>.
Loading...
Searching...
No Matches
qiosfileengineassetslibrary.mm
Go to the documentation of this file.
1// Copyright (C) 2016 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3
5
6#import <UIKit/UIKit.h>
7#import <AssetsLibrary/AssetsLibrary.h>
8
9#include <QtCore/QTimer>
10#include <QtCore/private/qcoreapplication_p.h>
11#include <QtCore/qurl.h>
12#include <QtCore/qset.h>
13#include <QtCore/qthreadstorage.h>
14#include <QtCore/qfileselector.h>
15#include <QtCore/qpointer.h>
16
18
19using namespace Qt::StringLiterals;
20
21static QThreadStorage<QString> g_iteratorCurrentUrl;
22static QThreadStorage<QPointer<QIOSAssetData> > g_assetDataCache;
23
24static const int kBufferSize = 10;
25static ALAsset *kNoAsset = nullptr;
26
28{
29 if ([ALAssetsLibrary authorizationStatus] != ALAuthorizationStatusNotDetermined)
30 return true;
31
32 if (static_cast<QCoreApplicationPrivate *>(QObjectPrivate::get(qApp))->in_exec)
33 return true;
34
35 if ([NSThread isMainThread]) {
36 // The dialog is about to show, but since main has not finished, the dialog will be held
37 // back until the launch completes. This is problematic since we cannot successfully return
38 // back to the caller before the asset is ready, which also includes showing the dialog. To
39 // work around this, we create an event loop to that will complete the launch (return from the
40 // applicationDidFinishLaunching callback). But this will only work if we're on the main thread.
41 QEventLoop loop;
43 loop.exec();
44 } else {
45 NSLog(@"QIOSFileEngine: unable to show assets authorization dialog from non-gui thread before QApplication is executing.");
46 return false;
47 }
48
49 return true;
50}
51
52// -------------------------------------------------------------------------
53
55{
56public:
57 QIOSAssetEnumerator(ALAssetsLibrary *assetsLibrary, ALAssetsGroupType type)
58 : m_semWriteAsset(dispatch_semaphore_create(kBufferSize))
59 , m_semReadAsset(dispatch_semaphore_create(0))
60 , m_stop(false)
61 , m_assetsLibrary([assetsLibrary retain])
62 , m_type(type)
63 , m_buffer(QVector<ALAsset *>(kBufferSize))
64 , m_readIndex(0)
65 , m_writeIndex(0)
66 , m_nextAssetReady(false)
67 {
69 writeAsset(kNoAsset);
70 else
71 startEnumerate();
72 }
73
75 {
76 m_stop = true;
77
78 // Flush and autorelease remaining assets in the buffer
79 while (hasNext())
80 next();
81
82 // Documentation states that we need to balance out calls to 'wait'
83 // and 'signal'. Since the enumeration function always will be one 'wait'
84 // ahead, we need to signal m_semProceedToNextAsset one last time.
85 dispatch_semaphore_signal(m_semWriteAsset);
86 dispatch_release(m_semReadAsset);
87 dispatch_release(m_semWriteAsset);
88
89 [m_assetsLibrary autorelease];
90 }
91
92 bool hasNext()
93 {
94 if (!m_nextAssetReady) {
95 dispatch_semaphore_wait(m_semReadAsset, DISPATCH_TIME_FOREVER);
96 m_nextAssetReady = true;
97 }
98 return m_buffer[m_readIndex] != kNoAsset;
99 }
100
101 ALAsset *next()
102 {
103 Q_ASSERT(m_nextAssetReady);
104 Q_ASSERT(m_buffer[m_readIndex]);
105
106 ALAsset *asset = [m_buffer[m_readIndex] autorelease];
107 dispatch_semaphore_signal(m_semWriteAsset);
108
109 m_readIndex = (m_readIndex + 1) % kBufferSize;
110 m_nextAssetReady = false;
111 return asset;
112 }
113
114private:
115 dispatch_semaphore_t m_semWriteAsset;
116 dispatch_semaphore_t m_semReadAsset;
117 std::atomic_bool m_stop;
118
119 ALAssetsLibrary *m_assetsLibrary;
120 ALAssetsGroupType m_type;
121 QVector<ALAsset *> m_buffer;
122 int m_readIndex;
123 int m_writeIndex;
124 bool m_nextAssetReady;
125
126 void writeAsset(ALAsset *asset)
127 {
128 dispatch_semaphore_wait(m_semWriteAsset, DISPATCH_TIME_FOREVER);
129 m_buffer[m_writeIndex] = [asset retain];
130 dispatch_semaphore_signal(m_semReadAsset);
131 m_writeIndex = (m_writeIndex + 1) % kBufferSize;
132 }
133
134 void startEnumerate()
135 {
136 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
137 [m_assetsLibrary enumerateGroupsWithTypes:m_type usingBlock:^(ALAssetsGroup *group, BOOL *stopEnumerate) {
138
139 if (!group) {
140 writeAsset(kNoAsset);
141 return;
142 }
143
144 if (m_stop) {
145 *stopEnumerate = true;
146 return;
147 }
148
149 [group enumerateAssetsUsingBlock:^(ALAsset *asset, NSUInteger index, BOOL *stopEnumerate) {
151 if (!asset || ![[asset valueForProperty:ALAssetPropertyType] isEqual:ALAssetTypePhoto])
152 return;
153
154 writeAsset(asset);
155 *stopEnumerate = m_stop;
156 }];
157 } failureBlock:^(NSError *error) {
158 NSLog(@"QIOSFileEngine: %@", error);
159 writeAsset(kNoAsset);
160 }];
161 });
162 }
163
164};
165
166// -------------------------------------------------------------------------
167
168class QIOSAssetData : public QObject
169{
170public:
172 : m_asset(0)
173 , m_assetUrl(assetUrl)
174 , m_assetLibrary(0)
175 {
177 return;
178
179 if (QIOSAssetData *assetData = g_assetDataCache.localData()) {
180 // It's a common pattern that QFiles pointing to the same path are created and destroyed
181 // several times during a single event loop cycle. To avoid loading the same asset
182 // over and over, we check if the last loaded asset has not been destroyed yet, and try to
183 // reuse its data.
184 if (assetData->m_assetUrl == assetUrl) {
185 m_assetLibrary = [assetData->m_assetLibrary retain];
186 m_asset = [assetData->m_asset retain];
187 return;
188 }
189 }
190
191 // We can only load images from the asset library async. And this might take time, since it
192 // involves showing the authorization dialog. But the QFile API is synchronuous, so we need to
193 // wait until we have access to the data. [ALAssetLibrary assetForUrl:] will schedule a block on
194 // the current thread. But instead of spinning the event loop to force the block to execute, we
195 // wrap the call inside a synchronuous dispatch queue so that it executes on another thread.
196 dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
197
198 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
199 NSURL *url = [NSURL URLWithString:assetUrl.toNSString()];
200 m_assetLibrary = [[ALAssetsLibrary alloc] init];
201 [m_assetLibrary assetForURL:url resultBlock:^(ALAsset *asset) {
202
203 if (!asset) {
204 // When an asset couldn't be loaded, chances are that it belongs to ALAssetsGroupPhotoStream.
205 // Such assets can be stored in the cloud and might need to be downloaded first. Unfortunately,
206 // forcing that to happen is hidden behind private APIs ([ALAsset requestDefaultRepresentation]).
207 // As a work-around, we search for it instead, since that will give us a pointer to the asset.
208 QIOSAssetEnumerator e(m_assetLibrary, ALAssetsGroupPhotoStream);
209 while (e.hasNext()) {
210 ALAsset *a = e.next();
211 QString url = QUrl::fromNSURL([a valueForProperty:ALAssetPropertyAssetURL]).toString();
212 if (url == assetUrl) {
213 asset = a;
214 break;
215 }
216 }
217 }
218
219 if (!asset)
220 engine->setError(QFile::OpenError, "could not open image"_L1);
221
222 m_asset = [asset retain];
223 dispatch_semaphore_signal(semaphore);
224 } failureBlock:^(NSError *error) {
225 engine->setError(QFile::OpenError, QString::fromNSString(error.localizedDescription));
226 dispatch_semaphore_signal(semaphore);
227 }];
228 });
229
230 dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
231 dispatch_release(semaphore);
232
233 g_assetDataCache.setLocalData(this);
234 }
235
237 {
238 [m_assetLibrary release];
240 if (g_assetDataCache.localData() == this)
241 g_assetDataCache.setLocalData(0);
242 }
243
244 ALAsset *m_asset;
245
246private:
247 QString m_assetUrl;
248 ALAssetsLibrary *m_assetLibrary;
249};
250
251// -------------------------------------------------------------------------
252
253#ifndef QT_NO_FILESYSTEMITERATOR
254
256{
257public:
259
261 const QString &path, QDir::Filters filters, const QStringList &nameFilters)
263 , m_enumerator(new QIOSAssetEnumerator([[[ALAssetsLibrary alloc] init] autorelease], ALAssetsGroupAll))
264 {
265 }
266
268 {
269 delete m_enumerator;
270 g_iteratorCurrentUrl.setLocalData(QString());
271 }
272
273 bool advance() override
274 {
275 if (!m_enumerator->hasNext())
276 return false;
277
278 // Cache the URL that we are about to return, since QDir will immediately create a
279 // new file engine on the file and ask if it exists. Unless we do this, we end up
280 // creating a new ALAsset just to verify its existence, which will be especially
281 // costly for assets belonging to ALAssetsGroupPhotoStream.
282 ALAsset *asset = m_enumerator->next();
283 QString url = QUrl::fromNSURL([asset valueForProperty:ALAssetPropertyAssetURL]).toString();
284 g_iteratorCurrentUrl.setLocalData(url);
285 return true;
286 }
287
288 QString currentFileName() const override
289 {
290 return g_iteratorCurrentUrl.localData();
291 }
292
293 QFileInfo currentFileInfo() const override
294 {
295 return QFileInfo(currentFileName());
296 }
297};
298
299#endif
300
301// -------------------------------------------------------------------------
302
309
314
315ALAsset *QIOSFileEngineAssetsLibrary::loadAsset() const
316{
317 if (!m_data)
318 m_data = new QIOSAssetData(m_assetUrl, const_cast<QIOSFileEngineAssetsLibrary *>(this));
319 return m_data->m_asset;
320}
321
322bool QIOSFileEngineAssetsLibrary::open(QIODevice::OpenMode openMode,
323 std::optional<QFile::Permissions> permissions)
324{
325 Q_UNUSED(permissions);
326
327 if (openMode & (QIODevice::WriteOnly | QIODevice::Text))
328 return false;
329 return loadAsset();
330}
331
333{
334 if (m_data) {
335 // Delete later, so that we can reuse the asset if a QFile is
336 // opened with the same path during the same event loop cycle.
337 m_data->deleteLater();
338 m_data = nullptr;
339 }
340 return true;
341}
342
343QAbstractFileEngine::FileFlags QIOSFileEngineAssetsLibrary::fileFlags(QAbstractFileEngine::FileFlags type) const
344{
345 QAbstractFileEngine::FileFlags flags;
346 const bool isDir = (m_assetUrl == "assets-library://"_L1);
347 if (!isDir) {
348 static const QFileSelector fileSelector;
349 static const auto selectors = fileSelector.allSelectors();
350 if (m_assetUrl.startsWith("assets-library://"_L1)) {
351 for (const auto &selector : selectors) {
352 if (m_assetUrl.endsWith(selector))
353 return flags;
354 }
355 }
356 }
357
358 const bool exists = isDir || m_assetUrl == g_iteratorCurrentUrl.localData() || loadAsset();
359
360 if (!exists)
361 return flags;
362
363 if (type & FlagsMask)
364 flags |= ExistsFlag;
365 if (type & PermsMask) {
366 ALAuthorizationStatus status = [ALAssetsLibrary authorizationStatus];
367 if (status != ALAuthorizationStatusRestricted && status != ALAuthorizationStatusDenied)
369 }
370 if (type & TypesMask)
371 flags |= isDir ? DirectoryType : FileType;
372
373 return flags;
374}
375
377{
378 if (ALAsset *asset = loadAsset())
379 return [[asset defaultRepresentation] size];
380 return 0;
381}
382
384{
385 ALAsset *asset = loadAsset();
386 if (!asset)
387 return -1;
388
389 qint64 bytesRead = qMin(maxlen, size() - m_offset);
390 if (!bytesRead)
391 return 0;
392
393 NSError *error = nullptr;
394 [[asset defaultRepresentation] getBytes:(uint8_t *)data fromOffset:m_offset length:bytesRead error:&error];
395
396 if (error) {
397 setError(QFile::ReadError, QString::fromNSString(error.localizedDescription));
398 return -1;
399 }
400
401 m_offset += bytesRead;
402 return bytesRead;
403}
404
406{
407 return m_offset;
408}
409
411{
412 if (pos >= size())
413 return false;
414 m_offset = pos;
415 return true;
416}
417
419{
420 Q_UNUSED(file);
421 return m_fileName;
422}
423
425{
426 if (m_data)
427 close();
428 m_fileName = file;
429 // QUrl::fromLocalFile() will remove double slashes. Since the asset url is
430 // passed around as a file name in the app (and converted to/from a file url, e.g
431 // in QFileDialog), we need to ensure that m_assetUrl ends up being valid.
432 qsizetype index = file.indexOf("/asset"_L1);
433 if (index == -1)
434 m_assetUrl = "assets-library://"_L1;
435 else
436 m_assetUrl = "assets-library:/"_L1 + file.mid(index);
437}
438
439#ifndef QT_NO_FILESYSTEMITERATOR
440
443 const QString &path, QDir::Filters filters, const QStringList &filterNames)
444{
445 return std::make_unique<QIOSFileEngineIteratorAssetsLibrary>(path, filters, filterNames);
446}
447
449
450#endif
static bool isEqual(const aiUVTransform &a, const aiUVTransform &b)
NSData * m_data
The QAbstractFileEngineIterator class provides an iterator interface for custom file engines.
QDir::Filters filters() const
Returns the entry filters for this iterator.
virtual QString currentFileName() const =0
This pure virtual function returns the name of the current directory entry, excluding the path.
QStringList nameFilters() const
Returns the name filters for this iterator.
virtual QFileInfo currentFileInfo() const
The virtual function returns a QFileInfo for the current directory entry.
virtual bool advance()=0
This pure virtual function advances the iterator to the next directory entry; if the operation was su...
std::unique_ptr< Iterator > IteratorUniquePtr
FileName
These values are used to request a file name in a particular format.
QFile::FileError error() const
Returns the QFile::FileError that resulted from the last failed operation.
\inmodule QtCore
Definition qeventloop.h:16
int exec(ProcessEventsFlags flags=AllEvents)
Enters the main event loop and waits until exit() is called.
void quit()
Tells the event loop to exit normally.
\inmodule QtCore
QStringList allSelectors() const
Returns the complete, ordered list of selectors used by this instance.
QIOSAssetData(const QString &assetUrl, QIOSFileEngineAssetsLibrary *engine)
QIOSAssetEnumerator(ALAssetsLibrary *assetsLibrary, ALAssetsGroupType type)
QString fileName(FileName file) const override
Return the file engine's current file name in the format specified by file.
void setError(QFile::FileError error, const QString &str)
bool open(QIODevice::OpenMode openMode, std::optional< QFile::Permissions > permissions) override
Opens the file in the specified mode.
qint64 pos() const override
Returns the current file position.
void setFileName(const QString &file) override
Sets the file engine's file name to file.
IteratorUniquePtr beginEntryList(const QString &path, QDir::Filters filters, const QStringList &filterNames) override
Returns a QAbstractFileEngine::IteratorUniquePtr, that can be used to iterate over the entries in pat...
bool seek(qint64 pos) override
Sets the file position to the given offset.
FileFlags fileFlags(FileFlags type) const override
This function should return the set of OR'd flags that are true for the file engine's file,...
qint64 read(char *data, qint64 maxlen) override
Reads a number of characters from the file into data.
qint64 size() const override
Returns the size of the file.
QIOSFileEngineAssetsLibrary(const QString &fileName)
bool close() override
Closes the file, returning true if successful; otherwise returns false.
Definition qlist.h:75
static QObjectPrivate * get(QObject *o)
Definition qobject_p.h:150
\inmodule QtCore
Definition qobject.h:103
void deleteLater()
\threadsafe
Definition qobject.cpp:2435
\inmodule QtCore
\macro QT_RESTRICTED_CAST_FROM_ASCII
Definition qstring.h:129
bool startsWith(const QString &s, Qt::CaseSensitivity cs=Qt::CaseSensitive) const
Returns true if the string starts with s; otherwise returns false.
Definition qstring.cpp:5455
bool endsWith(const QString &s, Qt::CaseSensitivity cs=Qt::CaseSensitive) const
Returns true if the string ends with s; otherwise returns false.
Definition qstring.cpp:5506
bool singleShot
whether the timer is a single-shot timer
Definition qtimer.h:22
Combined button and popup list for selecting options.
unsigned long NSUInteger
#define qApp
DBusConnection const char DBusError * error
static bool ensureAuthorizationDialogNotBlocked()
static const int kBufferSize
static QThreadStorage< QString > g_iteratorCurrentUrl
static QThreadStorage< QPointer< QIOSAssetData > > g_assetDataCache
static ALAsset * kNoAsset
constexpr const T & qMin(const T &a, const T &b)
Definition qminmax.h:40
GLboolean GLboolean GLboolean GLboolean a
[7]
GLuint index
[2]
GLenum GLuint GLenum GLsizei length
GLint GLsizei GLsizei GLenum GLenum GLsizei void * data
GLenum type
GLboolean GLuint group
GLbitfield flags
GLsizei const GLchar *const * path
#define Q_ASSERT(cond)
Definition qrandom.cpp:47
static QT_BEGIN_NAMESPACE void init(QTextBoundaryFinder::BoundaryType type, QStringView str, QCharAttributes *attributes)
#define Q_UNUSED(x)
ptrdiff_t qsizetype
Definition qtypes.h:165
long long qint64
Definition qtypes.h:60
QFile file
[0]
QFileSelector selector
[1]
QUrl url("example.com")
[constructor-url-reference]
sem release()
const QStringList filters({"Image files (*.png *.xpm *.jpg)", "Text files (*.txt)", "Any files (*)" })
[6]
char * toString(const MyType &t)
[31]
QJSEngine engine
[0]