diff --git a/CollectorAndroid/res/values/strings.xml b/CollectorAndroid/res/values/strings.xml index 9e643e87f..7b6826483 100644 --- a/CollectorAndroid/res/values/strings.xml +++ b/CollectorAndroid/res/values/strings.xml @@ -66,8 +66,9 @@ Additional options Settings Please select a Sapelli file + File could not be found or is not readable. The loading of bare XML files is no longer supported. Please package your project in a .sap file. - Unsupported Sapelli project file extension (%1$s), supported extensions are %2$s. + Unsupported Sapelli project file extension (%1$s), supported extensions are %2$s. Do you want to try loading a project from this file anyway? Loading project … Removing project … Failed to load or store Sapelli project (source: %1$s).\nCause: %2$s @@ -76,6 +77,7 @@ These files could not be found Downloading … Download error. Please check if you are connected to the Internet. + Download error: %s.\nPlease check if you are connected to the Internet. Cannot detect camera to take mandatory picture; cancelling record and exiting project… Cannot detect camera to record mandatory video; cancelling record and exiting project… Cannot detect camera to take picture; skipping field \"%1$s\"… @@ -90,6 +92,7 @@ Finger print Number of forms Model ID + Storage error: %s needs write access to the external/mass storage in order to function. Please insert an SD card and restart the application. Previously used file storage medium has become inaccessible, possibly due to the removal of an SD card or the device being connected to a PC.\nThe application will now exit.\nWhat would you like to happen when you restart it? It can either try using another storage medium, or retry the current medium. In the latter case please first re-insert SD card or disconnect from computer before restarting. Try another … diff --git a/CollectorAndroid/src/uk/ac/ucl/excites/sapelli/collector/activities/BaseActivity.java b/CollectorAndroid/src/uk/ac/ucl/excites/sapelli/collector/activities/BaseActivity.java index cc07757dd..5987b775e 100644 --- a/CollectorAndroid/src/uk/ac/ucl/excites/sapelli/collector/activities/BaseActivity.java +++ b/CollectorAndroid/src/uk/ac/ucl/excites/sapelli/collector/activities/BaseActivity.java @@ -95,7 +95,7 @@ public FileStorageProvider getFileStorageProvider() catch(FileStorageRemovedException e) { Log.e(getClass().getSimpleName(), "Error getting fileStorageProvider", e); - // Inform the user and close the application + // Inform the user and close the application: final Runnable useAlternativeStorage = new Runnable() { @Override @@ -110,12 +110,38 @@ public void run() catch(FileStorageUnavailableException e) { Log.e(getClass().getSimpleName(), "Error getting fileStorageProvider", e); - // Inform the user and close the application + // Inform the user and close the application: showErrorDialog(getString(R.string.app_name) + " " + getString(R.string.needsStorageAccess), true); } } return fileStorageProvider; } + + /** + * @author mstevens + * + */ + public interface FileStorageTask + { + + /** + * @param fsp guaranteed non-null + */ + public void run(FileStorageProvider fsp); + + } + + /** + * @param task + */ + public void runFileStorageTask(FileStorageTask task) + { + FileStorageProvider fsp = getFileStorageProvider(); // if this returns null an error dialog will show and upon OK the activity will finish, ... + if(fsp == null) // ... so abort task in that case... + return; + else + task.run(fsp); + } public void showOKDialog(int titleId, int messageId) { diff --git a/CollectorAndroid/src/uk/ac/ucl/excites/sapelli/collector/activities/ProjectManagerActivity.java b/CollectorAndroid/src/uk/ac/ucl/excites/sapelli/collector/activities/ProjectManagerActivity.java index 77d8f7fd3..b6576efd3 100644 --- a/CollectorAndroid/src/uk/ac/ucl/excites/sapelli/collector/activities/ProjectManagerActivity.java +++ b/CollectorAndroid/src/uk/ac/ucl/excites/sapelli/collector/activities/ProjectManagerActivity.java @@ -58,6 +58,8 @@ import uk.ac.ucl.excites.sapelli.collector.fragments.dialogs.AboutFragment; import uk.ac.ucl.excites.sapelli.collector.fragments.dialogs.EnterURLFragment; import uk.ac.ucl.excites.sapelli.collector.fragments.tabs.MainTabFragment; +import uk.ac.ucl.excites.sapelli.collector.io.FileStorageException; +import uk.ac.ucl.excites.sapelli.collector.io.FileStorageProvider; import uk.ac.ucl.excites.sapelli.collector.load.AndroidProjectLoaderStorer; import uk.ac.ucl.excites.sapelli.collector.load.ProjectLoader; import uk.ac.ucl.excites.sapelli.collector.load.ProjectLoaderStorer; @@ -617,7 +619,14 @@ public boolean importRecords(MenuItem item) */ public boolean backupSapelli(MenuItem item) { - Backup.Run(this, getFileStorageProvider()); + runFileStorageTask(new FileStorageTask() + { + @Override + public void run(FileStorageProvider fsp) + { + Backup.Run(ProjectManagerActivity.this, fsp); + } + }); return true; } @@ -630,37 +639,95 @@ public void runProject(View view) public void loadProject(String path) { - if(path == null || path.isEmpty()) + // Check path: + if(path == null || path.trim().isEmpty()) return; - //else: - String location = path.trim(); - // Download Sapelli file if path is a URL + + final String location = path.trim(); if(Patterns.WEB_URL.matcher(location).matches()) - // Location is a (remote) URL: download Sapelli file: - AsyncDownloader.Download(this, getFileStorageProvider().getSapelliDownloadsFolder(), location, this); // loading & store of the project will happen upon successful download (via callback) - else if(location.toLowerCase().endsWith("." + XML_FILE_EXTENSION)) - // Warn about bare XML file (no longer supported): - showErrorDialog(R.string.noBareXMLProjects); - else - { // loading project from local file: - File localFile = new File(location); - if(ProjectLoader.HasSapelliFileExtension(localFile)) - new AndroidProjectLoaderStorer(this, getFileStorageProvider(), projectStore).loadAndStore(localFile, Uri.fromFile(localFile).toString(), this); - else - showErrorDialog(getString(R.string.unsupportedExtension, FileHelpers.getFileExtension(localFile), StringUtils.join(ProjectLoader.SAPELLI_FILE_EXTENSIONS, ", "))); + { // Location is a (remote) URL: download Sapelli file: + runFileStorageTask(new FileStorageTask() + { + @Override + public void run(FileStorageProvider fsp) + { + try + { // loading & storing of the project will happen upon successful download (via callback) + AsyncDownloader.Download(ProjectManagerActivity.this, fsp.getSapelliDownloadsFolder() /*throws FileStorageException*/, location, ProjectManagerActivity.this); + } + catch(FileStorageException fse) + { + showErrorDialog(getString(R.string.storageError, fse.getMessage())); + } + } + }); } + else + // Load project from local file: + loadProject(new File(location), null, true); } @Override public void downloadSuccess(String downloadUrl, File downloadedFile) { - new AndroidProjectLoaderStorer(this, getFileStorageProvider(), projectStore).loadAndStore(downloadedFile, downloadUrl, this); + loadProject(downloadedFile, downloadUrl, false); // don't check extension for downloaded files } @Override public void downloadFailure(String downloadUrl, Exception cause) { - showErrorDialog(R.string.downloadError, false); + showErrorDialog( + cause != null ? + getString(R.string.downloadErrorWithCause, ExceptionHelpers.getMessage(cause)) : + getString(R.string.downloadError), + false); + } + + /** + * @param localFile + * @param sourceURI - may be null + * @param checkExtension + */ + public void loadProject(final File localFile, final String sourceURI, boolean checkExtension) + { + if(!FileHelpers.isReadableFile(localFile)) + { + showErrorDialog(R.string.invalidFile); + } + else if(checkExtension && XML_FILE_EXTENSION.equalsIgnoreCase(FileHelpers.getFileExtension(localFile))) + { + showErrorDialog(R.string.noBareXMLProjects); + } + else if(checkExtension && !ProjectLoader.HasSapelliFileExtension(localFile)) + { // Warn about extension: + showOKCancelDialog( + R.string.warning, + getString(R.string.unsupportedExtension, FileHelpers.getFileExtension(localFile), StringUtils.join(ProjectLoader.SAPELLI_FILE_EXTENSIONS, ", ")), + false, + new Runnable() + { + @Override + public void run() + { + loadProject(localFile, sourceURI, false); // try loading anyway + } + }, + false); + } + else + { // Actually load & store the project: + runFileStorageTask(new FileStorageTask() + { + @Override + public void run(FileStorageProvider fsp) + { + new AndroidProjectLoaderStorer(ProjectManagerActivity.this, fsp, projectStore).loadAndStore( + localFile, + sourceURI != null ? sourceURI : Uri.fromFile(localFile).toString(), + ProjectManagerActivity.this); + } + }); + } } @Override diff --git a/CollectorAndroid/src/uk/ac/ucl/excites/sapelli/collector/load/AndroidProjectLoaderStorer.java b/CollectorAndroid/src/uk/ac/ucl/excites/sapelli/collector/load/AndroidProjectLoaderStorer.java index e9b421f26..87d4aa382 100644 --- a/CollectorAndroid/src/uk/ac/ucl/excites/sapelli/collector/load/AndroidProjectLoaderStorer.java +++ b/CollectorAndroid/src/uk/ac/ucl/excites/sapelli/collector/load/AndroidProjectLoaderStorer.java @@ -20,6 +20,7 @@ import java.io.File; import java.io.InputStream; +import java.util.List; import uk.ac.ucl.excites.sapelli.collector.R; import uk.ac.ucl.excites.sapelli.collector.db.ProjectStore; @@ -51,9 +52,34 @@ public AndroidProjectLoaderStorer(Context context, FileStorageProvider fileStora * @see uk.ac.ucl.excites.sapelli.collector.load.ProjectLoaderStorer#loadAndStore(java.io.File, java.lang.String, uk.ac.ucl.excites.sapelli.collector.load.ProjectLoaderStorer.Callback) */ @Override - public void loadAndStore(File sapelliFile, String sourceURI, FileSourceCallback callback) + public void loadAndStore(final File sapelliFile, final String sourceURI, final FileSourceCallback callback) { - new AsyncProjectLoadStoreTask(context, sapelliFile, sourceURI, callback).execute(); + // Try opening input stream from file: + InputStream in; + try + { + in = FileHelpers.openInputStream(sapelliFile, true); + } + catch(Exception e) + { + callback.projectLoadStoreFailure(sapelliFile, sourceURI, e); + return; + } + // Input stream successfully opened, try loading project: + new AsyncProjectLoadStoreTask(context, new StreamSourceCallback() + { + @Override + public void projectLoadStoreSuccess(Project project, List warnings) + { + callback.projectLoadStoreSuccess(sapelliFile, sourceURI, project, warnings); + } + + @Override + public void projectLoadStoreFailure(Exception cause) + { + callback.projectLoadStoreFailure(sapelliFile, sourceURI, cause); + } + }).execute(in); } /** @@ -64,54 +90,32 @@ public void loadAndStore(File sapelliFile, String sourceURI, FileSourceCallback @Override public void loadAndStore(InputStream sapelliFileInputStream, StreamSourceCallback callback) { - new AsyncProjectLoadStoreTask(context, sapelliFileInputStream, callback).execute(); + new AsyncProjectLoadStoreTask(context, callback).execute(sapelliFileInputStream); } /** * @author mstevens */ - private class AsyncProjectLoadStoreTask extends AsyncTaskWithWaitingDialog + private class AsyncProjectLoadStoreTask extends AsyncTaskWithWaitingDialog { - private File sapelliFile; - private String sourceURI; - private Callback callback; - private final InputStream sapelliFileInputStream; - + private final StreamSourceCallback callback; private Exception failure; - public AsyncProjectLoadStoreTask(Context context, File sapelliFile, String sourceURI, FileSourceCallback callback) + public AsyncProjectLoadStoreTask(Context context, StreamSourceCallback callback) { super(context, context.getString(R.string.projectLoading)); - this.sapelliFile = sapelliFile; - this.sourceURI = sourceURI; - this.callback = callback; - InputStream in = null; - try - { - in = FileHelpers.openInputStream(sapelliFile, true); - } - catch(Exception e) - { - failure = e; - } - sapelliFileInputStream = in; - } - - public AsyncProjectLoadStoreTask(Context context, InputStream sapelliFileInputStream, StreamSourceCallback callback) - { - super(context, context.getString(R.string.projectLoading)); - this.sapelliFileInputStream = sapelliFileInputStream; this.callback = callback; } @Override - protected Project runInBackground(Void... params) + protected Project runInBackground(InputStream... params) { - if(failure != null || sapelliFileInputStream == null) - return null; try { + InputStream sapelliFileInputStream; + if(params.length < 1 || (sapelliFileInputStream = params[0]) == null) + throw new NullPointerException("Provide a non-null InputStream."); return AndroidProjectLoaderStorer.super.loadAndStore(sapelliFileInputStream); } catch(Exception e) @@ -124,27 +128,15 @@ protected Project runInBackground(Void... params) @Override protected void onPostExecute(Project project) { + // Hide dialog: + super.onPostExecute(project); + // Report back if needed: if(callback == null) return; - if(callback instanceof FileSourceCallback) - { - if(project == null || failure != null) - // Report failure: - ((FileSourceCallback) callback).projectLoadStoreFailure(sapelliFile, sourceURI, failure); - else - // Report success: - ((FileSourceCallback) callback).projectLoadStoreSuccess(sapelliFile, sourceURI, project, loader.getWarnings()); - } - else if(callback instanceof StreamSourceCallback) - { - if(project == null || failure != null) - // Report failure: - ((StreamSourceCallback) callback).projectLoadStoreFailure(failure); - else - // Report success: - ((StreamSourceCallback) callback).projectLoadStoreSuccess(project, loader.getWarnings()); - } - super.onPostExecute(project); + else if(project == null || failure != null) + callback.projectLoadStoreFailure(failure); // report failure + else + callback.projectLoadStoreSuccess(project, loader.getWarnings()); // report success } } diff --git a/Library/src/uk/ac/ucl/excites/sapelli/collector/load/ProjectLoader.java b/Library/src/uk/ac/ucl/excites/sapelli/collector/load/ProjectLoader.java index 929653ce3..abaeab006 100644 --- a/Library/src/uk/ac/ucl/excites/sapelli/collector/load/ProjectLoader.java +++ b/Library/src/uk/ac/ucl/excites/sapelli/collector/load/ProjectLoader.java @@ -20,6 +20,7 @@ import java.io.File; import java.io.FileInputStream; +import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.Collection; @@ -59,9 +60,9 @@ public class ProjectLoader implements WarningKeeper */ static public boolean HasSapelliFileExtension(File file) { - String path = file.getAbsolutePath().toLowerCase(); - for(String extention : ProjectLoader.SAPELLI_FILE_EXTENSIONS) - if(path.endsWith("." + extention)) + String fileExt = FileHelpers.getFileExtension(file); + for(String sapExt : ProjectLoader.SAPELLI_FILE_EXTENSIONS) + if(sapExt.equalsIgnoreCase(fileExt)) return true; return false; } @@ -176,14 +177,12 @@ static public Project ParseProjectXML(InputStream projectXMLInputStream, FormSch private final PostProcessor postProcessor; private List warnings; - private final File tempFolder; private final ProjectParser parser; /** * @param fileStorageProvider - * @throws FileStorageException */ - public ProjectLoader(FileStorageProvider fileStorageProvider) throws FileStorageException + public ProjectLoader(FileStorageProvider fileStorageProvider) { this(fileStorageProvider, null, null); // no post-processing, nor checking } @@ -192,20 +191,14 @@ public ProjectLoader(FileStorageProvider fileStorageProvider) throws FileStorage * @param fileStorageProvider * @param postProcessor (may be null) * @param checker (may be null) - * @throws FileStorageException */ - public ProjectLoader(FileStorageProvider fileStorageProvider, PostProcessor postProcessor, ProjectChecker checker) throws FileStorageException + public ProjectLoader(FileStorageProvider fileStorageProvider, PostProcessor postProcessor, ProjectChecker checker) { if(fileStorageProvider == null) throw new NullPointerException("fileStorageProvider cannot be null!"); this.fileStorageProvider = fileStorageProvider; this.postProcessor = postProcessor; this.checker = checker; - - // Get/create the temp folder: - tempFolder = fileStorageProvider.getTempFolder(true); - - // Create the project folder this.parser = new ProjectParser(); } @@ -232,18 +225,23 @@ public Project load(InputStream sapelliFileInputStream) throws Exception { clearWarnings(); Project project = null; - File extractFolder = new File(tempFolder.getAbsolutePath() + File.separator + System.currentTimeMillis()); + File extractFolder = null; try { + // STEP 0 - Create the extraction folder: + extractFolder = new File(fileStorageProvider.getTempFolder(true), "" + System.currentTimeMillis()); + if(!FileHelpers.createDirectory(extractFolder)) + throw new FileStorageException("Could not create folder to extract project file into."); + // STEP 1 - Extract the content of the Sapelli file to a new subfolder of the temp folder: try { - FileHelpers.createDirectory(extractFolder); - Unzipper.unzip(sapelliFileInputStream, extractFolder); + if(Unzipper.unzip(sapelliFileInputStream, extractFolder) == 0) + throw new Exception("Sapelli file is not a valid ZIP archive or does not contain any files."); } - catch(Exception e) + catch(IOException ioe) { - throw new Exception("Error on extracting contents of Sapelli file.", e); + throw new Exception("Error on extracting contents of Sapelli file.", ioe.getCause()); } // STEP 2 - Parse PROJECT.xml: @@ -302,6 +300,7 @@ public Project load(InputStream sapelliFileInputStream) throws Exception { // Delete temp or install folder: FileUtils.deleteQuietly(extractFolder); + // Re-throw Exception: throw e; } diff --git a/Library/src/uk/ac/ucl/excites/sapelli/collector/load/ProjectLoaderStorer.java b/Library/src/uk/ac/ucl/excites/sapelli/collector/load/ProjectLoaderStorer.java index 0b05943db..a1456651d 100644 --- a/Library/src/uk/ac/ucl/excites/sapelli/collector/load/ProjectLoaderStorer.java +++ b/Library/src/uk/ac/ucl/excites/sapelli/collector/load/ProjectLoaderStorer.java @@ -27,7 +27,6 @@ import uk.ac.ucl.excites.sapelli.collector.db.ProjectStore; import uk.ac.ucl.excites.sapelli.collector.db.exceptions.ProjectDuplicateException; -import uk.ac.ucl.excites.sapelli.collector.io.FileStorageException; import uk.ac.ucl.excites.sapelli.collector.io.FileStorageProvider; import uk.ac.ucl.excites.sapelli.collector.load.process.PostProcessor; import uk.ac.ucl.excites.sapelli.collector.model.Project; @@ -48,9 +47,8 @@ public class ProjectLoaderStorer implements ProjectLoader.ProjectChecker, Warnin /** * @param fileStorageProvider * @param projectStore - * @throws FileStorageException */ - public ProjectLoaderStorer(FileStorageProvider fileStorageProvider, ProjectStore projectStore) throws FileStorageException + public ProjectLoaderStorer(FileStorageProvider fileStorageProvider, ProjectStore projectStore) { this(fileStorageProvider, projectStore, null); // no post-processing } @@ -58,9 +56,8 @@ public ProjectLoaderStorer(FileStorageProvider fileStorageProvider, ProjectStore /** * @param fileStorageProvider * @param projectStore - * @throws FileStorageException */ - public ProjectLoaderStorer(FileStorageProvider fileStorageProvider, ProjectStore projectStore, PostProcessor postProcessor) throws FileStorageException + public ProjectLoaderStorer(FileStorageProvider fileStorageProvider, ProjectStore projectStore, PostProcessor postProcessor) { this.loader = new ProjectLoader(fileStorageProvider, postProcessor, this); this.projectStore = projectStore; diff --git a/Library/src/uk/ac/ucl/excites/sapelli/shared/io/Unzipper.java b/Library/src/uk/ac/ucl/excites/sapelli/shared/io/Unzipper.java index 34a2b8528..46491af88 100644 --- a/Library/src/uk/ac/ucl/excites/sapelli/shared/io/Unzipper.java +++ b/Library/src/uk/ac/ucl/excites/sapelli/shared/io/Unzipper.java @@ -34,15 +34,26 @@ public final class Unzipper private Unzipper() {} - static public void unzip(InputStream zipFileStream, File extractionFolder) throws IOException + /** + * Note that if the zipFileStream is not a ZIP file there will be no exception thrown, + * but also no files extracted (i.e. method returns 0). + * + * @param zipFileStream + * @param extractionFolder + * @return the number of extracted entries + * @throws IOException - always wraps around a causing Exception + */ + static public int unzip(InputStream zipFileStream, File extractionFolder) throws IOException { - String extractionPath = extractionFolder.getAbsolutePath() + File.separator; try { + String extractionPath = extractionFolder.getAbsolutePath() + File.separator; ZipInputStream zin = new ZipInputStream(zipFileStream); + int entryCount = 0; ZipEntry ze = null; while((ze = zin.getNextEntry()) != null) { + entryCount++; if(ze.isDirectory()) { if(!FileHelpers.createDirectory(extractionPath + ze.getName())) @@ -62,6 +73,7 @@ static public void unzip(InputStream zipFileStream, File extractionFolder) throw zin.closeEntry(); } zin.close(); + return entryCount; } catch(Exception e) {