diff --git a/platform/android/res/menu/project_folder_menu.xml b/platform/android/res/menu/project_folder_menu.xml deleted file mode 100644 index 92b3c7be09..0000000000 --- a/platform/android/res/menu/project_folder_menu.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - diff --git a/platform/android/res/menu/project_item_menu.xml b/platform/android/res/menu/project_item_menu.xml deleted file mode 100644 index 1a13b02d05..0000000000 --- a/platform/android/res/menu/project_item_menu.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - diff --git a/platform/android/res/menu/project_menu.xml b/platform/android/res/menu/project_menu.xml deleted file mode 100644 index 0dc338d644..0000000000 --- a/platform/android/res/menu/project_menu.xml +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - - - - - diff --git a/platform/android/res/values/strings.xml b/platform/android/res/values/strings.xml index 8607da0e69..de89829e40 100644 --- a/platform/android/res/values/strings.xml +++ b/platform/android/res/values/strings.xml @@ -36,10 +36,6 @@ Allow Deny Permanently Deny Once - Import Project from Folder - Import Project from ZIP - Import Dataset(s) - Storage Management Help Import Error The selected dataset(s) could not be imported properly. The selected project archive was not imported properly. @@ -52,13 +48,6 @@ Cancel Export Error The selected project folder was not exported properly. - Send to... - Send Compressed Folder to... - Export to Folder... - Add to Favorites - Remove from Favorites - Remove Dataset - Remove Project Folder Removal Confirmation The dataset will be permamently deleted, proceed with removal? The project folder will be permamently deleted, proceed with removal? @@ -70,6 +59,7 @@ I\'ll read later Please wait while QField is importing the project Please wait while QField is importing the dataset(s) + Please wait while QField is updating the local project Currently uploading attachments to QFieldCloud Operation Unsupported Your device does not support this import operation. diff --git a/platform/android/src/ch/opengis/qfield/QFieldActivity.java b/platform/android/src/ch/opengis/qfield/QFieldActivity.java index c6e2c35deb..79f4d2fee8 100644 --- a/platform/android/src/ch/opengis/qfield/QFieldActivity.java +++ b/platform/android/src/ch/opengis/qfield/QFieldActivity.java @@ -99,6 +99,14 @@ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE public class QFieldActivity extends QtActivity { + private static final int IMPORT_DATASET = 300; + private static final int IMPORT_PROJECT_FOLDER = 301; + private static final int IMPORT_PROJECT_ARCHIVE = 302; + + private static final int UPDATE_PROJECT_FROM_ARCHIVE = 400; + + private static final int EXPORT_TO_FOLDER = 500; + private SharedPreferences sharedPreferences; private SharedPreferences.Editor sharedPreferenceEditor; private ProgressDialog progressDialog; @@ -112,6 +120,7 @@ public class QFieldActivity extends QtActivity { private float originalBrightness; private boolean handleVolumeKeys = false; private String pathsToExport; + private String projectPath; private double sceneTopMargin = 0; private double sceneBottomMargin = 0; @@ -608,7 +617,7 @@ private void triggerImportDatasets() { intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true); intent.setType("*/*"); try { - startActivityForResult(intent, R.id.import_dataset); + startActivityForResult(intent, IMPORT_DATASET); } catch (ActivityNotFoundException e) { displayAlertDialog( getString(R.string.operation_unsupported), @@ -625,7 +634,7 @@ private void triggerImportProjectFolder() { intent.addFlags(Intent.FLAG_GRANT_PREFIX_URI_PERMISSION); intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION); try { - startActivityForResult(intent, R.id.import_project_folder); + startActivityForResult(intent, IMPORT_PROJECT_FOLDER); } catch (ActivityNotFoundException e) { displayAlertDialog( getString(R.string.operation_unsupported), @@ -643,7 +652,26 @@ private void triggerImportProjectArchive() { intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION); intent.setType("application/zip"); try { - startActivityForResult(intent, R.id.import_project_archive); + startActivityForResult(intent, IMPORT_PROJECT_ARCHIVE); + } catch (ActivityNotFoundException e) { + displayAlertDialog( + getString(R.string.operation_unsupported), + getString(R.string.import_operation_unsupported)); + Log.w("QField", "No activity found for ACTION_OPEN_DOCUMENT."); + } + return; + } + + private void triggerUpdateProjectFromArchive(String path) { + projectPath = path; + Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | + Intent.FLAG_GRANT_WRITE_URI_PERMISSION); + intent.addFlags(Intent.FLAG_GRANT_PREFIX_URI_PERMISSION); + intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION); + intent.setType("application/zip"); + try { + startActivityForResult(intent, UPDATE_PROJECT_FROM_ARCHIVE); } catch (ActivityNotFoundException e) { displayAlertDialog( getString(R.string.operation_unsupported), @@ -695,7 +723,7 @@ private void exportToFolder(String paths) { intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION); intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION); try { - startActivityForResult(intent, R.id.export_to_folder); + startActivityForResult(intent, EXPORT_TO_FOLDER); } catch (ActivityNotFoundException e) { displayAlertDialog( getString(R.string.operation_unsupported), @@ -948,6 +976,54 @@ public void run() { }); } + void updateProjectFromArchive(Uri archiveUri) { + File externalFilesDir = getExternalFilesDir(null); + if (externalFilesDir == null) { + return; + } + + ProgressDialog progressDialog = + new ProgressDialog(this, R.style.DialogTheme); + progressDialog.setMessage(getString(R.string.update_project_wait)); + progressDialog.setIndeterminate(true); + progressDialog.setCancelable(false); + progressDialog.show(); + + Context context = getApplication().getApplicationContext(); + ContentResolver resolver = getContentResolver(); + + executorService.execute(new Runnable() { + @Override + public void run() { + DocumentFile documentFile = + DocumentFile.fromSingleUri(context, archiveUri); + + String projectFolder = + new File(projectPath).getParentFile().getAbsolutePath() + + "/"; + boolean imported = false; + try { + InputStream input = resolver.openInputStream(archiveUri); + imported = QFieldUtils.zipToFolder(input, projectFolder); + } catch (Exception e) { + e.printStackTrace(); + + if (!isFinishing()) { + displayAlertDialog( + getString(R.string.import_error), + getString(R.string.import_project_archive_error)); + } + } + + progressDialog.dismiss(); + if (imported) { + // Trigger a project re-load + openProject(projectPath); + } + } + }); + } + private void checkPermissions() { List permissionsList = new ArrayList(); if (ContextCompat.checkSelfPermission( @@ -1093,8 +1169,7 @@ public void onClick( protected void onActivityResult(int requestCode, int resultCode, Intent data) { - if (requestCode == R.id.import_dataset && - resultCode == Activity.RESULT_OK) { + if (requestCode == IMPORT_DATASET && resultCode == Activity.RESULT_OK) { Log.d("QField", "handling import dataset(s)"); File externalFilesDir = getExternalFilesDir(null); if (externalFilesDir == null || data == null) { @@ -1159,7 +1234,7 @@ public void onClick(DialogInterface dialog, int id) { } else { importDatasets(datasetUris); } - } else if (requestCode == R.id.import_project_folder && + } else if (requestCode == IMPORT_PROJECT_FOLDER && resultCode == Activity.RESULT_OK) { Log.d("QField", "handling import project folder"); File externalFilesDir = getExternalFilesDir(null); @@ -1199,7 +1274,7 @@ public void onClick(DialogInterface dialog, int id) { } else { importProjectFolder(uri); } - } else if (requestCode == R.id.import_project_archive && + } else if (requestCode == IMPORT_PROJECT_ARCHIVE && resultCode == Activity.RESULT_OK) { Log.d("QField", "handling import project archive"); File externalFilesDir = getExternalFilesDir(null); @@ -1248,7 +1323,23 @@ public void onClick(DialogInterface dialog, int id) { } else { importProjectArchive(uri); } - } else if (requestCode == R.id.export_to_folder && + } else if (requestCode == UPDATE_PROJECT_FROM_ARCHIVE && + resultCode == Activity.RESULT_OK) { + Log.d("QField", "handling updating project from archive"); + File externalFilesDir = getExternalFilesDir(null); + if (externalFilesDir == null || data == null) { + return; + } + + Uri uri = data.getData(); + Context context = getApplication().getApplicationContext(); + ContentResolver resolver = getContentResolver(); + + DocumentFile documentFile = + DocumentFile.fromSingleUri(context, uri); + + updateProjectFromArchive(uri); + } else if (requestCode == EXPORT_TO_FOLDER && resultCode == Activity.RESULT_OK) { Log.d("QField", "handling export to folder"); diff --git a/src/core/platforms/android/androidplatformutilities.cpp b/src/core/platforms/android/androidplatformutilities.cpp index e2aad1d454..04ec5c424f 100644 --- a/src/core/platforms/android/androidplatformutilities.cpp +++ b/src/core/platforms/android/androidplatformutilities.cpp @@ -98,7 +98,7 @@ AndroidPlatformUtilities::AndroidPlatformUtilities() PlatformUtilities::Capabilities AndroidPlatformUtilities::capabilities() const { - PlatformUtilities::Capabilities capabilities = Capabilities() | NativeCamera | AdjustBrightness | CustomLocalDataPicker | CustomImport | CustomExport | CustomSend | FilePicker | VolumeKeys; + PlatformUtilities::Capabilities capabilities = Capabilities() | NativeCamera | AdjustBrightness | CustomLocalDataPicker | CustomImport | CustomExport | CustomSend | FilePicker | VolumeKeys | UpdateProjectFromArchive; #ifdef WITH_SENTRY capabilities |= SentryFramework; #endif @@ -251,6 +251,25 @@ void AndroidPlatformUtilities::importDatasets() const } } +void AndroidPlatformUtilities::updateProjectFromArchive( const QString &projectPath ) const +{ + if ( mActivity.isValid() ) + { + runOnAndroidMainThread( [projectPath] { + auto activity = qtAndroidContext(); + if ( activity.isValid() ) + { +#if QT_VERSION < QT_VERSION_CHECK( 6, 0, 0 ) + QAndroidJniObject projectPathJni = QAndroidJniObject::fromString( projectPath ); +#else + QJniObject projectPathJni = QJniObject::fromString( projectPath ); +#endif + activity.callMethod( "triggerUpdateProjectFromArchive", "(Ljava/lang/String;)V", projectPathJni.object() ); + } + } ); + } +} + void AndroidPlatformUtilities::sendDatasetTo( const QString &path ) const { if ( mActivity.isValid() ) diff --git a/src/core/platforms/android/androidplatformutilities.h b/src/core/platforms/android/androidplatformutilities.h index 658db30705..271dd696d1 100644 --- a/src/core/platforms/android/androidplatformutilities.h +++ b/src/core/platforms/android/androidplatformutilities.h @@ -46,6 +46,8 @@ class AndroidPlatformUtilities : public PlatformUtilities void importProjectArchive() const override; void importDatasets() const override; + void updateProjectFromArchive( const QString &projectPath ) const override; + void sendDatasetTo( const QString &path ) const override; void exportDatasetTo( const QString &path ) const override; void removeDataset( const QString &path ) const override; diff --git a/src/core/platforms/platformutilities.cpp b/src/core/platforms/platformutilities.cpp index b180b66f7b..9bf567ddb4 100644 --- a/src/core/platforms/platformutilities.cpp +++ b/src/core/platforms/platformutilities.cpp @@ -206,6 +206,11 @@ void PlatformUtilities::importProjectArchive() const void PlatformUtilities::importDatasets() const {} +void PlatformUtilities::updateProjectFromArchive( const QString &projectPath ) const +{ + Q_UNUSED( projectPath ) +} + void PlatformUtilities::exportFolderTo( const QString &path ) const { Q_UNUSED( path ) diff --git a/src/core/platforms/platformutilities.h b/src/core/platforms/platformutilities.h index 21b3c9ea06..f5bc966cc3 100644 --- a/src/core/platforms/platformutilities.h +++ b/src/core/platforms/platformutilities.h @@ -41,16 +41,17 @@ class QFIELD_CORE_EXPORT PlatformUtilities : public QObject public: enum Capability { - NoCapabilities = 0, //!< No capabilities - NativeCamera = 1, //!< Native camera handling support - AdjustBrightness = 1 << 1, //!< Screen brightness adjustment support - SentryFramework = 1 << 2, //!< Sentry framework support - CustomLocalDataPicker = 1 << 3, //!< Custom QML local data picker support - CustomImport = 1 << 4, //!< Import project and dataset support - CustomExport = 1 << 5, //!< Export project and dataset support - CustomSend = 1 << 6, //!< Send/share files support - FilePicker = 1 << 7, //!< File picker support - VolumeKeys = 1 << 8, //!< Volume keys handling support + NoCapabilities = 0, //!< No capabilities + NativeCamera = 1, //!< Native camera handling support + AdjustBrightness = 1 << 1, //!< Screen brightness adjustment support + SentryFramework = 1 << 2, //!< Sentry framework support + CustomLocalDataPicker = 1 << 3, //!< Custom QML local data picker support + CustomImport = 1 << 4, //!< Import project and dataset support + CustomExport = 1 << 5, //!< Export project and dataset support + CustomSend = 1 << 6, //!< Send/share files support + FilePicker = 1 << 7, //!< File picker support + VolumeKeys = 1 << 8, //!< Volume keys handling support + UpdateProjectFromArchive = 1 << 9, //!< Update local project from a ZIP archive support }; Q_DECLARE_FLAGS( Capabilities, Capability ) Q_FLAGS( Capabilities ) @@ -130,6 +131,12 @@ class QFIELD_CORE_EXPORT PlatformUtilities : public QObject //! Requests and imports one or more datasets into QField's application directory action Q_INVOKABLE virtual void importDatasets() const; + /** + * Update a local project content from a user-picked archive file action + * \param projectPath the project file path + */ + Q_INVOKABLE virtual void updateProjectFromArchive( const QString &projectPath ) const; + //! Exports a folder \a path to a user-specified location Q_INVOKABLE virtual void exportFolderTo( const QString &path ) const; //! Exports a dataset \a path to a user-specified location diff --git a/src/qml/QFieldLocalDataPickerScreen.qml b/src/qml/QFieldLocalDataPickerScreen.qml index f95852f29f..6ea9c714f3 100644 --- a/src/qml/QFieldLocalDataPickerScreen.qml +++ b/src/qml/QFieldLocalDataPickerScreen.qml @@ -325,9 +325,14 @@ Page { } QfToolButton { - id: importButton + id: actionButton round: true - visible: table.model.currentPath === 'root' + + // Since the project menu only has one action for now, hide if PlatformUtilities.UpdateProjectFromArchive is missing + property bool isLocalProject: QFieldCloudUtils.getProjectId(qgisProject.fileName) === '' && + (projectInfo.filePath.endsWith('.qgs') || projectInfo.filePath.endsWith('.qgz')) && + platformUtilities.capabilities & PlatformUtilities.UpdateProjectFromArchive + visible: (projectFolderView && isLocalProject && table.model.currentDepth === 1) || table.model.currentPath === 'root' anchors.bottom: parent.bottom anchors.right: parent.right @@ -338,8 +343,12 @@ Page { iconSource: Theme.getThemeIcon( "ic_add_white_24dp" ) onClicked: { - importMenu.popup(importButton.x + importButton.width - importMenu.width + 10, - importButton.y - importButton.height - header.height - 10) + var xy = mapToItem(mainWindow.contentItem, actionButton.width, actionButton.height) + if (projectFolderView) { + projectMenu.popup(xy.x - projectMenu.width, xy.y - projectMenu.height - header.height) + } else { + importMenu.popup(xy.x - importMenu.width, xy.y - importMenu.height - header.height) + } } } } @@ -558,6 +567,40 @@ Page { onTriggered: { Qt.openUrlExternally("https://docs.qfield.org/get-started/storage/") } } } + + Menu { + id: projectMenu + + title: qsTr('Project Actions') + + width: { + var result = 0; + var padding = 0; + for (var i = 0; i < count; ++i) { + var item = itemAt(i); + result = Math.max(item.contentItem.implicitWidth, result); + padding = Math.max(item.padding, padding); + } + return Math.min( result + padding * 2,mainWindow.width - 20); + } + + topMargin: sceneTopMargin + bottomMargin: sceneBottomMargin + + MenuItem { + id: updateProjectFromArchive + + enabled: platformUtilities.capabilities & PlatformUtilities.UpdateProjectFromArchive + visible: enabled + font: Theme.defaultFont + width: parent.width + height: enabled ? undefined : 0 + leftPadding: 10 + + text: qsTr( "Update project from ZIP" ) + onTriggered: { platformUtilities.updateProjectFromArchive(projectInfo.filePath); } + } + } } Dialog {