package org.commcare.android.javarosa; import android.content.ComponentName; import android.content.Intent; import android.net.Uri; import android.os.BadParcelableException; import android.os.Bundle; import android.util.Log; import org.commcare.provider.SimprintsCalloutProcessing; import org.commcare.logging.AndroidLogger; import org.commcare.utils.FileUtil; import org.javarosa.core.model.Constants; import org.javarosa.core.model.FormDef; import org.javarosa.core.model.condition.EvaluationContext; import org.javarosa.core.model.condition.Recalculate; import org.javarosa.core.model.data.AnswerDataFactory; import org.javarosa.core.model.data.IAnswerData; import org.javarosa.core.model.data.StringData; import org.javarosa.core.model.instance.AbstractTreeElement; import org.javarosa.core.model.instance.TreeReference; import org.javarosa.core.services.Logger; import org.javarosa.core.util.externalizable.DeserializationException; import org.javarosa.core.util.externalizable.ExtUtil; import org.javarosa.core.util.externalizable.ExtWrapList; import org.javarosa.core.util.externalizable.ExtWrapMap; import org.javarosa.core.util.externalizable.ExtWrapMapPoly; import org.javarosa.core.util.externalizable.ExtWrapNullable; import org.javarosa.core.util.externalizable.Externalizable; import org.javarosa.core.util.externalizable.PrototypeFactory; import org.javarosa.xpath.expr.FunctionUtils; import org.javarosa.xpath.expr.XPathExpression; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.File; import java.io.IOException; import java.util.Enumeration; import java.util.Hashtable; import java.util.Vector; /** * @author ctsims */ public class IntentCallout implements Externalizable { private static final String TAG = IntentCallout.class.getSimpleName(); private String className; private Hashtable<String, XPathExpression> refs; private Hashtable<String, Vector<TreeReference>> responseToRefMap; private FormDef formDef; private String type; private String component; private String data; private String buttonLabel; private String updateButtonLabel; private String appearance; // Generic Extra from intent callout extensions public static final String INTENT_RESULT_VALUE = "odk_intent_data"; // Bundle of extra values public static final String INTENT_RESULT_BUNDLE = "odk_intent_bundle"; /** * Intent flag to identify whether this callout should be included in attempts to compound * similar intents */ public static final String INTENT_EXTRA_CAN_AGGREGATE = "cc:compound_include"; public IntentCallout() { // for serialization } /** * @param buttonLabel Intent callout button text for initially calling the intent. * @param updateButtonLabel Intent callout button text for re-calling the intent to update the answer * @param appearance if 'quick' then intent is automatically called when question is shown, and advanced when intent answer is received */ public IntentCallout(String className, Hashtable<String, XPathExpression> refs, Hashtable<String, Vector<TreeReference>> responseToRefMap, String type, String component, String data, String buttonLabel, String updateButtonLabel, String appearance) { this.className = className; this.refs = refs; this.responseToRefMap = responseToRefMap; this.type = type; this.component = component; this.data = data; this.buttonLabel = buttonLabel; this.updateButtonLabel = updateButtonLabel; this.appearance = appearance; } public void attachToForm(FormDef form) { this.formDef = form; } public Intent generate(EvaluationContext ec) { Intent i = new Intent(); if (className != null) { i.setAction(className); } if(data != null && type != null){ // Weird hack but this call seems specifically to be needed to play video // http://stackoverflow.com/questions/1572107/android-intent-for-playing-video i.setDataAndType(Uri.parse(data), type); } else { if (type != null) { i.setType(type); } if (data != null) { i.setData(Uri.parse(data)); } } if (component != null) { i.setComponent(new ComponentName(component, className)); } if (refs != null) { for (Enumeration<String> en = refs.keys(); en.hasMoreElements(); ) { String key = en.nextElement(); Object xpathResult = refs.get(key).eval(ec); if (INTENT_EXTRA_CAN_AGGREGATE.equals(key)) { if(key != null && !"".equals(key)) { i.putExtra(INTENT_EXTRA_CAN_AGGREGATE, FunctionUtils.toBoolean(xpathResult)); } } else{ String extraVal = FunctionUtils.toString(xpathResult); if (extraVal != null && !"".equals(extraVal)) { i.putExtra(key, extraVal); } } } } Logger.log(AndroidLogger.TYPE_FORM_ENTRY, "Generated Intent: " + i.toString()); return i; } /** * @return if answer was set from intent successfully */ public boolean processResponse(Intent intent, TreeReference intentQuestionRef, File destination) { if (intentInvalid(intent)) { return false; } else if (SimprintsCalloutProcessing.isRegistrationResponse(intent)) { return SimprintsCalloutProcessing.processRegistrationResponse(formDef, intent, intentQuestionRef, responseToRefMap); } else { return processOdkResponse(intent, intentQuestionRef, destination) // If this is a print callout, then we shouldn't be setting any answer from // the result anyway, so always return true || isPrintIntentCallout(); } } private boolean isPrintIntentCallout() { return "org.commcare.dalvik.action.PRINT".equals(this.className); } public void processBarcodeResponse(TreeReference intentQuestionRef, String scanResult) { setNodeValue(formDef, intentQuestionRef, scanResult); } private static boolean intentInvalid(Intent intent) { if (intent == null) { return true; } try { // force unparcelling to check if we are missing classes to // correctly process callout response intent.hasExtra(INTENT_RESULT_VALUE); } catch (BadParcelableException e) { Log.w(TAG, "unable to unparcel intent: " + e.getMessage()); return true; } return false; } private boolean processOdkResponse(Intent intent, TreeReference intentQuestionRef, File destination) { String result = intent.getStringExtra(INTENT_RESULT_VALUE); setNodeValue(formDef, intentQuestionRef, result); // see if we have a return bundle Bundle response = intent.getBundleExtra(INTENT_RESULT_BUNDLE); // Load all of the data from the incoming bundle if (responseToRefMap != null && response != null) { for (String key : responseToRefMap.keySet()) { // See if the value exists at all, if not, skip it if (!response.containsKey(key)) { continue; } // Get our response value String responseValue = response.getString(key); if (key == null) { key = ""; } for (TreeReference ref : responseToRefMap.get(key)) { processResponseItem(ref, responseValue, intentQuestionRef, destination); } } } return (result != null); } public static void setNodeValue(FormDef formDef, TreeReference reference, String stringValue) { // todo: this code is very similar to SetValueAction.processAction, could be unified? if (stringValue != null) { EvaluationContext evaluationContext = new EvaluationContext(formDef.getEvaluationContext(), reference); AbstractTreeElement node = evaluationContext.resolveReference(reference); int dataType = node.getDataType(); setValueInFormDef(formDef, reference, stringValue, dataType); } else { formDef.setValue(null, reference); } } public static void setValueInFormDef(FormDef formDef, TreeReference ref, String responseValue, int dataType) { IAnswerData val = Recalculate.wrapData(responseValue, dataType); if (val != null) { val = AnswerDataFactory.templateByDataType(dataType).cast(val.uncast()); } formDef.setValue(val, ref); } private void processResponseItem(TreeReference ref, String responseValue, TreeReference contextRef, File destinationFile) { TreeReference fullRef = ref.contextualize(contextRef); EvaluationContext context = new EvaluationContext(formDef.getEvaluationContext(), contextRef); AbstractTreeElement node = context.resolveReference(fullRef); if (node == null) { Log.e(TAG, "Unable to resolve ref " + ref); return; } int dataType = node.getDataType(); //TODO: Handle file system errors in a way that is more visible to the user if (dataType == Constants.DATATYPE_BINARY) { storePointerToFileResponse(fullRef, responseValue, destinationFile); } else { setValueInFormDef(formDef, fullRef, responseValue, dataType); } } private void storePointerToFileResponse(TreeReference ref, String responseValue, File destinationFile) { //We need to copy the binary data at this address into the appropriate location if (responseValue == null || responseValue.equals("")) { //If the response was blank, wipe out any data that was present before formDef.setValue(null, ref); return; } //Otherwise, grab that file File src = new File(responseValue); if (!src.exists()) { //TODO: How hard should we be failing here? Log.w(TAG, "CommCare received a link to a file at " + src.toString() + " to be included in the form, but it was not present on the phone!"); //Wipe out any reference that exists formDef.setValue(null, ref); } else { File newFile = new File(destinationFile, src.getName()); //Looks like our source file exists, so let's go grab it try { FileUtil.copyFile(src, newFile); } catch (IOException e) { Log.e(TAG, "IOExeception copying Intent binary."); e.printStackTrace(); } //That code throws no errors, so we have to manually check whether the copy worked. if (newFile.exists() && newFile.length() == src.length()) { formDef.setValue(new StringData(newFile.toString()), ref); } else { Log.e(TAG, "CommCare failed to property write a file to " + newFile.toString()); formDef.setValue(null, ref); } } } @Override public void readExternal(DataInputStream in, PrototypeFactory pf) throws IOException, DeserializationException { className = ExtUtil.readString(in); refs = (Hashtable<String, XPathExpression>)ExtUtil.read(in, new ExtWrapMapPoly(String.class, true), pf); responseToRefMap = (Hashtable<String, Vector<TreeReference>>)ExtUtil.read(in, new ExtWrapMap(String.class, new ExtWrapList(TreeReference.class)), pf); appearance = (String)ExtUtil.read(in, new ExtWrapNullable(String.class), pf); component = (String)ExtUtil.read(in, new ExtWrapNullable(String.class), pf); buttonLabel = (String)ExtUtil.read(in, new ExtWrapNullable(String.class), pf); updateButtonLabel = (String)ExtUtil.read(in, new ExtWrapNullable(String.class), pf); type = (String)ExtUtil.read(in, new ExtWrapNullable(String.class), pf); data = (String)ExtUtil.read(in, new ExtWrapNullable(String.class), pf); } @Override public void writeExternal(DataOutputStream out) throws IOException { ExtUtil.writeString(out, className); ExtUtil.write(out, new ExtWrapMapPoly(refs)); ExtUtil.write(out, new ExtWrapMap(responseToRefMap, new ExtWrapList())); ExtUtil.write(out, new ExtWrapNullable(appearance)); ExtUtil.write(out, new ExtWrapNullable(component)); ExtUtil.write(out, new ExtWrapNullable(buttonLabel)); ExtUtil.write(out, new ExtWrapNullable(updateButtonLabel)); ExtUtil.write(out, new ExtWrapNullable(type)); ExtUtil.write(out, new ExtWrapNullable(data)); } public String getButtonLabel() { return buttonLabel; } public String getUpdateButtonLabel() { return updateButtonLabel; } public String getAppearance() { return appearance; } public boolean isSimprintsCallout() { return "com.simprints.id.REGISTER".equals(className); } }