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