package org.commcare.tasks; import android.content.Context; import android.database.Cursor; import android.net.Uri; import android.os.Environment; import android.support.v4.util.Pair; import android.util.Log; import org.commcare.CommCareApplication; import org.commcare.activities.components.FormEntryInstanceState; import org.commcare.android.logging.ForceCloseLogger; import org.commcare.android.resource.installers.XFormAndroidInstaller; import org.commcare.core.process.CommCareInstanceInitializer; import org.commcare.logging.UserCausedRuntimeException; import org.commcare.logging.XPathErrorLogger; import org.commcare.logic.AndroidFormController; import org.javarosa.xpath.XPathException; import org.commcare.engine.extensions.XFormExtensionUtils; import org.commcare.logging.AndroidLogger; import org.commcare.logic.FileReferenceFactory; import org.commcare.models.encryption.EncryptionIO; import org.commcare.provider.FormsProviderAPI; import org.commcare.tasks.templates.CommCareTask; import org.commcare.utils.FileUtil; import org.commcare.utils.GlobalConstants; 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.form.api.FormEntryController; import org.javarosa.form.api.FormEntryModel; import org.javarosa.xform.parse.XFormParseException; import org.javarosa.xform.parse.XFormParser; import java.io.BufferedInputStream; 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 java.io.InputStream; import java.io.OutputStream; import javax.crypto.spec.SecretKeySpec; /** * @author Carl Hartung (carlhartung@gmail.com) * @author Yaw Anokwa (yanokwa@gmail.com) */ public abstract class FormLoaderTask<R> extends CommCareTask<Uri, String, FormLoaderTask.FECWrapper, R> { public static InstanceInitializationFactory iif; private final SecretKeySpec mSymetricKey; private final boolean mReadOnly; private final boolean recordEntrySession; private final R activity; private FECWrapper data; public static final int FORM_LOADER_TASK_ID = 16; public FormLoaderTask(SecretKeySpec symetricKey, boolean readOnly, boolean recordEntrySession, R activity) { this.mSymetricKey = symetricKey; this.mReadOnly = readOnly; this.activity = activity; this.taskId = FORM_LOADER_TASK_ID; this.recordEntrySession = recordEntrySession; TAG = FormLoaderTask.class.getSimpleName(); } /** * 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 doTaskBackground(Uri... form) { FormDef fd = null; Pair<String, String> formAndMediaPaths = getFormAndMediaPaths(form[0]); String formPath = formAndMediaPaths.first; String formMediaPath = formAndMediaPaths.second; File formXml = new File(formPath); String formHash = FileUtil.getMd5Hash(formXml); File formBin = getCachedForm(formHash); if (formBin.exists()) { // if we have binary, deserialize binary Log.i(TAG, "Attempting to load " + formXml.getName() + " from cached file: " + formBin.getAbsolutePath()); fd = deserializeFormDef((Context)activity, formBin); if (fd == null) { Logger.log(AndroidLogger.SOFT_ASSERT, "Deserialization of " + formXml.getName() + " form failed."); // Remove the file, and make a new .formdef from xml formBin.delete(); } } // If we couldn't find a cached version, load the form from the XML if (fd == null) { fd = loadFormFromFile(formXml); } // Try to write the form definition to a cached location try { serializeFormDef(fd, formPath); } catch (Exception e) { // The cache is a bonus, so if we can't write it, don't crash, but log // it so we can clean up whatever is preventing the cached version from // working Logger.log(AndroidLogger.TYPE_RESOURCES, "XForm could not be serialized. Error trace:\n" + ForceCloseLogger.getStackTrace(e)); } FormEntryController fec = initFormDef(fd); // Remove previous forms ReferenceManager.instance().clearSession(); setupFormMedia(formMediaPath, formXml); AndroidFormController formController = new AndroidFormController(fec, mReadOnly); data = new FECWrapper(formController); return data; } private Pair<String, String> getFormAndMediaPaths(Uri formUri) { Cursor c = null; try { //TODO: Selection=? helper c = ((Context)activity).getContentResolver().query(formUri, new String[]{FormsProviderAPI.FormsColumns.FORM_FILE_PATH, FormsProviderAPI.FormsColumns.FORM_MEDIA_PATH}, null, null, null); if (c == null || !c.moveToFirst()) { throw new IllegalArgumentException("Invalid Form URI Provided! No form content found at URI: " + formUri.toString()); } return new Pair<>(c.getString(c.getColumnIndex(FormsProviderAPI.FormsColumns.FORM_FILE_PATH)), c.getString(c.getColumnIndex(FormsProviderAPI.FormsColumns.FORM_MEDIA_PATH))); } finally { if (c != null) { c.close(); } } } private FormDef loadFormFromFile(File formXmlFile) { FileInputStream fis; // no binary, read from xml Log.i(TAG, "Attempting to load from: " + formXmlFile.getAbsolutePath()); try { fis = new FileInputStream(formXmlFile); } catch (FileNotFoundException e) { throw new RuntimeException("Error reading XForm file", e); } XFormAndroidInstaller.registerAndroidLevelFormParsers(); FormDef fd = XFormExtensionUtils.getFormFromInputStream(fis); if (fd == null) { throw new RuntimeException("Error reading XForm file: FormDef is null"); } return fd; } private String getSystemLocale() { Localizer mLocalizer = Localization.getGlobalLocalizerAdvanced(); if(mLocalizer != null) { return mLocalizer.getLocale(); } else{ Logger.log("formloader", "Could not get the localizer"); } return null; } private FormEntryController initFormDef(FormDef formDef) { // create FormEntryController from formdef FormEntryModel fem = new FormEntryModel(formDef); FormEntryController fec; if (recordEntrySession) { fec = FormEntryController.buildRecordingController(fem); } else { fec = new FormEntryController(fem); } //TODO: Get a reasonable IIF object boolean isNewFormInstance = FormEntryInstanceState.mInstancePath == null; if (!isNewFormInstance) { importData(FormEntryInstanceState.mInstancePath, fec); } try { formDef.initialize(isNewFormInstance, iif, getSystemLocale()); } catch (XPathException e) { XPathErrorLogger.INSTANCE.logErrorToCurrentApp(e); throw new UserCausedRuntimeException(e.getMessage(), e); } catch (CommCareInstanceInitializer.FixtureInitializationException e) { throw new UserCausedRuntimeException(e.getMessage(), e); } if (mReadOnly) { formDef.getInstance().getRoot().setEnabled(false); } return fec; } private void setupFormMedia(String formMediaPath, File formXmlFile) { if (formMediaPath != null) { ReferenceManager.instance().addSessionRootTranslator( new RootTranslator("jr://images/", formMediaPath)); ReferenceManager.instance().addSessionRootTranslator( new RootTranslator("jr://audio/", formMediaPath)); ReferenceManager.instance().addSessionRootTranslator( new RootTranslator("jr://video/", formMediaPath)); } else { // This should get moved to the Application Class if (ReferenceManager.instance().getFactories().length == 0) { // this is /sdcard/odk ReferenceManager.instance().addReferenceFactory( new FileReferenceFactory(Environment.getExternalStorageDirectory() + "/odk")); } // set paths to /sdcard/odk/forms/formfilename-media/ String formFileName = formXmlFile.getName().substring(0, formXmlFile.getName().lastIndexOf(".")); // Set jr://... to point to /sdcard/odk/forms/filename-media/ ReferenceManager.instance().addSessionRootTranslator( new RootTranslator("jr://images/", "jr://file/forms/" + formFileName + "-media/")); ReferenceManager.instance().addSessionRootTranslator( new RootTranslator("jr://audio/", "jr://file/forms/" + formFileName + "-media/")); ReferenceManager.instance().addSessionRootTranslator( new RootTranslator("jr://video/", "jr://file/forms/" + formFileName + "-media/")); } } private boolean importData(String filePath, FormEntryController fec) { // convert files into a byte array InputStream is; try { is = EncryptionIO.getFileInputStream(filePath, mSymetricKey); } catch (FileNotFoundException e) { e.printStackTrace(); throw new RuntimeException("Unable to open encrypted form instance file: " + filePath); } // get the root of the saved and template instances TreeElement savedRoot; try { savedRoot = XFormParser.restoreDataModel(is, null).getRoot(); } catch (IOException e) { e.printStackTrace(); throw new XFormParseException("Bad parsing from byte array " + e.getMessage()); } 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(TAG, "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); // populated model to current form fec.getModel().getForm().getInstance().setRoot(templateRoot); return true; } } /** * Read serialized {@link FormDef} from file and recreate as object. */ private static FormDef deserializeFormDef(Context context, File formDefFile) { FileInputStream fis; FormDef fd; try { // create new form def fd = new FormDef(); fis = new FileInputStream(formDefFile); DataInputStream dis = new DataInputStream(new BufferedInputStream(fis)); // read serialized formdef into new formdef fd.readExternal(dis, CommCareApplication.instance().getPrototypeFactory(context)); dis.close(); } catch (Throwable e) { e.printStackTrace(); fd = null; } return fd; } /** * Write the FormDef to the file system as a binary blob. */ private void serializeFormDef(FormDef fd, String formFilePath) throws IOException { // calculate unique md5 identifier for this form String hash = FileUtil.getMd5Hash(new File(formFilePath)); File formDef = getCachedForm(hash); // create a serialized form file if there isn't already one at this hash if (!formDef.exists()) { OutputStream outputStream = null; try { outputStream = new FileOutputStream(formDef); DataOutputStream dos; outputStream = dos = new DataOutputStream(outputStream); fd.writeExternal(dos); dos.flush(); } finally { //make sure we clean up the stream if (outputStream != null) { try { outputStream.close(); } catch (IOException e) { // Swallow this. If we threw an exception from inside the // try, this close exception will trump it on the return // path, and we care a lot more about that exception // than this one. } } } } } private File getCachedForm(String hash) { return new File(CommCareApplication.instance().getCurrentApp(). fsPath(GlobalConstants.FILE_CC_CACHE) + "/" + hash + ".formdef"); } public void destroy() { if (data != null) { data.free(); data = null; } } protected static class FECWrapper { AndroidFormController controller; protected FECWrapper(AndroidFormController controller) { this.controller = controller; } public AndroidFormController getController() { return controller; } protected void free() { controller = null; } } }