Skip to content

Commit

Permalink
Many improvements relating to project loading.
Browse files Browse the repository at this point in the history
Should fix issue #43.

ProjectManagerActivity:
 - multiple safety/error handling improvements mostly related to project loading/downloading
 - user is now warned about unexpected project file extensions but can opt to continue loading anyway (except for xml files), extension check is disabled for download projects as those may not have a proper extension

ProjectLoader:
 - made safer by moving the creation of temp folder into the actual load() process
 - load() now checks if ZIP extraction actually extracted files at all
 - improvement to HasSapelliFileExtension(File)

Unzipper: unzip() now returns the number of extracted entries (+ added comment which clarifies that NO exceptions are thrown when trying to extract a non-ZIP file)

AndroidProjectLoaderStorer: safety & readability improvements

BaseActivity: added new helper method to execute code that needs the fileStorageProvider, using it from ProjectManagerActivity

Signed-off-by: Matthias Stevens <matthias.stevens@gmail.com>
  • Loading branch information
mstevens83 committed Jan 4, 2016
1 parent a7bd066 commit 2332428
Show file tree
Hide file tree
Showing 7 changed files with 194 additions and 98 deletions.
5 changes: 4 additions & 1 deletion CollectorAndroid/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,9 @@
<string name="more_options">Additional options</string>
<string name="settings">Settings</string>
<string name="pleaseSelect">Please select a Sapelli file</string>
<string name="invalidFile">File could not be found or is not readable.</string>
<string name="noBareXMLProjects">The loading of bare XML files is no longer supported. Please package your project in a .sap file.</string>
<string name="unsupportedExtension">Unsupported Sapelli project file extension (%1$s), supported extensions are %2$s.</string>
<string name="unsupportedExtension">Unsupported Sapelli project file extension (%1$s), supported extensions are %2$s. Do you want to try loading a project from this file anyway?</string>
<string name="projectLoading">Loading project …</string>
<string name="projectRemoving">Removing project …</string>
<string name="sapelliFileLoadFailure">Failed to load or store Sapelli project (source: %1$s).\nCause: %2$s</string>
Expand All @@ -76,6 +77,7 @@
<string name="missingFiles">These files could not be found</string>
<string name="downloading">Downloading …</string>
<string name="downloadError">Download error. Please check if you are connected to the Internet.</string>
<string name="downloadErrorWithCause">Download error: %s.\nPlease check if you are connected to the Internet.</string>
<string name="photoCameraErrorFatal">Cannot detect camera to take mandatory picture; cancelling record and exiting project&#8230;</string>
<string name="videoCameraErrorFatal">Cannot detect camera to record mandatory video; cancelling record and exiting project&#8230;</string>
<string name="photoCameraErrorSkip">Cannot detect camera to take picture; skipping field \"%1$s\"&#8230;</string>
Expand All @@ -90,6 +92,7 @@
<string name="fingerprint">Finger print</string>
<string name="numForms">Number of forms</string>
<string name="modelID">Model ID</string>
<string name="storageError">Storage error: %s</string>
<string name="needsStorageAccess">needs write access to the external/mass storage in order to function. Please insert an SD card and restart the application.</string>
<string name="unavailableStorageAccess">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.</string>
<string name="useAlternativeStorage">Try another …</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}

Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<String> warnings)
{
callback.projectLoadStoreSuccess(sapelliFile, sourceURI, project, warnings);
}

@Override
public void projectLoadStoreFailure(Exception cause)
{
callback.projectLoadStoreFailure(sapelliFile, sourceURI, cause);
}
}).execute(in);
}

/**
Expand All @@ -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<Context, Void, Project>
private class AsyncProjectLoadStoreTask extends AsyncTaskWithWaitingDialog<Context, InputStream, Project>
{

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)
Expand All @@ -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
}

}
Expand Down
Loading

0 comments on commit 2332428

Please sign in to comment.