diff --git a/android/src/main/java/org/openforis/collect/android/gui/BaseActivity.java b/android/src/main/java/org/openforis/collect/android/gui/BaseActivity.java index 9d4e0be3..df64523a 100644 --- a/android/src/main/java/org/openforis/collect/android/gui/BaseActivity.java +++ b/android/src/main/java/org/openforis/collect/android/gui/BaseActivity.java @@ -1,5 +1,6 @@ package org.openforis.collect.android.gui; +import android.app.Activity; import android.os.Bundle; import android.view.MenuItem; @@ -70,4 +71,10 @@ public void run() { } }); } + + public static void restartMainActivity(Activity context) { + ServiceLocator.reset(context); + Activities.startNewClearTask(context, MainActivity.class); + context.finish(); + } } diff --git a/android/src/main/java/org/openforis/collect/android/gui/MainActivity.java b/android/src/main/java/org/openforis/collect/android/gui/MainActivity.java index e67ce785..fc77e2d0 100644 --- a/android/src/main/java/org/openforis/collect/android/gui/MainActivity.java +++ b/android/src/main/java/org/openforis/collect/android/gui/MainActivity.java @@ -46,11 +46,11 @@ protected void initialize() { private void initializeServices() { try { ServiceLocator.init(this); - } catch (WorkingDirNotWritable ignore) { - DialogFragment newFragment = new SecondaryStorageNotFoundFragment(); + } catch (WorkingDirNotAccessible ignore) { + DialogFragment newFragment = new WorkingDirectoryNotAccessibleFragment(); newFragment.show(getSupportFragmentManager(), "secondaryStorageNotFound"); - } catch (StorageAccessException e) { - handleStorageAccessException(e); + } catch (Exception e) { + handleStorageAccessException(new StorageAccessException()); } } @@ -98,6 +98,8 @@ protected void onCreate(@Nullable Bundle savedState) { @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + if (!Permissions.isPermissionGranted(grantResults)) { return; } diff --git a/android/src/main/java/org/openforis/collect/android/gui/ServiceLocator.java b/android/src/main/java/org/openforis/collect/android/gui/ServiceLocator.java index b33389b6..33a1ad5d 100644 --- a/android/src/main/java/org/openforis/collect/android/gui/ServiceLocator.java +++ b/android/src/main/java/org/openforis/collect/android/gui/ServiceLocator.java @@ -70,7 +70,7 @@ public class ServiceLocator { * Initializes the ServiceLocator. * Returns true if there is a selected survey, false otherwise */ - public static boolean init(Context applicationContext) throws WorkingDirNotWritable { + public static boolean init(Context applicationContext) throws WorkingDirNotAccessible { if (surveyService == null) { SettingsActivity.init(applicationContext); UILanguageInitializer.init(applicationContext); @@ -161,7 +161,8 @@ private static void deleteDatabase(String databaseName, String surveyName, Andro } public static boolean isSurveyImported(String surveyName, Context context) { - return context.getDatabasePath(databasePath(MODEL_DB, surveyName, context).getAbsolutePath()).exists(); + File dbFile = context.getDatabasePath(databasePath(MODEL_DB, surveyName, context).getAbsolutePath()); + return dbFile.exists(); } private static AndroidDatabase createModelDatabase(String surveyName, Context applicationContext) { @@ -264,9 +265,7 @@ private static CollectModelManager createCollectModelManager(AndroidDatabase mod RecordFileManager recordFileManager = new RecordFileManager() {{ storageDirectory = AppDirs.surveyImagesDir(surveyName, context); if (!storageDirectory.exists()) { - if (!storageDirectory.mkdirs()) - throw new WorkingDirNotWritable(storageDirectory); - AndroidFiles.makeDiscoverable(storageDirectory, context); + AndroidFiles.createAndMakeDiscoverableDir(storageDirectory, context); } }}; recordFileManager.setDefaultRootStoragePath(AppDirs.surveyDatabasesDir(surveyName, context).getAbsolutePath()); diff --git a/android/src/main/java/org/openforis/collect/android/gui/SurveyImporter.java b/android/src/main/java/org/openforis/collect/android/gui/SurveyImporter.java index 87e94ab6..46e92ccb 100644 --- a/android/src/main/java/org/openforis/collect/android/gui/SurveyImporter.java +++ b/android/src/main/java/org/openforis/collect/android/gui/SurveyImporter.java @@ -108,7 +108,7 @@ private void migrateIfNeeded(Version surveyAppVersion, File targetSurveyDatabase public static void importDefaultSurvey(Context context) { try { - File tempDir = createTempDir(); + File tempDir = org.openforis.collect.android.util.FileUtils.createTempDir(); InputStream sourceInput = SurveyImporter.class.getResourceAsStream("/demo.collect-mobile"); File intermediateSurveyPath = new File(tempDir, "demo.collect-mobile"); FileOutputStream intermediateOutput = new FileOutputStream(intermediateSurveyPath); @@ -138,13 +138,13 @@ private SurveyBackupInfo info(File dir) { } catch (IOException e) { throw new MalformedSurvey(sourceSurveyPath, e); } finally { - close(is); + IOUtils.closeQuietly(is); } return info; } private File unzipSurveyDefinition() throws IOException { - File folder = createTempDir(); + File folder = org.openforis.collect.android.util.FileUtils.createTempDir(); File zipFile = new File(sourceSurveyPath); if (!zipFile.exists()) throw new FileNotFoundException("File not found: " + sourceSurveyPath); @@ -157,23 +157,6 @@ private File unzipSurveyDefinition() throws IOException { return folder; } - private static File createTempDir() throws IOException { - File tempDir = File.createTempFile("collect", Long.toString((System.nanoTime()))); - if (!tempDir.delete()) - throw new IOException("Failed to create temp dir:" + tempDir.getAbsolutePath()); - if (!tempDir.mkdir()) - throw new IOException("Failed to create temp dir:" + tempDir.getAbsolutePath()); - return tempDir; - } - - private void close(InputStream is) { - if (is != null) - try { - is.close(); - } catch (IOException ignore) { - } - } - public static String surveyMinorVersion(Version version) { return version.getMajor() + "." + version.getMinor() + ".x"; } diff --git a/android/src/main/java/org/openforis/collect/android/gui/SurveyListActivity.java b/android/src/main/java/org/openforis/collect/android/gui/SurveyListActivity.java index 9a4d7889..9bb150d7 100644 --- a/android/src/main/java/org/openforis/collect/android/gui/SurveyListActivity.java +++ b/android/src/main/java/org/openforis/collect/android/gui/SurveyListActivity.java @@ -165,13 +165,6 @@ private static class ImportSurveyTask extends SlowAsyncTask protected Boolean runTask() throws Exception { super.runTask(); File file = AndroidFiles.copyUriContentToCache(context, uri); - if (file == null) { - throw new IllegalStateException(String.format( - "Failed to import survey; could not determine file path for URI: %s", uri)); - } - if (file.length() == 0) { - throw new IllegalStateException(context.getString(R.string.survey_import_failed_empty_file_message)); - } if (ServiceLocator.importSurvey(file.getAbsolutePath(), overwrite, context) || overwrite) { onSurveyImportComplete(); return false; //survey imported successfully diff --git a/android/src/main/java/org/openforis/collect/android/gui/SurveyNodeActivity.java b/android/src/main/java/org/openforis/collect/android/gui/SurveyNodeActivity.java index 59ae637d..f101677f 100644 --- a/android/src/main/java/org/openforis/collect/android/gui/SurveyNodeActivity.java +++ b/android/src/main/java/org/openforis/collect/android/gui/SurveyNodeActivity.java @@ -22,7 +22,6 @@ import org.openforis.collect.android.NodeEvent; import org.openforis.collect.android.SurveyListener; import org.openforis.collect.android.SurveyService; -import org.openforis.collect.android.gui.backup.Backup; import org.openforis.collect.android.gui.barcode.BarcodeCaptureActivity; import org.openforis.collect.android.gui.detail.ExportDialogFragment; import org.openforis.collect.android.gui.entitytable.EntityTableDialogFragment; @@ -81,7 +80,6 @@ public class SurveyNodeActivity extends BaseActivity implements SurveyListener, private VideoFileAttributeComponent videoListener; private DocumentFileAttributeComponent fileDocumentListener; private BarcodeTextAttributeComponent barcodeCaptureListener; - private boolean twoPane; public static void startClearSurveyNodeActivity(Context context) { @@ -270,10 +268,6 @@ public void navigateDown(View view) { navigateDown(); } - public void backup(MenuItem item) { - Backup.showBackupModeChooseDialog(this); - } - public void exportDialog(MenuItem item) { if (Permissions.checkStoragePermissionOrRequestIt(this)) { @@ -432,6 +426,7 @@ protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (fileDocumentListener != null && data != null) { fileDocumentListener.documentSelected(data.getData()); } + break; } } super.onActivityResult(requestCode, resultCode, data); diff --git a/android/src/main/java/org/openforis/collect/android/gui/WorkingDirNotAccessible.java b/android/src/main/java/org/openforis/collect/android/gui/WorkingDirNotAccessible.java new file mode 100644 index 00000000..aa70658e --- /dev/null +++ b/android/src/main/java/org/openforis/collect/android/gui/WorkingDirNotAccessible.java @@ -0,0 +1,9 @@ +package org.openforis.collect.android.gui; + +import java.io.File; + +public class WorkingDirNotAccessible extends RuntimeException { + public WorkingDirNotAccessible(File workingDir) { + super("workingDir:" + workingDir); + } +} diff --git a/android/src/main/java/org/openforis/collect/android/gui/WorkingDirNotWritable.java b/android/src/main/java/org/openforis/collect/android/gui/WorkingDirNotWritable.java deleted file mode 100644 index 66de5755..00000000 --- a/android/src/main/java/org/openforis/collect/android/gui/WorkingDirNotWritable.java +++ /dev/null @@ -1,9 +0,0 @@ -package org.openforis.collect.android.gui; - -import java.io.File; - -public class WorkingDirNotWritable extends RuntimeException { - public WorkingDirNotWritable(File workingDir) { - super("workingDir:" + workingDir); - } -} diff --git a/android/src/main/java/org/openforis/collect/android/gui/SecondaryStorageNotFoundFragment.java b/android/src/main/java/org/openforis/collect/android/gui/WorkingDirectoryNotAccessibleFragment.java similarity index 89% rename from android/src/main/java/org/openforis/collect/android/gui/SecondaryStorageNotFoundFragment.java rename to android/src/main/java/org/openforis/collect/android/gui/WorkingDirectoryNotAccessibleFragment.java index 0498f6db..04dadabc 100644 --- a/android/src/main/java/org/openforis/collect/android/gui/SecondaryStorageNotFoundFragment.java +++ b/android/src/main/java/org/openforis/collect/android/gui/WorkingDirectoryNotAccessibleFragment.java @@ -12,11 +12,11 @@ import org.openforis.collect.android.gui.util.Activities; import org.openforis.collect.android.gui.util.AppDirs; -public class SecondaryStorageNotFoundFragment extends DialogFragment { +public class WorkingDirectoryNotAccessibleFragment extends DialogFragment { public Dialog onCreateDialog(Bundle savedInstanceState) { AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); - builder.setTitle(R.string.storage_not_found_message) + builder.setTitle(R.string.working_directory_not_accessible_message) .setMessage(AppDirs.root(getActivity()).getAbsolutePath()) .setPositiveButton(R.string.storage_not_found_retry, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int id) { diff --git a/android/src/main/java/org/openforis/collect/android/gui/backup/Backup.java b/android/src/main/java/org/openforis/collect/android/gui/backup/Backup.java index ebf32c43..1fbf45f5 100644 --- a/android/src/main/java/org/openforis/collect/android/gui/backup/Backup.java +++ b/android/src/main/java/org/openforis/collect/android/gui/backup/Backup.java @@ -11,14 +11,20 @@ import android.widget.ListView; import androidx.annotation.NonNull; +import androidx.appcompat.app.AppCompatActivity; import androidx.fragment.app.DialogFragment; import androidx.fragment.app.FragmentActivity; import org.apache.commons.io.FileUtils; import org.openforis.collect.R; +import org.openforis.collect.android.collectadapter.BackupGenerator; +import org.openforis.collect.android.gui.util.Activities; import org.openforis.collect.android.gui.util.AndroidFiles; +import org.openforis.collect.android.gui.util.App; import org.openforis.collect.android.gui.util.AppDirs; +import org.openforis.collect.android.gui.util.Dates; import org.openforis.collect.android.gui.util.Dialogs; +import org.openforis.collect.android.gui.util.MimeType; import org.openforis.collect.android.sqlite.AndroidDatabase; import java.io.File; @@ -26,7 +32,10 @@ public class Backup { - public static void showBackupModeChooseDialog(FragmentActivity activity) { + private static String BACKUP_FILE_PREFIX = "of_collect_mobile_backup_"; + public static String BACKUP_FILE_EXTENSION = "ofcmbck"; + + public static void showBackupModeChooseDialog(AppCompatActivity activity) { DialogFragment dialogFragment = new BackupModeDialogFragment(); dialogFragment.show(activity.getSupportFragmentManager(), "backupModeSelection"); } @@ -105,6 +114,55 @@ public void run() { } } + private void backupIntoDownloads() { + try { + File downloadDir = AndroidFiles.getDownloadsDir(context); + File backupFile = generateBackupFile(downloadDir); + if (backupFile == null) return; + AndroidFiles.makeDiscoverable(backupFile, context); + Dialogs.alert(context, R.string.backup_file_generation_complete, R.string.backup_file_generation_into_downloads_complete_message); + } catch (Exception e) { + showBackupErrorMessage(e); + } + } + + private void backupAndShare() { + File backupFile = generateBackupFile(null); + if (backupFile == null) return; + Activities.shareFile(context, backupFile, MimeType.BINARY, R.string.share_file, false); + } + + private File generateBackupFile(File parentDir) { + File destFile = null, tempFile = null; + try { + File destParentDir; + String destFileName = BACKUP_FILE_PREFIX + Dates.formatNowISO() + "." + BACKUP_FILE_EXTENSION; + if (parentDir == null) { + tempFile = File.createTempFile(BACKUP_FILE_PREFIX, "." + BACKUP_FILE_EXTENSION); + destParentDir = tempFile.getParentFile(); + destFile = new File(destParentDir, destFileName); + FileUtils.moveFile(tempFile, destFile); + } else { + destParentDir = parentDir; + destFile = new File(destParentDir, destFileName); + } + if (AndroidFiles.enoughSpaceToCopy(surveysDir, destParentDir)) { + BackupGenerator backupGenerator = new BackupGenerator(surveysDir, App.versionName(context), destFile); + backupGenerator.generate(); + } else { + destFile.delete(); + } + } catch (IOException e) { + showBackupErrorMessage(e); + if (tempFile != null) { + tempFile.delete(); + } + if (destFile != null) { + destFile.delete(); + } + } + return destFile; + } private void showInsertSdCardDialog() { Intent intent = new Intent(); @@ -176,7 +234,10 @@ public void onClick(View v) { backupExecutor.backupToNewSdCard(); break; case 1: - backupExecutor.backupInternally(); + backupExecutor.backupIntoDownloads(); + break; + case 2: + backupExecutor.backupAndShare(); break; } alertDialog.dismiss(); diff --git a/android/src/main/java/org/openforis/collect/android/gui/backup/Restore.java b/android/src/main/java/org/openforis/collect/android/gui/backup/Restore.java new file mode 100644 index 00000000..4c5a83dc --- /dev/null +++ b/android/src/main/java/org/openforis/collect/android/gui/backup/Restore.java @@ -0,0 +1,152 @@ +package org.openforis.collect.android.gui.backup; + +import android.app.Activity; +import android.net.Uri; + +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.FilenameUtils; +import org.openforis.collect.R; +import org.openforis.collect.android.collectadapter.BackupGenerator; +import org.openforis.collect.android.collectadapter.BackupInfo; +import org.openforis.collect.android.gui.BaseActivity; +import org.openforis.collect.android.gui.settings.SettingsActivity; +import org.openforis.collect.android.gui.util.Activities; +import org.openforis.collect.android.gui.util.AndroidFiles; +import org.openforis.collect.android.gui.util.App; +import org.openforis.collect.android.gui.util.AppDirs; +import org.openforis.collect.android.gui.util.Dialogs; +import org.openforis.collect.android.gui.util.SlowJob; +import org.openforis.collect.android.util.Permissions; +import org.openforis.collect.android.util.Unzipper; +import org.openforis.commons.versioning.Version; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; + +public class Restore { + + public static void confirmRestore(final Activity context) { + Dialogs.confirm(context, R.string.restore_confirm_title, R.string.restore_confirm_message, new Runnable() { + public void run() { + selectFileToRestore(context); + } + }); + } + + private static void selectFileToRestore(final Activity context) { + if (Permissions.checkReadExternalStoragePermissionOrRequestIt(context)) { + ((SettingsActivity) context).setRestoreFileSelectedListener(new RestoreFileSelectedListener() { + @Override + public void fileSelected(Uri fileUri) { + onFileSelected(fileUri, context); + } + }); + Activities.startFileChooserActivity(context, "Select file to restore", SettingsActivity.RESTORE_FILE_SELECTED_REQUEST_CODE, "*/*"); + } + } + + private static void onFileSelected(final Uri fileUri, final Activity context) { + String fileName = AndroidFiles.getUriContentFileName(context, fileUri); + if (!Backup.BACKUP_FILE_EXTENSION.equals(FilenameUtils.getExtension(fileName))) { + Dialogs.alert(context, R.string.warning, R.string.restore_error_invalid_backup_file); + } else { + Dialogs.confirm(context, R.string.restore_confirm_title, R.string.restore_confirm_message_2, new Runnable() { + public void run() { + new RestoreJob(context, fileUri).execute(); + } + }); + } + } + + private static class RestoreJob extends SlowJob { + + private final Uri fileUri; + + RestoreJob(Activity context, Uri fileUri) { + super(context, null, R.string.restore_progress_dialog_title, R.string.please_wait); + this.fileUri = fileUri; + } + + @Override + protected Boolean runTask() throws Exception { + super.runTask(); + File zipFile = null, + unzippedDir = null; + try { + zipFile = AndroidFiles.copyUriContentToCache(context, fileUri); + unzippedDir = org.openforis.collect.android.util.FileUtils.createTempDir(); + if (AndroidFiles.enoughSpaceToCopy(zipFile, unzippedDir)) { + new Unzipper(zipFile, unzippedDir).unzipAll(); + if (!checkBackupFile(unzippedDir)) { + return false; + } + File surveysDir = AppDirs.surveysDir(context); + if (surveysDir.exists() && !backupSurveysDir()) { + return false; + } + File infoFile = new File(unzippedDir, BackupGenerator.INFO_FILE_NAME); + infoFile.delete(); + File unzippedSurveysDir = new File(unzippedDir, BackupGenerator.SURVEYS_DIR); + FileUtils.moveDirectory(unzippedSurveysDir, surveysDir); + AndroidFiles.makeDiscoverable(surveysDir, context); + return true; + } else { + showWarning(R.string.backup_not_enough_space_working_directory); + return false; + } + } finally { + if (zipFile != null) zipFile.delete(); + if (unzippedDir != null) FileUtils.deleteDirectory(unzippedDir); + } + } + + private boolean checkBackupFile(File unzippedDir) throws IOException { + File infoPropertiesFile = new File(unzippedDir, BackupGenerator.INFO_FILE_NAME); + if (!infoPropertiesFile.exists()) { + showWarning(R.string.restore_error_invalid_backup_file); + return false; + } + BackupInfo backupInfo = BackupInfo.parse(new FileInputStream(infoPropertiesFile)); + if (backupInfo.getCollectMobileVersion().compareTo(App.version(context), Version.Significance.MAJOR) < 0) { + showWarning(R.string.restore_error_backup_file_generated_with_newer_version); + return false; + } + File surveysDir = new File(unzippedDir, BackupGenerator.SURVEYS_DIR); + if (!surveysDir.exists() || surveysDir.list().length == 0) { + showWarning(R.string.restore_error_empty_surveys_folder); + return false; + } + return true; + } + + private boolean backupSurveysDir() throws IOException { + File surveysDir = AppDirs.surveysDir(context); + SnapshotsManager snapshotsManager = new SnapshotsManager(surveysDir); + File snapshotSurveysDir = snapshotsManager.newSnapshotDir(); + if (AndroidFiles.enoughSpaceToCopy(surveysDir, snapshotSurveysDir)) { + FileUtils.moveDirectory(surveysDir, snapshotSurveysDir); + AndroidFiles.makeDiscoverable(snapshotSurveysDir, context); + return true; + } else { + showWarning(R.string.backup_not_enough_space_working_directory); + return false; + } + } + + @Override + protected void onPostExecute(Boolean result) { + super.onPostExecute(result); + if (result != null && result.booleanValue()) { + BaseActivity.restartMainActivity(context); + } else { + String errorDetails = lastException == null ? null : lastException.getMessage(); + showError(R.string.restore_error, errorDetails); + } + } + } + + public static interface RestoreFileSelectedListener { + void fileSelected(Uri fileUri); + } +} \ No newline at end of file diff --git a/android/src/main/java/org/openforis/collect/android/gui/detail/ExportDialogFragment.java b/android/src/main/java/org/openforis/collect/android/gui/detail/ExportDialogFragment.java index 77f1d1ed..5b07457d 100644 --- a/android/src/main/java/org/openforis/collect/android/gui/detail/ExportDialogFragment.java +++ b/android/src/main/java/org/openforis/collect/android/gui/detail/ExportDialogFragment.java @@ -85,7 +85,7 @@ public void onClick(DialogInterface dialog, int which) { private static void openShareOrSaveDialog(final Activity activity, final SurveyDataExportParameters exportParameters) { List options = new ArrayList(Arrays.asList( - activity.getString(R.string.export_dialog_option_share), + activity.getString(R.string.share_file), activity.getString(R.string.export_dialog_option_save_to_downloads) )); final int[] checkedItem = {0}; @@ -126,10 +126,7 @@ protected File runTask() throws Exception { File exportedFile = surveyService.exportSurvey(AppDirs.surveysDir(context), parameters); AndroidFiles.makeDiscoverable(exportedFile, context); if (parameters.saveToDownloads) { - File downloadDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS); - if (!downloadDir.exists() && !downloadDir.mkdirs()) { - throw new IOException("Cannot create Download folder: " + downloadDir.getAbsolutePath()); - } + File downloadDir = AndroidFiles.getDownloadsDir(context); File downloadDirDestinationFile = new File(downloadDir, exportedFile.getName()); IOUtils.copy(new FileInputStream(exportedFile), new FileOutputStream(downloadDirDestinationFile)); AndroidFiles.makeDiscoverable(downloadDirDestinationFile, context); diff --git a/android/src/main/java/org/openforis/collect/android/gui/input/AudioFileAttributeComponent.java b/android/src/main/java/org/openforis/collect/android/gui/input/AudioFileAttributeComponent.java index 4a00b444..f1fdb961 100644 --- a/android/src/main/java/org/openforis/collect/android/gui/input/AudioFileAttributeComponent.java +++ b/android/src/main/java/org/openforis/collect/android/gui/input/AudioFileAttributeComponent.java @@ -89,7 +89,7 @@ private void setupRecorder() { mediaRecorder.setOutputFile(file.getPath()); mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC); mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP); - mediaRecorder.setAudioEncoder(MediaRecorder.OutputFormat.AMR_NB); + mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB); mediaRecorder.prepare(); } catch (IOException e) { Log.e("CollectAudioRecorder", "Error setting up media recorder", e); @@ -206,17 +206,22 @@ private void selectFile() { public void audioSelected(Uri uri) { try { File selectedFile = AndroidFiles.copyUriContentToCache(context, uri); - if (selectedFile != null && "3gp".equalsIgnoreCase(FilenameUtils.getExtension(selectedFile.getName()))) { + if ("3gp".equalsIgnoreCase(FilenameUtils.getExtension(selectedFile.getName()))) { reset(); FileUtils.copyFile(selectedFile, file); fileChanged(); } else { - Dialogs.alert(context, R.string.warning, R.string.file_attribute_audio_wrong_file_type_selected); + showWrongAudioFileSelectedMessage(); } } catch (Exception e) { + showWrongAudioFileSelectedMessage(); } } + private void showWrongAudioFileSelectedMessage() { + Dialogs.alert(context, R.string.warning, R.string.file_attribute_audio_wrong_file_type_selected); + } + private void resetRecordingChronometer() { recordingChronometer.setBase(SystemClock.elapsedRealtime()); } diff --git a/android/src/main/java/org/openforis/collect/android/gui/input/DocumentFileAttributeComponent.java b/android/src/main/java/org/openforis/collect/android/gui/input/DocumentFileAttributeComponent.java index f71d111f..cc8f6c3c 100644 --- a/android/src/main/java/org/openforis/collect/android/gui/input/DocumentFileAttributeComponent.java +++ b/android/src/main/java/org/openforis/collect/android/gui/input/DocumentFileAttributeComponent.java @@ -110,12 +110,10 @@ protected void showGallery() { public void documentSelected(Uri uri) { try { File selectedFile = AndroidFiles.copyUriContentToCache(context, uri); - if (selectedFile != null) { - String extension = FilenameUtils.getExtension(selectedFile.getName()); - file = Files.changeExtension(file, extension); - FileUtils.copyFile(selectedFile, file); - fileChanged(); - } + String extension = FilenameUtils.getExtension(selectedFile.getName()); + file = Files.changeExtension(file, extension); + FileUtils.copyFile(selectedFile, file); + fileChanged(); } catch (Exception e) { Toast.makeText(context, context.getString(R.string.file_attribute_file_select_error, e.getMessage()), Toast.LENGTH_LONG).show(); } diff --git a/android/src/main/java/org/openforis/collect/android/gui/input/FileAttributeComponent.java b/android/src/main/java/org/openforis/collect/android/gui/input/FileAttributeComponent.java index 9bdf3296..14347465 100644 --- a/android/src/main/java/org/openforis/collect/android/gui/input/FileAttributeComponent.java +++ b/android/src/main/java/org/openforis/collect/android/gui/input/FileAttributeComponent.java @@ -7,6 +7,7 @@ import androidx.fragment.app.FragmentActivity; import org.openforis.collect.android.SurveyService; +import org.openforis.collect.android.gui.util.Activities; import org.openforis.collect.android.gui.util.AndroidFiles; import org.openforis.collect.android.viewmodel.UiFileAttribute; @@ -52,23 +53,11 @@ protected void removeFile() { } protected void startFileChooserActivity(String title, int requestCode, String type, String... extraMimeTypes) { - Intent intent = createFileSelectorIntent(type, extraMimeTypes); - context.startActivityForResult(Intent.createChooser(intent, title), requestCode); + Activities.startFileChooserActivity(context, title, requestCode, type, extraMimeTypes); } protected boolean canStartFileChooserActivity(String type) { - Intent intent = createFileSelectorIntent(type); - return intent.resolveActivity(context.getPackageManager()) != null; - } - - private Intent createFileSelectorIntent(String type, String... extraMimeTypes) { - Intent intent = new Intent(); - intent.setType(type); - intent.setAction(Intent.ACTION_GET_CONTENT); - if (extraMimeTypes != null && extraMimeTypes.length > 0) { - intent.putExtra(Intent.EXTRA_MIME_TYPES, extraMimeTypes); - } - return intent; + return Activities.canStartFileChooserActivity(context, type); } protected void startShowFileActivity() { diff --git a/android/src/main/java/org/openforis/collect/android/gui/settings/SettingsActivity.java b/android/src/main/java/org/openforis/collect/android/gui/settings/SettingsActivity.java index d0352119..3f50894d 100644 --- a/android/src/main/java/org/openforis/collect/android/gui/settings/SettingsActivity.java +++ b/android/src/main/java/org/openforis/collect/android/gui/settings/SettingsActivity.java @@ -1,7 +1,7 @@ package org.openforis.collect.android.gui.settings; -import android.app.Activity; import android.content.Context; +import android.content.Intent; import android.content.SharedPreferences; import android.content.res.Resources; import android.os.Bundle; @@ -11,6 +11,7 @@ import android.preference.PreferenceManager; import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; import org.apache.commons.lang3.StringUtils; import org.openforis.collect.R; @@ -19,6 +20,8 @@ import org.openforis.collect.android.gui.SurveyNodeActivity; import org.openforis.collect.android.gui.ThemeInitializer; import org.openforis.collect.android.gui.UILanguageInitializer; +import org.openforis.collect.android.gui.backup.Backup; +import org.openforis.collect.android.gui.backup.Restore; import org.openforis.collect.android.util.MessageSources; import org.openforis.collect.android.util.Permissions; import org.openforis.collect.manager.MessageSource; @@ -39,7 +42,7 @@ /** * @author Daniel Wiell */ -public class SettingsActivity extends Activity { +public class SettingsActivity extends AppCompatActivity { public static final String LANGUAGES_RESOURCE_BUNDLE_NAME = "org/openforis/collect/resourcebundles/languages"; private static final MessageSource LANGUAGE_MESSAGE_SOURCE = new LanguagesResourceBundleMessageSource(); @@ -61,6 +64,14 @@ public class SettingsActivity extends Activity { public static final String REMOTE_COLLECT_PASSWORD = "remoteCollectPassword"; public static final String REMOTE_COLLECT_TEST = "remoteCollectTest"; + public static final String BACKUP = "backup"; + + public static final String RESTORE = "restore"; + + public static final int RESTORE_FILE_SELECTED_REQUEST_CODE = 6400; + + private Restore.RestoreFileSelectedListener restoreFileSelectedListener; + public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); ThemeInitializer.init(this); @@ -93,6 +104,23 @@ public void onBackPressed() { super.onBackPressed(); } + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + if (resultCode == RESULT_OK) { + switch (requestCode) { + case RESTORE_FILE_SELECTED_REQUEST_CODE: + if (restoreFileSelectedListener != null) { + restoreFileSelectedListener.fileSelected(data.getData()); + } + } + } + super.onActivityResult(requestCode, resultCode, data); + } + + public void setRestoreFileSelectedListener(Restore.RestoreFileSelectedListener restoreFileSelectedListener) { + this.restoreFileSelectedListener = restoreFileSelectedListener; + } + public static class SettingsFragment extends PreferenceFragment { public void onCreate(Bundle savedInstanceState) { @@ -113,6 +141,8 @@ public void onCreate(Bundle savedInstanceState) { setupRemoteCollectUsernamePreference(); setupRemoteCollectPasswordPreference(); setupRemoteCollectConnectionTestPreference(); + setupBackupPreference(); + setupRestorePreference(); } private void setupCrewIdPreference() { @@ -335,6 +365,26 @@ public boolean onPreferenceClick(Preference preference) { }); } + private void setupBackupPreference() { + Preference preference = findPreference(BACKUP); + preference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { + public boolean onPreferenceClick(Preference preference) { + Backup.showBackupModeChooseDialog((AppCompatActivity) getActivity()); + return false; + } + }); + } + + private void setupRestorePreference() { + Preference preference = findPreference(RESTORE); + preference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { + public boolean onPreferenceClick(Preference preference) { + Restore.confirmRestore(getActivity()); + return false; + } + }); + } + private void handleLanguageChanged(Context context) { UILanguageInitializer.init(context); ServiceLocator.resetModelManager(context); diff --git a/android/src/main/java/org/openforis/collect/android/gui/settings/WorkingDirectoryPreferenceInitializer.java b/android/src/main/java/org/openforis/collect/android/gui/settings/WorkingDirectoryPreferenceInitializer.java index 90d27412..e9ab7b80 100644 --- a/android/src/main/java/org/openforis/collect/android/gui/settings/WorkingDirectoryPreferenceInitializer.java +++ b/android/src/main/java/org/openforis/collect/android/gui/settings/WorkingDirectoryPreferenceInitializer.java @@ -8,12 +8,12 @@ import android.preference.PreferenceFragment; import android.preference.PreferenceManager; +import androidx.annotation.NonNull; + import com.codekidlabs.storagechooser.StorageChooser; import org.openforis.collect.R; -import org.openforis.collect.android.gui.MainActivity; -import org.openforis.collect.android.gui.ServiceLocator; -import org.openforis.collect.android.gui.util.Activities; +import org.openforis.collect.android.gui.BaseActivity; import org.openforis.collect.android.gui.util.AndroidFiles; import org.openforis.collect.android.gui.util.AppDirs; import org.openforis.collect.android.gui.util.Dialogs; @@ -22,8 +22,6 @@ import java.util.ArrayList; import java.util.List; -import androidx.annotation.NonNull; - class WorkingDirectoryPreferenceInitializer { private static final String PREFERENCE_WORKING_DIR_LOCATION = "workingDirLocation"; @@ -131,10 +129,7 @@ public void run() { editor.apply(); - // reset service locator and restart main activity - ServiceLocator.reset(activity); - Activities.startNewClearTask(activity, MainActivity.class); - activity.finish(); + BaseActivity.restartMainActivity(activity); } }); } diff --git a/android/src/main/java/org/openforis/collect/android/gui/util/Activities.java b/android/src/main/java/org/openforis/collect/android/gui/util/Activities.java index ffdfb1f6..1cf627cf 100644 --- a/android/src/main/java/org/openforis/collect/android/gui/util/Activities.java +++ b/android/src/main/java/org/openforis/collect/android/gui/util/Activities.java @@ -94,4 +94,24 @@ public static void shareFile(Context context, File file, String contentType, int context.startActivity(Intent.createChooser(intent, context.getText(messageKey))); } + + public static void startFileChooserActivity(Activity context, String title, int requestCode, String type, String... extraMimeTypes) { + Intent intent = createFileSelectorIntent(type, extraMimeTypes); + context.startActivityForResult(Intent.createChooser(intent, title), requestCode); + } + + public static boolean canStartFileChooserActivity(Activity context, String type) { + Intent intent = createFileSelectorIntent(type); + return intent.resolveActivity(context.getPackageManager()) != null; + } + + private static Intent createFileSelectorIntent(String type, String... extraMimeTypes) { + Intent intent = new Intent(); + intent.setType(type); + intent.setAction(Intent.ACTION_GET_CONTENT); + if (extraMimeTypes != null && extraMimeTypes.length > 0) { + intent.putExtra(Intent.EXTRA_MIME_TYPES, extraMimeTypes); + } + return intent; + } } diff --git a/android/src/main/java/org/openforis/collect/android/gui/util/AndroidFiles.java b/android/src/main/java/org/openforis/collect/android/gui/util/AndroidFiles.java index 31e9d543..64dfd4f0 100644 --- a/android/src/main/java/org/openforis/collect/android/gui/util/AndroidFiles.java +++ b/android/src/main/java/org/openforis/collect/android/gui/util/AndroidFiles.java @@ -6,6 +6,7 @@ import android.database.Cursor; import android.media.MediaScannerConnection; import android.net.Uri; +import android.os.Environment; import android.os.ParcelFileDescriptor; import android.os.StatFs; import android.provider.MediaStore; @@ -17,6 +18,8 @@ import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; import org.apache.commons.io.filefilter.TrueFileFilter; +import org.openforis.collect.R; +import org.openforis.collect.android.gui.WorkingDirNotAccessible; import java.io.File; import java.io.FileInputStream; @@ -54,6 +57,12 @@ private static void makeDirectoryDiscoverable(File dir, Context context) { } } + public static void createAndMakeDiscoverableDir(File dir, Context context) { + if (!dir.mkdirs()) + throw new WorkingDirNotAccessible(dir); + makeDiscoverable(dir, context); + } + public static Uri getUriForFile(Context context, File file) { return FileProvider.getUriForFile(context, FILE_PROVIDER_AUTHORITY, file); } @@ -98,17 +107,21 @@ public static boolean copyUriContentToFile(Context context, Uri uri, File file) } } - public static File copyUriContentToCache(Context context, Uri uri) { + public static File copyUriContentToCache(Context context, Uri uri) throws Exception { try { ParcelFileDescriptor fileDescriptor = context.getContentResolver().openFileDescriptor(uri, "r"); - if (fileDescriptor == null) return null; + if (fileDescriptor == null) throw new Exception("Error "); String fileName = getUriContentFileName(context, uri); InputStream is = new FileInputStream(fileDescriptor.getFileDescriptor()); File fileCache = new File(context.getCacheDir(), fileName); IOUtils.copy(is, new FileOutputStream(fileCache)); + if (fileCache.length() == 0) { + throw new IllegalStateException(context.getString(R.string.survey_import_failed_empty_file_message)); + } return fileCache; } catch (Exception e) { - return null; + throw new Exception(String.format( + "Error copying file; could not determine file path for URI: %s", uri), e); } } @@ -147,7 +160,7 @@ public static long availableSpaceMB(File path) { } public static boolean enoughSpaceToCopy(File fromPath, File toPath) { - return FileUtils.sizeOfDirectory(fromPath) < availableSize(toPath); + return (fromPath.isDirectory() ? FileUtils.sizeOfDirectory(fromPath) : fromPath.length()) < availableSize(toPath); } private static File firstExistingAncestorOrSelf(File file) { @@ -157,4 +170,12 @@ private static File firstExistingAncestorOrSelf(File file) { } return current; } + + public static File getDownloadsDir(Context context) throws IOException { + File downloadDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS); + if (!downloadDir.exists() && !downloadDir.mkdirs()) { + throw new IOException(context.getResources().getString(R.string.error_cannot_create_downloads_folder, downloadDir.getAbsolutePath())); + } + return downloadDir; + } } \ No newline at end of file diff --git a/android/src/main/java/org/openforis/collect/android/gui/util/App.java b/android/src/main/java/org/openforis/collect/android/gui/util/App.java index 22f5cdf7..7ae5ef30 100644 --- a/android/src/main/java/org/openforis/collect/android/gui/util/App.java +++ b/android/src/main/java/org/openforis/collect/android/gui/util/App.java @@ -6,6 +6,8 @@ import androidx.annotation.Nullable; +import org.openforis.commons.versioning.Version; + /** * @author Stefano Ricci */ @@ -25,6 +27,10 @@ public static String versionFull(Context context) { return versionName(context) + " [" + versionCode(context) + "]"; } + public static Version version(Context context) { + return new Version(versionName(context)); + } + @Nullable private static PackageInfo getPackageInfo(Context context) { PackageManager manager = context.getPackageManager(); diff --git a/android/src/main/java/org/openforis/collect/android/gui/util/AppDirs.java b/android/src/main/java/org/openforis/collect/android/gui/util/AppDirs.java index 9137ea89..981ce6fa 100644 --- a/android/src/main/java/org/openforis/collect/android/gui/util/AppDirs.java +++ b/android/src/main/java/org/openforis/collect/android/gui/util/AppDirs.java @@ -9,7 +9,7 @@ import androidx.core.content.ContextCompat; -import org.openforis.collect.android.gui.WorkingDirNotWritable; +import org.openforis.collect.android.gui.WorkingDirNotAccessible; import java.io.File; import java.util.ArrayList; @@ -18,10 +18,12 @@ import java.util.List; public abstract class AppDirs { + + public static final String SURVEYS_DIR_NAME = "surveys"; public static final String PREFERENCE_KEY = "workingDir"; private static final String ENV_SECONDARY_STORAGE = "SECONDARY_STORAGE"; - public static File root(Context context) throws WorkingDirNotWritable { + public static File root(Context context) throws WorkingDirNotAccessible { File workingDir = readFromPreference(context); if (workingDir == null) { workingDir = defaultWorkingDir(context); @@ -29,11 +31,9 @@ public static File root(Context context) throws WorkingDirNotWritable { } if (!workingDir.exists()) { - if (!workingDir.mkdirs()) - throw new WorkingDirNotWritable(workingDir); - AndroidFiles.makeDiscoverable(workingDir, context); - } else if (!workingDir.canWrite()) - throw new WorkingDirNotWritable(workingDir); + AndroidFiles.createAndMakeDiscoverableDir(workingDir, context); + } else if (!workingDir.canRead() || !workingDir.canWrite()) + throw new WorkingDirNotAccessible(workingDir); Log.i("CollectMobile", "Working dir: " + workingDir); return workingDir; } @@ -43,15 +43,15 @@ public static String rootAbsolutePath(Context context) { return root == null ? "" : root.getAbsolutePath(); } - public static File surveyDatabasesDir(String surveyName, Context context) throws WorkingDirNotWritable { + public static File surveyDatabasesDir(String surveyName, Context context) throws WorkingDirNotAccessible { return new File(surveysDir(context), surveyName); } - public static File surveysDir(Context context) throws WorkingDirNotWritable { - return new File(root(context), "surveys"); + public static File surveysDir(Context context) throws WorkingDirNotAccessible { + return new File(root(context), SURVEYS_DIR_NAME); } - public static File surveyImagesDir(String surveyName, Context context) throws WorkingDirNotWritable { + public static File surveyImagesDir(String surveyName, Context context) throws WorkingDirNotAccessible { return new File(surveyDatabasesDir(surveyName, context), "collect_upload"); } diff --git a/android/src/main/java/org/openforis/collect/android/gui/util/Dates.java b/android/src/main/java/org/openforis/collect/android/gui/util/Dates.java index f1b89600..251a9567 100644 --- a/android/src/main/java/org/openforis/collect/android/gui/util/Dates.java +++ b/android/src/main/java/org/openforis/collect/android/gui/util/Dates.java @@ -7,8 +7,16 @@ public abstract class Dates { public static final SimpleDateFormat FORMAT_FULL = new SimpleDateFormat("dd MMMMM yyyy (HH:mm:ss)", Locale.ENGLISH); - + public static final SimpleDateFormat FORMAT_ISO = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.ENGLISH); public static String formatFull(Date date) { return FORMAT_FULL.format(date); } + + public static String formatISO(Date date) { + return FORMAT_ISO.format(date); + } + + public static String formatNowISO() { + return formatISO(new Date()); + } } diff --git a/android/src/main/java/org/openforis/collect/android/gui/util/SimpleSlowJob.java b/android/src/main/java/org/openforis/collect/android/gui/util/SimpleSlowJob.java new file mode 100644 index 00000000..2e7be02c --- /dev/null +++ b/android/src/main/java/org/openforis/collect/android/gui/util/SimpleSlowJob.java @@ -0,0 +1,13 @@ +package org.openforis.collect.android.gui.util; + +import android.app.Activity; + +/** + * @author Stefano Ricci + */ + +public class SimpleSlowJob extends SlowJob { + public SimpleSlowJob(Activity context, Runnable runnable, ExceptionHandler exceptionHandler, int progressDialogTitleResId, int progressDialogMessageResId) { + super(context, runnable, exceptionHandler, progressDialogTitleResId, progressDialogMessageResId); + } +} \ No newline at end of file diff --git a/android/src/main/java/org/openforis/collect/android/gui/util/SlowAsyncTask.java b/android/src/main/java/org/openforis/collect/android/gui/util/SlowAsyncTask.java index da1c9761..f78120f7 100644 --- a/android/src/main/java/org/openforis/collect/android/gui/util/SlowAsyncTask.java +++ b/android/src/main/java/org/openforis/collect/android/gui/util/SlowAsyncTask.java @@ -101,6 +101,15 @@ protected void handleException(Exception e) { } } + protected void showWarning(final int messageKey) { + context.runOnUiThread(new Runnable() { + @Override + public void run() { + Dialogs.alert(context, R.string.warning, messageKey); + } + }); + } + public interface ExceptionHandler { void handle(Exception e); } diff --git a/android/src/main/java/org/openforis/collect/android/gui/util/SlowJob.java b/android/src/main/java/org/openforis/collect/android/gui/util/SlowJob.java new file mode 100644 index 00000000..14e9c291 --- /dev/null +++ b/android/src/main/java/org/openforis/collect/android/gui/util/SlowJob.java @@ -0,0 +1,112 @@ +package org.openforis.collect.android.gui.util; + +import android.app.Activity; +import android.app.ProgressDialog; + +import org.openforis.collect.R; + +public class SlowJob { + + enum Status { + PENDING, RUNNING, COMPLETED, ERROR + } + + protected final Activity context; + private final Runnable runnable; + private final int progressDialogTitleResId; + private final int progressDialogMessageResId; + private final ExceptionHandler exceptionHandler; + + protected Status status = Status.PENDING; + private ProgressDialog progressDialog; + protected Exception lastException; + + public SlowJob(Activity context, Runnable runnable, int progressDialogTitleResId, int progressDialogMessageResId) { + this(context, runnable, null, progressDialogTitleResId, progressDialogMessageResId); + } + + public SlowJob(Activity context, Runnable runnable, ExceptionHandler exceptionHandler, int progressDialogTitleResId, int progressDialogMessageResId) { + super(); + this.context = context; + this.runnable = runnable; + this.exceptionHandler = exceptionHandler; + this.progressDialogTitleResId = progressDialogTitleResId; + this.progressDialogMessageResId = progressDialogMessageResId; + } + + public void execute() { + onPreExecute(); + + Result result = doInBackground(); + + onPostExecute(result); + } + + protected void onPreExecute() { + progressDialog = ProgressDialog.show(context, context.getString(progressDialogTitleResId), + context.getString(progressDialogMessageResId), true); + } + + protected Result doInBackground(Params... params) { + status = Status.RUNNING; + try { + Result result = runTask(); + status = Status.COMPLETED; + return result; + } catch (Exception e) { + lastException = e; + status = Status.ERROR; + } + return null; + } + + protected Result runTask() throws Exception { + if (runnable != null) { + runnable.run(); + } + return null; + } + + protected void onPostExecute(Result result) { + if (progressDialog != null) { + progressDialog.dismiss(); + progressDialog = null; + } + + if (status == Status.ERROR) { + handleException(lastException); + } + } + + protected void handleException(Exception e) { + if (exceptionHandler != null) { + exceptionHandler.handle(lastException); + } + } + + protected void showInfo(int messageKey, Object ...messageArgs) { + showMessage(R.string.info, messageKey, messageArgs); + } + + protected void showWarning(int messageKey, Object ...messageArgs) { + showMessage(R.string.warning, messageKey, messageArgs); + } + + protected void showError(int messageKey, Object ...messageArgs) { + showMessage(R.string.error, messageKey, messageArgs); + } + + protected void showMessage(final int titleKey, final int messageKey, final Object ...messageArgs) { + context.runOnUiThread(new Runnable() { + @Override + public void run() { + String message = context.getString(messageKey, messageArgs); + Dialogs.alert(context, titleKey, message); + } + }); + } + + public interface ExceptionHandler { + void handle(Exception e); + } +} diff --git a/android/src/main/java/org/openforis/collect/android/gui/util/Tasks.java b/android/src/main/java/org/openforis/collect/android/gui/util/Tasks.java index 1a2dcc89..4e39e79e 100644 --- a/android/src/main/java/org/openforis/collect/android/gui/util/Tasks.java +++ b/android/src/main/java/org/openforis/collect/android/gui/util/Tasks.java @@ -17,12 +17,33 @@ public static void runSlowTask(Activity context, Runnable runnable, int progress runSlowTask(context, runnable, new DefaultExceptionHandler(context), progressDialogTitleResId, progressDialogMessageResId); } - public static void runSlowTask(Activity context, Runnable runnable, SlowAsyncTask.ExceptionHandler exceptionHandler, int progressDialogTitleResId, + public static void runSlowTask(Activity context, final Runnable runnable, SlowAsyncTask.ExceptionHandler exceptionHandler, int progressDialogTitleResId, int progressDialogMessageResId) { +// ExecutorService executor = Executors.newSingleThreadExecutor(); +// final Handler handler = new Handler(Looper.getMainLooper()); +// executor.execute(new Runnable() { +// @Override +// public void run() { +// handler.post(new Runnable() { +// @Override +// public void run() { +// +// runnable.run(); +// } +// }); +// } +// }); +// + new SimpleSlowAsyncTask(context, runnable, exceptionHandler, progressDialogTitleResId, progressDialogMessageResId) .execute(); } + public static void runSlowJob(Activity context, final Runnable runnable, SlowJob.ExceptionHandler exceptionHandler, int progressDialogTitleResId, + int progressDialogMessageResId) { + new SimpleSlowJob(context, runnable, exceptionHandler, progressDialogTitleResId, progressDialogMessageResId).execute(); + } + public static Handler runDelayed(final Runnable runnable, int delay) { Handler handler = new Handler(); handler.postDelayed(runnable, delay); diff --git a/android/src/main/res/menu/node_activity_actions.xml b/android/src/main/res/menu/node_activity_actions.xml index 43872310..6f671bac 100644 --- a/android/src/main/res/menu/node_activity_actions.xml +++ b/android/src/main/res/menu/node_activity_actions.xml @@ -12,12 +12,6 @@ android:icon="?attr/helpIcon" app:showAsAction="never" android:onClick="openSurveyGuide" /> - Encuesta copiada a %s Error al crear copia de seguridad: %s - Almacenamiento secundario no encontrado + Almacenamiento secundario no encontrado Probar de nuevo Configurar ubicación diff --git a/android/src/main/res/values-fr/strings.xml b/android/src/main/res/values-fr/strings.xml index 7c193180..8cc0f9e9 100644 --- a/android/src/main/res/values-fr/strings.xml +++ b/android/src/main/res/values-fr/strings.xml @@ -28,7 +28,7 @@ Questionnaire sauvegardé sur %s Sauvegarde échouée: %s - Stockage secondaire non trouvée + Stockage secondaire non trouvée Veuillez réessayer Veuillez configurez l’emplacement diff --git a/android/src/main/res/values-ru/strings.xml b/android/src/main/res/values-ru/strings.xml index 4183ade1..2d0fc662 100644 --- a/android/src/main/res/values-ru/strings.xml +++ b/android/src/main/res/values-ru/strings.xml @@ -50,7 +50,7 @@ Резервное копирование не выполнено: %s Данный атрибут не доступен - Вторичное хранилище не найдено + Вторичное хранилище не найдено Повторить попытку Настроить локацию diff --git a/android/src/main/res/values-sq/strings.xml b/android/src/main/res/values-sq/strings.xml index b1b8ebc6..538652fa 100644 --- a/android/src/main/res/values-sq/strings.xml +++ b/android/src/main/res/values-sq/strings.xml @@ -51,7 +51,7 @@ Kopja Rezerve Deshtoi: %s Ky Atribut nuk eshte i Pershtatshem - Nuk u Gjet Memorie Shtese + Nuk u Gjet Memorie Shtese Provoje Perseri Konfiguro Pozicionin diff --git a/android/src/main/res/values-sv/strings.xml b/android/src/main/res/values-sv/strings.xml index 0ab11ab6..63c977f0 100644 --- a/android/src/main/res/values-sv/strings.xml +++ b/android/src/main/res/values-sv/strings.xml @@ -21,7 +21,7 @@ Säkerhetskopierade till %s Misslyckades att säkerhetskopiera: %s - Kan inte hitta lagringsplatsen + Kan inte hitta lagringsplatsen Försök igen Konfigurara lagringsplatsen diff --git a/android/src/main/res/values/array.xml b/android/src/main/res/values/array.xml index 12e14c29..a4533511 100644 --- a/android/src/main/res/values/array.xml +++ b/android/src/main/res/values/array.xml @@ -38,6 +38,7 @@ @string/backup_mode_new_sdcard - @string/backup_mode_internal + @string/backup_mode_save_to_downloads + @string/backup_mode_share \ No newline at end of file diff --git a/android/src/main/res/values/strings.xml b/android/src/main/res/values/strings.xml index 16b2b67b..cbe76591 100644 --- a/android/src/main/res/values/strings.xml +++ b/android/src/main/res/values/strings.xml @@ -30,6 +30,7 @@ Warning Error + Share (by email, DropBox, Google Drive, etc.) Low device storage Collect @@ -55,16 +56,35 @@ About Exit + Backup / Restore + Backup + Tap to generate a full backup of the application Choose backup mode Into NEW SD card Internal + Save to \"Download\" folder + Share Not enough space in internal memory to complete the backup process - Not enough space in working directory to complete the backup process + Not enough space in working directory to complete the process Please insert SD card to store backup on Backed up survey to %s Backup failed: %s Delete oldest snapshot of the %s and try again? Failed to delete the oldest snapshot: %s + Backup file generation complete + Backup file generated into \"Download\" folder + Send backup file to + Restore + Tap to start a full data restore process + Restoring backup file + Invalid backup file selected; only valid Collect Mobile Backup files (with extension .ofcmbck) can be selected + Backup file generated with a newer version of Collect Mobile + Empty surveys folder in backup file; nothing could be restored. + Confirm data restore + Please select a valid backup file (with .ofcmbck extension) to restore. The data restore process will DELETE ALL EXISTING DATA. Continue? + ALL EXISTING DATA WILL BE DELETED and the selected backup file will be restored. Continue? + Data restored successfully! + Error restoring data: %s Exported survey to %s Failed to export survey: %s @@ -85,7 +105,7 @@ Error opening Survey Guide file: %s Survey guide not found in the survey - Secondary storage not found + Working directory not accessible Retry Configure location @@ -196,8 +216,7 @@ Only selected records Exclude image files Exclude calculated attribute values - Share (by email, DropBox, Google Drive, etc.) - Save to \"Download\" directory + Save to \"Download\" folder Exporting data Send exported data to Data export completed @@ -287,5 +306,8 @@ audio record camera to acquire the barcode + + Cannot create \"Download\" folder with path %s + Exit from Collect Mobile?\n\n(data has been automatically saved already) diff --git a/android/src/main/res/xml/preferences.xml b/android/src/main/res/xml/preferences.xml index 524206a3..ce9b93ed 100644 --- a/android/src/main/res/xml/preferences.xml +++ b/android/src/main/res/xml/preferences.xml @@ -97,4 +97,19 @@ android:summary="@string/settings_remote_sync_test_summary" android:title="@string/settings_remote_sync_test" /> + + + + + \ No newline at end of file diff --git a/model/src/main/java/org/openforis/collect/android/collectadapter/BackupGenerator.java b/model/src/main/java/org/openforis/collect/android/collectadapter/BackupGenerator.java new file mode 100644 index 00000000..fe9841ca --- /dev/null +++ b/model/src/main/java/org/openforis/collect/android/collectadapter/BackupGenerator.java @@ -0,0 +1,68 @@ +package org.openforis.collect.android.collectadapter; + +import org.apache.commons.io.IOUtils; +import org.openforis.collect.android.util.FileUtils; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.List; +import java.util.logging.Logger; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +public class BackupGenerator { + private static final Logger LOG = Logger.getLogger(BackupGenerator.class.getName()); + + public static String INFO_FILE_NAME = "info.properties"; + public static String SURVEYS_DIR = "surveys"; + + private File surveysDir; + private String appVersion; + private File destFile; + private ZipOutputStream zipOutputStream; + + public BackupGenerator(File surveysDir, String appVersion, File destFile) { + this.surveysDir = surveysDir; + this.appVersion = appVersion; + this.destFile = destFile; + } + + public void generate() throws IOException { + try { + zipOutputStream = new ZipOutputStream(new FileOutputStream(destFile)); + addInfoFile(); + addSourceFiles(); + } finally { + IOUtils.closeQuietly(zipOutputStream); + } + } + + private void addSourceFiles() throws IOException { + List files = FileUtils.listFilesRecursively(surveysDir); + for (File file: files) { + String filePath = file.getAbsolutePath(); + String entryName = SURVEYS_DIR + "/" + filePath.substring(surveysDir.getAbsolutePath().length() + 1, filePath.length()); + writeFile(file, entryName); + } + } + + private void addInfoFile() throws IOException { + try { + zipOutputStream.putNextEntry(new ZipEntry(INFO_FILE_NAME)); + BackupInfo info = new BackupInfo(appVersion); + info.store(zipOutputStream); + } finally { + zipOutputStream.closeEntry(); + } + } + + private void writeFile(File file, String entryName) throws IOException { + ZipEntry entry = new ZipEntry(entryName); + zipOutputStream.putNextEntry(entry); + IOUtils.copy(new FileInputStream(file), zipOutputStream); + zipOutputStream.closeEntry(); + zipOutputStream.flush(); + } +} diff --git a/model/src/main/java/org/openforis/collect/android/collectadapter/BackupInfo.java b/model/src/main/java/org/openforis/collect/android/collectadapter/BackupInfo.java new file mode 100644 index 00000000..f931d73d --- /dev/null +++ b/model/src/main/java/org/openforis/collect/android/collectadapter/BackupInfo.java @@ -0,0 +1,88 @@ +package org.openforis.collect.android.collectadapter; + +import org.openforis.collect.Collect; +import org.openforis.collect.utils.Dates; +import org.openforis.commons.versioning.Version; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Date; +import java.util.Properties; + +/** + * + * @author S. Ricci + * + */ +public class BackupInfo { + + private static final String TIMESTAMP_PROP = "timestamp"; + private static final String DATE_PROP = "date"; + private static final String COLLECT_VERSION_PROP = "collect_version"; + private static final String COLLECT_MOBILE_VERSION_PROP = "collect_mobile_version"; + private Version collectVersion; + private Version collectMobileVersion; + private Date timestamp; + + public BackupInfo(String collectMobileVersion) { + this.collectVersion = Collect.VERSION; + this.collectMobileVersion = collectMobileVersion == null ? null : new Version(collectMobileVersion); + this.timestamp = new Date(); + } + + public void store(OutputStream os) throws IOException { + Properties props = toProperties(); + props.store(os, null); + } + + protected Properties toProperties() { + Properties props = new Properties(); + props.setProperty(COLLECT_VERSION_PROP, collectVersion.toString()); + props.setProperty(COLLECT_MOBILE_VERSION_PROP, collectMobileVersion.toString()); + props.setProperty(TIMESTAMP_PROP, Dates.formatDateTime(timestamp)); + return props; + } + + public static BackupInfo parse(InputStream is) throws IOException { + Properties props = new Properties(); + props.load(is); + return parse(props); + } + + protected static BackupInfo parse(Properties props) { + BackupInfo info = new BackupInfo(null); + info.collectVersion = new Version(props.getProperty(COLLECT_VERSION_PROP)); + info.collectMobileVersion = new Version(props.getProperty(COLLECT_MOBILE_VERSION_PROP)); + String timestampString = props.getProperty(TIMESTAMP_PROP); + if ( timestampString == null ) { + info.timestamp = Dates.parseDate(props.getProperty(DATE_PROP)); + } else { + info.timestamp = Dates.parseDateTime(timestampString); + } + return info; + } + public Date getTimestamp() { + return timestamp; + } + + public void setTimestamp(Date date) { + this.timestamp = date; + } + + public Version getCollectVersion() { + return collectVersion; + } + + public void setCollectVersion(Version collectVersion) { + this.collectVersion = collectVersion; + } + + public Version getCollectMobileVersion() { + return collectMobileVersion; + } + + public void setCollectMobileVersion(Version collectMobileVersion) { + this.collectMobileVersion = collectMobileVersion; + } +} diff --git a/model/src/main/java/org/openforis/collect/android/util/FileUtils.java b/model/src/main/java/org/openforis/collect/android/util/FileUtils.java new file mode 100644 index 00000000..414613b6 --- /dev/null +++ b/model/src/main/java/org/openforis/collect/android/util/FileUtils.java @@ -0,0 +1,36 @@ +package org.openforis.collect.android.util; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Stack; + +public abstract class FileUtils { + + public static List listFilesRecursively(File dir) { + List result = new ArrayList(); + File[] files = dir.listFiles(); + Stack stack = new Stack(); + stack.addAll(Arrays.asList(files)); + while (!stack.isEmpty()) { + File file = stack.pop(); + if (file.isFile()) { + result.add(file); + } else { + stack.addAll(Arrays.asList(file.listFiles())); + } + } + return result; + } + + public static File createTempDir() throws IOException { + File tempDir = File.createTempFile("collect", Long.toString((System.nanoTime()))); + if (!tempDir.delete()) + throw new IOException("Failed to create temp dir:" + tempDir.getAbsolutePath()); + if (!tempDir.mkdir()) + throw new IOException("Failed to create temp dir:" + tempDir.getAbsolutePath()); + return tempDir; + } +} diff --git a/model/src/main/java/org/openforis/collect/android/util/Unzipper.java b/model/src/main/java/org/openforis/collect/android/util/Unzipper.java index 26933fd2..784f75a1 100644 --- a/model/src/main/java/org/openforis/collect/android/util/Unzipper.java +++ b/model/src/main/java/org/openforis/collect/android/util/Unzipper.java @@ -1,5 +1,7 @@ package org.openforis.collect.android.util; +import org.apache.commons.io.FileUtils; + import java.io.*; import java.util.Arrays; import java.util.HashSet; @@ -40,14 +42,30 @@ public void unzip(String... fileNames) throws IOException { } } + public void unzipAll() throws IOException { + ZipInputStream zipInputStream = new ZipInputStream(new FileInputStream(zipFile)); + try { + ZipEntry zipEntry = zipInputStream.getNextEntry(); + while (zipEntry != null) { + // keep folders hierarchy + String entryOutputFileName = zipEntry.getName().replace("/", File.separator); + extractEntry(zipInputStream, entryOutputFileName); + zipEntry = zipInputStream.getNextEntry(); + } + } finally { + zipInputStream.close(); + } + } + private String entryName(ZipEntry zipEntry) { String name = zipEntry.getName(); int i = name.lastIndexOf('/'); // Ignore directories return i == -1 ? name : name.substring(i + 1); } - private void extractEntry(ZipInputStream zipInputStream, String entryName) throws IOException { - File newFile = new File(outputFolder + File.separator + entryName); + private void extractEntry(ZipInputStream zipInputStream, String outputFileName) throws IOException { + File newFile = new File(outputFolder + File.separator + outputFileName); + FileUtils.forceMkdirParent(newFile); OutputStream fos = new FileOutputStream(newFile); write(zipInputStream, fos); }