Qt 6.x
The Qt SDK
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
17
18using namespace Qt::StringLiterals;
19
22
23static const int kBufferSize = 10;
24static ALAsset *kNoAsset = nullptr;
25
27{
28 if ([ALAssetsLibrary authorizationStatus] != ALAuthorizationStatusNotDetermined)
29 return true;
30
31 if (static_cast<QCoreApplicationPrivate *>(QObjectPrivate::get(qApp))->in_exec)
32 return true;
33
34 if ([NSThread isMainThread]) {
35 // The dialog is about to show, but since main has not finished, the dialog will be held
36 // back until the launch completes. This is problematic since we cannot successfully return
37 // back to the caller before the asset is ready, which also includes showing the dialog. To
38 // work around this, we create an event loop to that will complete the launch (return from the
39 // applicationDidFinishLaunching callback). But this will only work if we're on the main thread.
40 QEventLoop loop;
42 loop.exec();
43 } else {
44 NSLog(@"QIOSFileEngine: unable to show assets authorization dialog from non-gui thread before QApplication is executing.");
45 return false;
46 }
47
48 return true;
49}
50
51// -------------------------------------------------------------------------
52
54{
55public:
56 QIOSAssetEnumerator(ALAssetsLibrary *assetsLibrary, ALAssetsGroupType type)
57 : m_semWriteAsset(dispatch_semaphore_create(kBufferSize))
58 , m_semReadAsset(dispatch_semaphore_create(0))
59 , m_stop(false)
60 , m_assetsLibrary([assetsLibrary retain])
61 , m_type(type)
62 , m_buffer(QVector<ALAsset *>(kBufferSize))
63 , m_readIndex(0)
64 , m_writeIndex(0)
65 , m_nextAssetReady(false)
66 {
68 writeAsset(kNoAsset);
69 else
70 startEnumerate();
71 }
72
74 {
75 m_stop = true;
76
77 // Flush and autorelease remaining assets in the buffer
78 while (hasNext())
79 next();
80
81 // Documentation states that we need to balance out calls to 'wait'
82 // and 'signal'. Since the enumeration function always will be one 'wait'
83 // ahead, we need to signal m_semProceedToNextAsset one last time.
84 dispatch_semaphore_signal(m_semWriteAsset);
85 dispatch_release(m_semReadAsset);
86 dispatch_release(m_semWriteAsset);
87
88 [m_assetsLibrary autorelease];
89 }
90
91 bool hasNext()
92 {
93 if (!m_nextAssetReady) {
94 dispatch_semaphore_wait(m_semReadAsset, DISPATCH_TIME_FOREVER);
95 m_nextAssetReady = true;
96 }
97 return m_buffer[m_readIndex] != kNoAsset;
98 }
99
100 ALAsset *next()
101 {
102 Q_ASSERT(m_nextAssetReady);
103 Q_ASSERT(m_buffer[m_readIndex]);
104
105 ALAsset *asset = [m_buffer[m_readIndex] autorelease];
106 dispatch_semaphore_signal(m_semWriteAsset);
107
108 m_readIndex = (m_readIndex + 1) % kBufferSize;
109 m_nextAssetReady = false;
110 return asset;
111 }
112
113private:
114 dispatch_semaphore_t m_semWriteAsset;
115 dispatch_semaphore_t m_semReadAsset;
116 std::atomic_bool m_stop;
117
118 ALAssetsLibrary *m_assetsLibrary;
119 ALAssetsGroupType m_type;
120 QVector<ALAsset *> m_buffer;
121 int m_readIndex;
122 int m_writeIndex;
123 bool m_nextAssetReady;
124
125 void writeAsset(ALAsset *asset)
126 {
127 dispatch_semaphore_wait(m_semWriteAsset, DISPATCH_TIME_FOREVER);
128 m_buffer[m_writeIndex] = [asset retain];
129 dispatch_semaphore_signal(m_semReadAsset);
130 m_writeIndex = (m_writeIndex + 1) % kBufferSize;
131 }
132
133 void startEnumerate()
134 {
135 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
136 [m_assetsLibrary enumerateGroupsWithTypes:m_type usingBlock:^(ALAssetsGroup *group, BOOL *stopEnumerate) {
137
138 if (!group) {
139 writeAsset(kNoAsset);
140 return;
141 }
142
143 if (m_stop) {
144 *stopEnumerate = true;
145 return;
146 }
147
148 [group enumerateAssetsUsingBlock:^(ALAsset *asset, NSUInteger index, BOOL *stopEnumerate) {
150 if (!asset || ![[asset valueForProperty:ALAssetPropertyType] isEqual:ALAssetTypePhoto])
151 return;
152
153 writeAsset(asset);
154 *stopEnumerate = m_stop;
155 }];
156 } failureBlock:^(NSError *error) {
157 NSLog(@"QIOSFileEngine: %@", error);
158 writeAsset(kNoAsset);
159 }];
160 });
161 }
162
163};
164
165// -------------------------------------------------------------------------
166
167class QIOSAssetData : public QObject
168{
169public:
171 : m_asset(0)
172 , m_assetUrl(assetUrl)
173 , m_assetLibrary(0)
174 {
176 return;
177
178 if (QIOSAssetData *assetData = g_assetDataCache.localData()) {
179 // It's a common pattern that QFiles pointing to the same path are created and destroyed
180 // several times during a single event loop cycle. To avoid loading the same asset
181 // over and over, we check if the last loaded asset has not been destroyed yet, and try to
182 // reuse its data.
183 if (assetData->m_assetUrl == assetUrl) {
184 m_assetLibrary = [assetData->m_assetLibrary retain];
185 m_asset = [assetData->m_asset retain];
186 return;
187 }
188 }
189
190 // We can only load images from the asset library async. And this might take time, since it
191 // involves showing the authorization dialog. But the QFile API is synchronuous, so we need to
192 // wait until we have access to the data. [ALAssetLibrary assetForUrl:] will schedule a block on
193 // the current thread. But instead of spinning the event loop to force the block to execute, we
194 // wrap the call inside a synchronuous dispatch queue so that it executes on another thread.
195 dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
196
197 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
198 NSURL *url = [NSURL URLWithString:assetUrl.toNSString()];
199 m_assetLibrary = [[ALAssetsLibrary alloc] init];
200 [m_assetLibrary assetForURL:url resultBlock:^(ALAsset *asset) {
201
202 if (!asset) {
203 // When an asset couldn't be loaded, chances are that it belongs to ALAssetsGroupPhotoStream.
204 // Such assets can be stored in the cloud and might need to be downloaded first. Unfortunately,
205 // forcing that to happen is hidden behind private APIs ([ALAsset requestDefaultRepresentation]).
206 // As a work-around, we search for it instead, since that will give us a pointer to the asset.
207 QIOSAssetEnumerator e(m_assetLibrary, ALAssetsGroupPhotoStream);
208 while (e.hasNext()) {
209 ALAsset *a = e.next();
210 QString url = QUrl::fromNSURL([a valueForProperty:ALAssetPropertyAssetURL]).toString();
211 if (url == assetUrl) {
212 asset = a;
213 break;
214 }
215 }
216 }
217
218 if (!asset)
219 engine->setError(QFile::OpenError, "could not open image"_L1);
220
221 m_asset = [asset retain];
222 dispatch_semaphore_signal(semaphore);
223 } failureBlock:^(NSError *error) {
224 engine->setError(QFile::OpenError, QString::fromNSString(error.localizedDescription));
225 dispatch_semaphore_signal(semaphore);
226 }];
227 });
228
229 dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
230 dispatch_release(semaphore);
231
232 g_assetDataCache.setLocalData(this);
233 }
234
236 {
237 [m_assetLibrary release];
239 if (g_assetDataCache.localData() == this)
240 g_assetDataCache.setLocalData(0);
241 }
242
243 ALAsset *m_asset;
244
245private:
246 QString m_assetUrl;
247 ALAssetsLibrary *m_assetLibrary;
248};
249
250// -------------------------------------------------------------------------
251
252#ifndef QT_NO_FILESYSTEMITERATOR
253
255{
256public:
258
260 QDir::Filters filters, const QStringList &nameFilters)
262 , m_enumerator(new QIOSAssetEnumerator([[[ALAssetsLibrary alloc] init] autorelease], ALAssetsGroupAll))
263 {
264 }
265
267 {
268 delete m_enumerator;
269 g_iteratorCurrentUrl.setLocalData(QString());
270 }
271
272 QString next() override
273 {
274 // Cache the URL that we are about to return, since QDir will immediately create a
275 // new file engine on the file and ask if it exists. Unless we do this, we end up
276 // creating a new ALAsset just to verify its existence, which will be especially
277 // costly for assets belonging to ALAssetsGroupPhotoStream.
278 ALAsset *asset = m_enumerator->next();
279 QString url = QUrl::fromNSURL([asset valueForProperty:ALAssetPropertyAssetURL]).toString();
280 g_iteratorCurrentUrl.setLocalData(url);
281 return url;
282 }
283
284 bool hasNext() const override
285 {
286 return m_enumerator->hasNext();
287 }
288
289 QString currentFileName() const override
290 {
291 return g_iteratorCurrentUrl.localData();
292 }
293
294 QFileInfo currentFileInfo() const override
295 {
296 return QFileInfo(currentFileName());
297 }
298};
299
300#endif
301
302// -------------------------------------------------------------------------
303
305 : m_offset(0)
306 , m_data(0)
307{
309}
310
312{
313 close();
314}
315
316ALAsset *QIOSFileEngineAssetsLibrary::loadAsset() const
317{
318 if (!m_data)
319 m_data = new QIOSAssetData(m_assetUrl, const_cast<QIOSFileEngineAssetsLibrary *>(this));
320 return m_data->m_asset;
321}
322
323bool QIOSFileEngineAssetsLibrary::open(QIODevice::OpenMode openMode,
324 std::optional<QFile::Permissions> permissions)
325{
326 Q_UNUSED(permissions);
327
328 if (openMode & (QIODevice::WriteOnly | QIODevice::Text))
329 return false;
330 return loadAsset();
331}
332
334{
335 if (m_data) {
336 // Delete later, so that we can reuse the asset if a QFile is
337 // opened with the same path during the same event loop cycle.
338 m_data->deleteLater();
339 m_data = nullptr;
340 }
341 return true;
342}
343
344QAbstractFileEngine::FileFlags QIOSFileEngineAssetsLibrary::fileFlags(QAbstractFileEngine::FileFlags type) const
345{
346 QAbstractFileEngine::FileFlags flags;
347 const bool isDir = (m_assetUrl == "assets-library://"_L1);
348 if (!isDir) {
349 static const QFileSelector fileSelector;
350 static const auto selectors = fileSelector.allSelectors();
351 if (m_assetUrl.startsWith("assets-library://"_L1)) {
352 for (const auto &selector : selectors) {
353 if (m_assetUrl.endsWith(selector))
354 return flags;
355 }
356 }
357 }
358
359 const bool exists = isDir || m_assetUrl == g_iteratorCurrentUrl.localData() || loadAsset();
360
361 if (!exists)
362 return flags;
363
364 if (type & FlagsMask)
365 flags |= ExistsFlag;
366 if (type & PermsMask) {
367 ALAuthorizationStatus status = [ALAssetsLibrary authorizationStatus];
368 if (status != ALAuthorizationStatusRestricted && status != ALAuthorizationStatusDenied)
370 }
371 if (type & TypesMask)
372 flags |= isDir ? DirectoryType : FileType;
373
374 return flags;
375}
376
378{
379 if (ALAsset *asset = loadAsset())
380 return [[asset defaultRepresentation] size];
381 return 0;
382}
383
385{
386 ALAsset *asset = loadAsset();
387 if (!asset)
388 return -1;
389
390 qint64 bytesRead = qMin(maxlen, size() - m_offset);
391 if (!bytesRead)
392 return 0;
393
394 NSError *error = nullptr;
395 [[asset defaultRepresentation] getBytes:(uint8_t *)data fromOffset:m_offset length:bytesRead error:&error];
396
397 if (error) {
398 setError(QFile::ReadError, QString::fromNSString(error.localizedDescription));
399 return -1;
400 }
401
402 m_offset += bytesRead;
403 return bytesRead;
404}
405
407{
408 return m_offset;
409}
410
412{
413 if (pos >= size())
414 return false;
415 m_offset = pos;
416 return true;
417}
418
420{
421 Q_UNUSED(file);
422 return m_fileName;
423}
424
426{
427 if (m_data)
428 close();
429 m_fileName = file;
430 // QUrl::fromLocalFile() will remove double slashes. Since the asset url is
431 // passed around as a file name in the app (and converted to/from a file url, e.g
432 // in QFileDialog), we need to ensure that m_assetUrl ends up being valid.
433 qsizetype index = file.indexOf("/asset"_L1);
434 if (index == -1)
435 m_assetUrl = "assets-library://"_L1;
436 else
437 m_assetUrl = "assets-library:/"_L1 + file.mid(index);
438}
439
440#ifndef QT_NO_FILESYSTEMITERATOR
441
443 QDir::Filters filters, const QStringList &filterNames)
444{
445 return new QIOSFileEngineIteratorAssetsLibrary(filters, filterNames);
446}
447
449{
450 return 0;
451}
452
454
455#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 next()=0
This pure virtual function advances the iterator to the next directory entry, and returns the file pa...
virtual bool hasNext() const =0
This pure virtual function returns true if there is at least one more entry in the current directory ...
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.
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 \reentrant
Definition qfileinfo.h:22
\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.
Iterator * beginEntryList(QDir::Filters filters, const QStringList &filterNames) override
Returns an instance of a QAbstractFileEngineIterator using filters for entry filtering and filterName...
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:74
static QObjectPrivate * get(QObject *o)
Definition qobject_p.h:153
\inmodule QtCore
Definition qobject.h:90
void deleteLater()
\threadsafe
Definition qobject.cpp:2352
\inmodule QtCore
\macro QT_RESTRICTED_CAST_FROM_ASCII
Definition qstring.h:127
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:5299
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:5350
\inmodule QtCore
bool singleShot
whether the timer is a single-shot timer
Definition qtimer.h:22
double e
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
GLenum type
GLboolean GLuint group
GLbitfield flags
GLint GLsizei GLsizei GLenum GLenum GLsizei void * data
#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:70
long long qint64
Definition qtypes.h:55
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]