Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
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// Qt-Security score:significant reason:default
4
6
7#import <UIKit/UIKit.h>
8#import <AssetsLibrary/AssetsLibrary.h>
9
10#include <QtCore/QTimer>
11#include <QtCore/private/qcoreapplication_p.h>
12#include <QtCore/qurl.h>
13#include <QtCore/qset.h>
14#include <QtCore/qthreadstorage.h>
15#include <QtCore/qfileselector.h>
16#include <QtCore/qpointer.h>
17
19
20using namespace Qt::StringLiterals;
21
24
25static const int kBufferSize = 10;
26static ALAsset *kNoAsset = nullptr;
27
29{
30 if ([ALAssetsLibrary authorizationStatus] != ALAuthorizationStatusNotDetermined)
31 return true;
32
33 if (static_cast<QCoreApplicationPrivate *>(QObjectPrivate::get(qApp))->in_exec)
34 return true;
35
36 if ([NSThread isMainThread]) {
37 // The dialog is about to show, but since main has not finished, the dialog will be held
38 // back until the launch completes. This is problematic since we cannot successfully return
39 // back to the caller before the asset is ready, which also includes showing the dialog. To
40 // work around this, we create an event loop to that will complete the launch (return from the
41 // applicationDidFinishLaunching callback). But this will only work if we're on the main thread.
42 QEventLoop loop;
43 QTimer::singleShot(1, &loop, &QEventLoop::quit);
44 loop.exec();
45 } else {
46 NSLog(@"QIOSFileEngine: unable to show assets authorization dialog from non-gui thread before QApplication is executing.");
47 return false;
48 }
49
50 return true;
51}
52
53// -------------------------------------------------------------------------
54
56{
57public:
74
76 {
77 m_stop = true;
78
79 // Flush and autorelease remaining assets in the buffer
80 while (hasNext())
81 next();
82
83 // Documentation states that we need to balance out calls to 'wait'
84 // and 'signal'. Since the enumeration function always will be one 'wait'
85 // ahead, we need to signal m_semProceedToNextAsset one last time.
86 dispatch_semaphore_signal(m_semWriteAsset);
87 dispatch_release(m_semReadAsset);
88 dispatch_release(m_semWriteAsset);
89
90 [m_assetsLibrary autorelease];
91 }
92
93 bool hasNext()
94 {
95 if (!m_nextAssetReady) {
96 dispatch_semaphore_wait(m_semReadAsset, DISPATCH_TIME_FOREVER);
97 m_nextAssetReady = true;
98 }
99 return m_buffer[m_readIndex] != kNoAsset;
100 }
101
103 {
104 Q_ASSERT(m_nextAssetReady);
105 Q_ASSERT(m_buffer[m_readIndex]);
106
107 ALAsset *asset = [m_buffer[m_readIndex] autorelease];
108 dispatch_semaphore_signal(m_semWriteAsset);
109
110 m_readIndex = (m_readIndex + 1) % kBufferSize;
111 m_nextAssetReady = false;
112 return asset;
113 }
114
115private:
116 dispatch_semaphore_t m_semWriteAsset;
117 dispatch_semaphore_t m_semReadAsset;
118 std::atomic_bool m_stop;
119
120 ALAssetsLibrary *m_assetsLibrary;
121 ALAssetsGroupType m_type;
122 QVector<ALAsset *> m_buffer;
123 int m_readIndex;
124 int m_writeIndex;
125 bool m_nextAssetReady;
126
127 void writeAsset(ALAsset *asset)
128 {
129 dispatch_semaphore_wait(m_semWriteAsset, DISPATCH_TIME_FOREVER);
130 m_buffer[m_writeIndex] = [asset retain];
131 dispatch_semaphore_signal(m_semReadAsset);
132 m_writeIndex = (m_writeIndex + 1) % kBufferSize;
133 }
134
135 void startEnumerate()
136 {
137 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
138 [m_assetsLibrary enumerateGroupsWithTypes:m_type usingBlock:^(ALAssetsGroup *group, BOOL *stopEnumerate) {
139
140 if (!group) {
141 writeAsset(kNoAsset);
142 return;
143 }
144
145 if (m_stop) {
146 *stopEnumerate = true;
147 return;
148 }
149
150 [group enumerateAssetsUsingBlock:^(ALAsset *asset, NSUInteger index, BOOL *stopEnumerate) {
151 Q_UNUSED(index);
152 if (!asset || ![[asset valueForProperty:ALAssetPropertyType] isEqual:ALAssetTypePhoto])
153 return;
154
155 writeAsset(asset);
156 *stopEnumerate = m_stop;
157 }];
158 } failureBlock:^(NSError *error) {
159 NSLog(@"QIOSFileEngine: %@", error);
160 writeAsset(kNoAsset);
161 }];
162 });
163 }
164
165};
166
167// -------------------------------------------------------------------------
168
169class QIOSAssetData : public QObject
170{
171public:
172 QIOSAssetData(const QString &assetUrl, QIOSFileEngineAssetsLibrary *engine)
173 : m_asset(0)
175 , m_assetLibrary(0)
176 {
178 return;
179
180 if (QIOSAssetData *assetData = g_assetDataCache.localData()) {
181 // It's a common pattern that QFiles pointing to the same path are created and destroyed
182 // several times during a single event loop cycle. To avoid loading the same asset
183 // over and over, we check if the last loaded asset has not been destroyed yet, and try to
184 // reuse its data.
185 if (assetData->m_assetUrl == assetUrl) {
186 m_assetLibrary = [assetData->m_assetLibrary retain];
187 m_asset = [assetData->m_asset retain];
188 return;
189 }
190 }
191
192 // We can only load images from the asset library async. And this might take time, since it
193 // involves showing the authorization dialog. But the QFile API is synchronuous, so we need to
194 // wait until we have access to the data. [ALAssetLibrary assetForUrl:] will schedule a block on
195 // the current thread. But instead of spinning the event loop to force the block to execute, we
196 // wrap the call inside a synchronuous dispatch queue so that it executes on another thread.
197 dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
198
199 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
200 NSURL *url = [NSURL URLWithString:assetUrl.toNSString()];
201 m_assetLibrary = [[ALAssetsLibrary alloc] init];
202 [m_assetLibrary assetForURL:url resultBlock:^(ALAsset *asset) {
203
204 if (!asset) {
205 // When an asset couldn't be loaded, chances are that it belongs to ALAssetsGroupPhotoStream.
206 // Such assets can be stored in the cloud and might need to be downloaded first. Unfortunately,
207 // forcing that to happen is hidden behind private APIs ([ALAsset requestDefaultRepresentation]).
208 // As a work-around, we search for it instead, since that will give us a pointer to the asset.
209 QIOSAssetEnumerator e(m_assetLibrary, ALAssetsGroupPhotoStream);
210 while (e.hasNext()) {
211 ALAsset *a = e.next();
212 QString url = QUrl::fromNSURL([a valueForProperty:ALAssetPropertyAssetURL]).toString();
213 if (url == assetUrl) {
214 asset = a;
215 break;
216 }
217 }
218 }
219
220 if (!asset)
221 engine->setError(QFile::OpenError, "could not open image"_L1);
222
223 m_asset = [asset retain];
224 dispatch_semaphore_signal(semaphore);
225 } failureBlock:^(NSError *error) {
226 engine->setError(QFile::OpenError, QString::fromNSString(error.localizedDescription));
227 dispatch_semaphore_signal(semaphore);
228 }];
229 });
230
231 dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
232 dispatch_release(semaphore);
233
234 g_assetDataCache.setLocalData(this);
235 }
236
238 {
239 [m_assetLibrary release];
240 [m_asset release];
241 if (g_assetDataCache.localData() == this)
242 g_assetDataCache.setLocalData(0);
243 }
244
246
247private:
248 QString m_assetUrl;
249 ALAssetsLibrary *m_assetLibrary;
250};
251
252// -------------------------------------------------------------------------
253
254#ifndef QT_NO_FILESYSTEMITERATOR
255
257{
258public:
260
261 QIOSFileEngineIteratorAssetsLibrary(
262 const QString &path, QDirListing::IteratorFlags filters, const QStringList &nameFilters)
265 {
266 }
267
268 ~QIOSFileEngineIteratorAssetsLibrary()
269 {
270 delete m_enumerator;
271 g_iteratorCurrentUrl.setLocalData(QString());
272 }
273
274 bool advance() override
275 {
277 return false;
278
279 // Cache the URL that we are about to return, since QDir will immediately create a
280 // new file engine on the file and ask if it exists. Unless we do this, we end up
281 // creating a new ALAsset just to verify its existence, which will be especially
282 // costly for assets belonging to ALAssetsGroupPhotoStream.
283 ALAsset *asset = m_enumerator->next();
284 QString url = QUrl::fromNSURL([asset valueForProperty:ALAssetPropertyAssetURL]).toString();
285 g_iteratorCurrentUrl.setLocalData(url);
286 return true;
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{
308 setFileName(fileName);
309}
310
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)
369 flags |= ReadOwnerPerm | ReadUserPerm | ReadGroupPerm | ReadOtherPerm;
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
384qint64 QIOSFileEngineAssetsLibrary::read(char *data, qint64 maxlen)
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
425void QIOSFileEngineAssetsLibrary::setFileName(const QString &file)
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
444 const QString &path, QDirListing::IteratorFlags filters, const QStringList &filterNames)
445{
446 return std::make_unique<QIOSFileEngineIteratorAssetsLibrary>(path, filters, filterNames);
447}
448
449QT_END_NAMESPACE
450
451#endif
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.
qint64 pos() const override
Returns the current file position.
void setFileName(const QString &file) override
Sets the file engine's file name to file.
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)
IteratorUniquePtr beginEntryList(const QString &path, QDirListing::IteratorFlags filters, const QStringList &filterNames) override
Returns a QAbstractFileEngine::IteratorUniquePtr, that can be used to iterate over the entries in pat...
bool close() override
Closes the file, returning true if successful; otherwise returns false.
static const int kBufferSize
static QThreadStorage< QPointer< QIOSAssetData > > g_assetDataCache
static bool ensureAuthorizationDialogNotBlocked()
static ALAsset * kNoAsset
static QThreadStorage< QString > g_iteratorCurrentUrl