/* * Copyright (C) 2009 University of Washington * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package org.odk.collect.android.tasks; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import javax.crypto.spec.SecretKeySpec; import org.javarosa.core.model.FormDef; import org.javarosa.core.model.instance.InstanceInitializationFactory; import org.javarosa.core.model.instance.TreeElement; import org.javarosa.core.model.instance.TreeReference; import org.javarosa.core.reference.ReferenceManager; import org.javarosa.core.reference.RootTranslator; import org.javarosa.core.services.Logger; import org.javarosa.core.services.locale.Localization; import org.javarosa.core.services.locale.Localizer; import org.javarosa.core.util.externalizable.DeserializationException; import org.javarosa.form.api.FormEntryController; import org.javarosa.form.api.FormEntryModel; import org.javarosa.xform.parse.XFormParseException; import org.javarosa.xform.parse.XFormParser; import org.javarosa.xform.util.XFormUtils; import org.odk.collect.android.activities.FormEntryActivity; import org.odk.collect.android.application.Collect; import org.odk.collect.android.jr.extensions.CalendaredDateFormatHandler; import org.odk.collect.android.jr.extensions.IntentExtensionParser; import org.odk.collect.android.jr.extensions.PollSensorAction; import org.odk.collect.android.jr.extensions.PollSensorExtensionParser; import org.odk.collect.android.listeners.FormLoaderListener; import org.odk.collect.android.logic.FileReferenceFactory; import org.odk.collect.android.logic.FormController; import org.odk.collect.android.provider.FormsProviderAPI.FormsColumns; import org.odk.collect.android.utilities.ApkUtils; import org.odk.collect.android.utilities.FileUtils; import android.content.Context; import android.database.Cursor; import android.net.Uri; import android.os.AsyncTask; import android.os.Environment; import android.util.Log; /** * Background task for loading a form. * * @author Carl Hartung (carlhartung@gmail.com) * @author Yaw Anokwa (yanokwa@gmail.com) */ public class FormLoaderTask extends AsyncTask<Uri, String, FormLoaderTask.FECWrapper> { public static InstanceInitializationFactory iif; private final static String t = "FormLoaderTask"; private FormLoaderListener mStateListener; private String mErrorMsg; private SecretKeySpec mSymetricKey; private boolean mReadOnly; private Context context; public FormLoaderTask(Context context) { this(context, null, false); } public FormLoaderTask(Context context, SecretKeySpec symetricKey, boolean readOnly) { this.context = context; this.mSymetricKey = symetricKey; this.mReadOnly = readOnly; } protected class FECWrapper { FormController controller; protected FECWrapper(FormController controller) { this.controller = controller; } protected FormController getController() { return controller; } protected void free() { controller = null; } } FECWrapper data; /** * (non-Javadoc) * @see android.os.AsyncTask#doInBackground(java.lang.Object[]) * * Initialize {@link FormEntryController} with {@link FormDef} from binary or from XML. If given * an instance, it will be used to fill the {@link FormDef}. */ @Override protected FECWrapper doInBackground(Uri... form) { FormEntryController fec = null; FormDef fd = null; FileInputStream fis = null; mErrorMsg = null; Uri theForm = form[0]; //TODO: Selection=? helper Cursor c = context.getContentResolver().query(theForm, new String[] {FormsColumns.FORM_FILE_PATH, FormsColumns.FORM_MEDIA_PATH}, null, null, null); if(!c.moveToFirst()) {throw new IllegalArgumentException("Invalid Form URI Provided! No form content found at URI: " + theForm.toString()); } String formPath = c.getString(c.getColumnIndex(FormsColumns.FORM_FILE_PATH)); File formXml = new File(formPath); String formHash = FileUtils.getMd5Hash(formXml); File formBin = new File(Collect.CACHE_PATH + "/" + formHash + ".formdef"); if (formBin.exists()) { // if we have binary, deserialize binary Log.i( t, "Attempting to load " + formXml.getName() + " from cached file: " + formBin.getAbsolutePath()); fd = deserializeFormDef(formBin); if (fd == null) { // some error occured with deserialization. Remove the file, and make a new .formdef // from xml Log.w(t, "Deserialization FAILED! Deleting cache file: " + formBin.getAbsolutePath()); formBin.delete(); } } if (fd == null) { // no binary, read from xml try { Log.i(t, "Attempting to load from: " + formXml.getAbsolutePath()); fis = new FileInputStream(formXml); XFormParser.registerHandler("intent", new IntentExtensionParser()); XFormParser.registerStructuredAction("pollsensor", new PollSensorExtensionParser()); fd = XFormUtils.getFormFromInputStream(fis); if (fd == null) { mErrorMsg = "Error reading XForm file"; } else { serializeFormDef(fd, formPath); } } catch (FileNotFoundException e) { e.printStackTrace(); mErrorMsg = e.getMessage(); } catch (XFormParseException e) { mErrorMsg = e.getMessage(); e.printStackTrace(); } catch (Exception e) { mErrorMsg = e.getMessage(); e.printStackTrace(); } } if (mErrorMsg != null) { return null; } fd.exprEvalContext.addFunctionHandler(new CalendaredDateFormatHandler(context)); // create FormEntryController from formdef FormEntryModel fem = new FormEntryModel(fd); fec = new FormEntryController(fem); //TODO: Get a reasonable IIF object //iif = something try { // import existing data into formdef if (FormEntryActivity.mInstancePath != null) { // This order is important. Import data, then initialize. importData(FormEntryActivity.mInstancePath, fec); fd.initialize(false, iif); } else { fd.initialize(true, iif); } if(mReadOnly) { fd.getInstance().getRoot().setEnabled(false); } } catch (RuntimeException e) { e.printStackTrace(); mErrorMsg = e.getMessage(); return null; } // set paths to /sdcard/odk/forms/formfilename-media/ String formFileName = formXml.getName().substring(0, formXml.getName().lastIndexOf(".")); // Remove previous forms ReferenceManager._().clearSession(); String formMediaPath = c.getString(c.getColumnIndex(FormsColumns.FORM_MEDIA_PATH)); if(formMediaPath != null) { ReferenceManager._().addSessionRootTranslator( new RootTranslator("jr://images/", formMediaPath)); ReferenceManager._().addSessionRootTranslator( new RootTranslator("jr://audio/", formMediaPath)); ReferenceManager._().addSessionRootTranslator( new RootTranslator("jr://video/", formMediaPath)); } else { // This should get moved to the Application Class if (ReferenceManager._().getFactories().length == 0) { // this is /sdcard/odk ReferenceManager._().addReferenceFactory( new FileReferenceFactory(Environment.getExternalStorageDirectory() + "/odk")); } // Set jr://... to point to /sdcard/odk/forms/filename-media/ ReferenceManager._().addSessionRootTranslator( new RootTranslator("jr://images/", "jr://file/forms/" + formFileName + "-media/")); ReferenceManager._().addSessionRootTranslator( new RootTranslator("jr://audio/", "jr://file/forms/" + formFileName + "-media/")); ReferenceManager._().addSessionRootTranslator( new RootTranslator("jr://video/", "jr://file/forms/" + formFileName + "-media/")); } // clean up vars fis = null; fd = null; formBin = null; formXml = null; formPath = null; FormController fc = new FormController(fec, mReadOnly); data = new FECWrapper(fc); return data; } public boolean importData(String filePath, FormEntryController fec) { // convert files into a byte array byte[] fileBytes = FileUtils.getFileAsBytes(new File(filePath), mSymetricKey); // get the root of the saved and template instances TreeElement savedRoot = XFormParser.restoreDataModel(fileBytes, null).getRoot(); TreeElement templateRoot = fec.getModel().getForm().getInstance().getRoot().deepCopy(true); // weak check for matching forms if (!savedRoot.getName().equals(templateRoot.getName()) || savedRoot.getMult() != 0) { Log.e(t, "Saved form instance does not match template form definition"); return false; } else { // populate the data model TreeReference tr = TreeReference.rootRef(); tr.add(templateRoot.getName(), TreeReference.INDEX_UNBOUND); templateRoot.populate(savedRoot, fec.getModel().getForm()); // populated model to current form fec.getModel().getForm().getInstance().setRoot(templateRoot); // fix any language issues // : http://bitbucket.org/javarosa/main/issue/5/itext-n-appearing-in-restored-instances if (fec.getModel().getLanguages() != null) { fec.getModel() .getForm() .localeChanged(fec.getModel().getLanguage(), fec.getModel().getForm().getLocalizer()); } return true; } } /** * Read serialized {@link FormDef} from file and recreate as object. * * @param formDef serialized FormDef file * @return {@link FormDef} object */ public FormDef deserializeFormDef(File formDef) { // TODO: any way to remove reliance on jrsp? // need a list of classes that formdef uses FileInputStream fis = null; FormDef fd = null; try { // create new form def fd = new FormDef(); fis = new FileInputStream(formDef); DataInputStream dis = new DataInputStream(fis); // read serialized formdef into new formdef fd.readExternal(dis, ApkUtils.getPrototypeFactory(context)); dis.close(); } catch (FileNotFoundException e) { e.printStackTrace(); fd = null; } catch (IOException e) { e.printStackTrace(); fd = null; } catch (DeserializationException e) { e.printStackTrace(); fd = null; } catch (Throwable e) { e.printStackTrace(); fd = null; } return fd; } /** * Write the FormDef to the file system as a binary blog. * * @param filepath path to the form file */ public void serializeFormDef(FormDef fd, String filepath) { // calculate unique md5 identifier String hash = FileUtils.getMd5Hash(new File(filepath)); File formDef = new File(Collect.CACHE_PATH + "/" + hash + ".formdef"); // formdef does not exist, create one. if (!formDef.exists()) { FileOutputStream fos; try { fos = new FileOutputStream(formDef); DataOutputStream dos = new DataOutputStream(fos); fd.writeExternal(dos); dos.flush(); dos.close(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } } /* * (non-Javadoc) * @see android.os.AsyncTask#onPostExecute(java.lang.Object) */ @Override protected void onPostExecute(FECWrapper wrapper) { synchronized (this) { if (mStateListener != null) { if (wrapper == null) { mStateListener.loadingError(mErrorMsg); } else { mStateListener.loadingComplete(wrapper.getController()); } } } } public void setFormLoaderListener(FormLoaderListener sl) { synchronized (this) { mStateListener = sl; } } public void destroy() { if (data != null) { data.free(); data = null; } } }