diff --git a/platform/ios/Info.plist.in b/platform/ios/Info.plist.in index 960877c135..4f9c60cb7a 100644 --- a/platform/ios/Info.plist.in +++ b/platform/ios/Info.plist.in @@ -131,7 +131,7 @@ LSSupportsOpeningDocumentsInPlace - + LSApplicationQueriesSchemes @@ -148,5 +148,197 @@ + + + CFBundleDocumentTypes + + + CFBundleTypeName + QGIS project + CFBundleTypeRole + Editor + LSHandlerRank + Owner + LSItemContentTypes + + org.qgis.qgs + org.qgis.qgz + + + + CFBundleTypeName + Geospatial dataset + CFBundleTypeRole + Editor + LSHandlerRank + Owner + LSItemContentTypes + + org.opengeospatial.geopackage + com.mapbox.mbtiles + org.geojson.geojson + com.google.earth.kml + com.google.earth.kmz + com.topografix.gpx + + + + CFBundleTypeName + Supported document + CFBundleTypeRole + Viewer + LSHandlerRank + Alternate + LSItemContentTypes + + com.adobe.pdf + public.zip-archive + public.json + public.tiff + public.jpeg-2000 + + + + + + UTImportedTypeDeclarations + + + UTTypeIdentifier + org.qgis.qgs + UTTypeDescription + QGIS project + UTTypeConformsTo + + public.xml + + UTTypeTagSpecification + + public.filename-extension + + qgs + + + + + UTTypeIdentifier + org.qgis.qgz + UTTypeDescription + QGIS compressed project + UTTypeConformsTo + + public.zip-archive + + UTTypeTagSpecification + + public.filename-extension + + qgz + + + + + UTTypeIdentifier + org.opengeospatial.geopackage + UTTypeDescription + GeoPackage + UTTypeConformsTo + + public.data + + UTTypeTagSpecification + + public.filename-extension + + gpkg + + + + + UTTypeIdentifier + com.mapbox.mbtiles + UTTypeDescription + MBTiles + UTTypeConformsTo + + public.data + + UTTypeTagSpecification + + public.filename-extension + + mbtiles + + + + + UTTypeIdentifier + org.geojson.geojson + UTTypeDescription + GeoJSON + UTTypeConformsTo + + public.json + + UTTypeTagSpecification + + public.filename-extension + + geojson + + + + + UTTypeIdentifier + com.google.earth.kml + UTTypeDescription + KML + UTTypeConformsTo + + public.xml + + UTTypeTagSpecification + + public.filename-extension + + kml + + + + + UTTypeIdentifier + com.google.earth.kmz + UTTypeDescription + KMZ + UTTypeConformsTo + + public.zip-archive + + UTTypeTagSpecification + + public.filename-extension + + kmz + + + + + UTTypeIdentifier + com.topografix.gpx + UTTypeDescription + GPX + UTTypeConformsTo + + public.xml + + UTTypeTagSpecification + + public.filename-extension + + gpx + + + + diff --git a/src/core/platforms/ios/iosplatformutilities.h b/src/core/platforms/ios/iosplatformutilities.h index f3f8306b86..f9b4863bc3 100644 --- a/src/core/platforms/ios/iosplatformutilities.h +++ b/src/core/platforms/ios/iosplatformutilities.h @@ -39,6 +39,7 @@ class IosPlatformUtilities : public PlatformUtilities void importProjectFolder() const override; void importProjectArchive() const override; void importDatasets() const override; + void importFile( const QString &path ) const override; void exportDatasetTo( const QString &path ) const override; void exportFolderTo( const QString &path ) const override; void sendDatasetTo( const QString &path ) const override; diff --git a/src/core/platforms/ios/iosplatformutilities.mm b/src/core/platforms/ios/iosplatformutilities.mm index f98d9020e7..cb4178d318 100644 --- a/src/core/platforms/ios/iosplatformutilities.mm +++ b/src/core/platforms/ios/iosplatformutilities.mm @@ -75,7 +75,8 @@ - (void)documentInteractionControllerDidEndPreview: PlatformUtilities::Capabilities IosPlatformUtilities::capabilities() const { PlatformUtilities::Capabilities capabilities = Capabilities() | NativeCamera | AdjustBrightness | FilePicker | - CustomImport | CustomSend | CustomExport | UpdateProjectFromArchive; + CustomImport | CustomSend | CustomExport | UpdateProjectFromArchive | + FileImport; #if WITH_SENTRY capabilities |= SentryFramework; #endif @@ -513,6 +514,114 @@ - (void)documentPickerWasCancelled: [root presentViewController:picker animated:YES completion:nil]; } +static void removeInboxFile(const QString &path, const QString &appDir) { + const QString inboxDir = appDir + QStringLiteral("/Inbox/"); + if (path.startsWith(inboxDir)) { + QFile::remove(path); + } +} + +void IosPlatformUtilities::importFile(const QString &path) const { + NSLog(@"QField[importFile] called with path: %@", path.toNSString()); + + QFileInfo fileInfo(path); + if (!fileInfo.exists() || !fileInfo.isFile()) { + NSLog(@"QField[importFile] abort: not an existing regular file"); + return; + } + if (!AppInterface::instance()) { + NSLog(@"QField[importFile] abort: AppInterface instance is null"); + return; + } + + const QString appDir = applicationDirectory(); + const QString suffix = fileInfo.suffix().toLower(); + const bool isProjectFile = + suffix == QLatin1String("qgs") || suffix == QLatin1String("qgz"); + const bool isArchive = suffix == QLatin1String("zip"); + NSLog(@"QField[importFile] suffix=%@ isProjectFile=%d isArchive=%d", + suffix.toNSString(), isProjectFile, isArchive); + + if (isArchive) { + const QString importBase = appDir + QStringLiteral("/Imported Projects/"); + QDir().mkpath(importBase); + + QString destinationDir = importBase + fileInfo.completeBaseName(); + int index = 1; + while (QFileInfo::exists(destinationDir)) { + destinationDir = importBase + fileInfo.completeBaseName() + + QStringLiteral("_%1").arg(index); + ++index; + } + QDir().mkpath(destinationDir); + + QStringList extractedFiles; + if (!FileUtils::unzip(path, destinationDir, extractedFiles, false)) { + NSLog(@"QField[importFile] abort: unzip failed for %@", + path.toNSString()); + return; + } + NSLog(@"QField[importFile] extracted %lu files into %@", + (unsigned long)extractedFiles.size(), destinationDir.toNSString()); + removeInboxFile(path, appDir); + + // Open the project bundled within the archive, or reveal its content + QString projectFile; + for (const QString &extractedFile : extractedFiles) { + const QString extractedSuffix = + QFileInfo(extractedFile).suffix().toLower(); + if (extractedSuffix == QLatin1String("qgs") || + extractedSuffix == QLatin1String("qgz")) { + projectFile = extractedFile; + break; + } + } + + if (!projectFile.isEmpty()) { + NSLog(@"QField[importFile] loading bundled project: %@", + projectFile.toNSString()); + AppInterface::instance()->loadFile(projectFile); + } else { + NSLog(@"QField[importFile] no project in archive, revealing: %@", + destinationDir.toNSString()); + emit AppInterface::instance()->openPath(destinationDir); + } + return; + } + + const QString importBase = + isProjectFile ? appDir + QStringLiteral("/Imported Projects/") + : appDir + QStringLiteral("/Imported Datasets/"); + QDir().mkpath(importBase); + + const QString suffixPart = + suffix.isEmpty() ? QString() : QStringLiteral(".") + suffix; + QString destinationFile = importBase + fileInfo.fileName(); + int index = 1; + while (QFileInfo::exists(destinationFile)) { + destinationFile = importBase + fileInfo.completeBaseName() + + QStringLiteral("_%1").arg(index) + suffixPart; + ++index; + } + + if (!QFile::copy(path, destinationFile)) { + NSLog(@"QField[importFile] abort: copy failed %@ -> %@", path.toNSString(), + destinationFile.toNSString()); + return; + } + NSLog(@"QField[importFile] copied to %@", destinationFile.toNSString()); + removeInboxFile(path, appDir); + + const bool loaded = AppInterface::instance()->loadFile(destinationFile); + NSLog(@"QField[importFile] loadFile(%@) returned %d", + destinationFile.toNSString(), loaded); + if (!loaded) { + NSLog(@"QField[importFile] revealing in browser: %@", + importBase.toNSString()); + emit AppInterface::instance()->openPath(importBase); + } +} + void IosPlatformUtilities::sendDatasetTo(const QString &path) const { NSMutableArray *items = [NSMutableArray array]; [items addObject:[NSURL fileURLWithPath:path.toNSString()]]; diff --git a/src/core/platforms/platformutilities.cpp b/src/core/platforms/platformutilities.cpp index 56e00cda01..35e3c9af3f 100644 --- a/src/core/platforms/platformutilities.cpp +++ b/src/core/platforms/platformutilities.cpp @@ -279,6 +279,11 @@ void PlatformUtilities::importProjectArchive() const void PlatformUtilities::importDatasets() const {} +void PlatformUtilities::importFile( const QString &path ) const +{ + Q_UNUSED( path ) +} + void PlatformUtilities::updateProjectFromArchive( const QString &projectPath ) const { const QString zipFilePath = QFileDialog::getOpenFileName( nullptr, diff --git a/src/core/platforms/platformutilities.h b/src/core/platforms/platformutilities.h index fb40e25e42..e0a137dc55 100644 --- a/src/core/platforms/platformutilities.h +++ b/src/core/platforms/platformutilities.h @@ -56,6 +56,7 @@ class QFIELD_CORE_EXPORT PlatformUtilities : public QObject Vibrate = 1 << 9, //!< Haptic feedback / vibration support UpdateProjectFromArchive = 1 << 10, //!< Update local project from a ZIP archive support PositioningService = 1 << 11, //!< Positioning service support + FileImport = 1 << 12, //!< Importing files shared with the app from other applications }; Q_DECLARE_FLAGS( Capabilities, Capability ) Q_FLAGS( Capabilities ) @@ -146,6 +147,8 @@ class QFIELD_CORE_EXPORT PlatformUtilities : public QObject Q_INVOKABLE virtual void importProjectArchive() const; //! Requests and imports one or more datasets into QField's application directory action Q_INVOKABLE virtual void importDatasets() const; + //! Imports a file shared with QField by the operating system into its application directory and opens it + Q_INVOKABLE virtual void importFile( const QString &path ) const; /** * Update a local project content from a user-picked archive file action diff --git a/src/core/qfieldurlhandler.cpp b/src/core/qfieldurlhandler.cpp index 88bcf77550..32a7f2568b 100644 --- a/src/core/qfieldurlhandler.cpp +++ b/src/core/qfieldurlhandler.cpp @@ -16,8 +16,11 @@ ***************************************************************************/ #include "appinterface.h" +#include "platformutilities.h" #include "qfieldurlhandler.h" +#include + QFieldUrlHandler::QFieldUrlHandler( AppInterface *iface, QObject *parent ) : QObject( parent ), mIface( iface ) { @@ -25,6 +28,15 @@ QFieldUrlHandler::QFieldUrlHandler( AppInterface *iface, QObject *parent ) void QFieldUrlHandler::handleUrl( const QUrl &url ) { + if ( url.isLocalFile() ) + { + // Import on the next event loop cycle so the potentially heavy work (e.g. + // unzipping an archive) does not block the incoming-URL handling + const QString path = url.toLocalFile(); + QTimer::singleShot( 0, [path]() { PlatformUtilities::instance()->importFile( path ); } ); + return; + } + if ( mIface ) { mIface->executeAction( url.toString() ); diff --git a/src/core/qgismobileapp.cpp b/src/core/qgismobileapp.cpp index 416f6a9f7e..8bedac44c5 100644 --- a/src/core/qgismobileapp.cpp +++ b/src/core/qgismobileapp.cpp @@ -237,6 +237,10 @@ QgisMobileapp::QgisMobileapp( QgsApplication *app, QObject *parent ) mUrlHandler.reset( new QFieldUrlHandler( mIface, this ) ); QDesktopServices::setUrlHandler( QStringLiteral( "qfield" ), mUrlHandler.get(), "handleUrl" ); + if ( PlatformUtilities::instance()->capabilities() & PlatformUtilities::FileImport ) + { + QDesktopServices::setUrlHandler( QStringLiteral( "file" ), mUrlHandler.get(), "handleUrl" ); + } mMessageLogModel = new MessageLogModel( this );