Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for the odk:recordaudio action and odk-instance-load event to support Collect background audio recording #620

Merged
merged 11 commits into from
Feb 7, 2021
3 changes: 2 additions & 1 deletion src/main/java/org/javarosa/core/model/CoreModelModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ public class CoreModelModule implements IModule {
"org.javarosa.core.model.data.UncastData",
"org.javarosa.core.model.data.helper.BasicDataPointer",
"org.javarosa.core.model.actions.SetValueAction",
"org.javarosa.core.model.actions.setgeopoint.StubSetGeopointAction"
"org.javarosa.core.model.actions.setgeopoint.StubSetGeopointAction",
"org.javarosa.core.model.actions.recordaudio.RecordAudioAction"
};

@Override
Expand Down
23 changes: 11 additions & 12 deletions src/main/java/org/javarosa/core/model/FormDef.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@
import java.util.Set;
import org.javarosa.core.log.WrappedException;
import org.javarosa.core.model.TriggerableDag.EventNotifierAccessor;
import org.javarosa.core.model.actions.Action;
import org.javarosa.core.model.actions.ActionController;
import org.javarosa.core.model.actions.Actions;
import org.javarosa.core.model.condition.Constraint;
import org.javarosa.core.model.condition.EvaluationContext;
import org.javarosa.core.model.condition.IConditionExpr;
Expand Down Expand Up @@ -397,7 +397,7 @@ public void setValue(IAnswerData data, TreeReference ref, TreeElement node,

QuestionDef currentQuestion = findQuestionByRef(ref, this);
if (valueChanged && currentQuestion != null) {
currentQuestion.getActionController().triggerActionsFromEvent(Action.EVENT_QUESTION_VALUE_CHANGED, this,
currentQuestion.getActionController().triggerActionsFromEvent(Actions.EVENT_QUESTION_VALUE_CHANGED, this,
ref.getParentRef(), null);
}

Expand Down Expand Up @@ -515,17 +515,17 @@ public void createNewRepeat(FormIndex index) throws InvalidReferenceException {

// Fire events before form re-computation (calculates, relevance, etc). First trigger actions defined in the
// model and then trigger actions defined in the body
actionController.triggerActionsFromEvent(Action.EVENT_JR_INSERT, this, repeatContextRef, this);
actionController.triggerActionsFromEvent(Action.EVENT_ODK_NEW_REPEAT, this, repeatContextRef, this);
actionController.triggerActionsFromEvent(Actions.EVENT_JR_INSERT, this, repeatContextRef, this);
actionController.triggerActionsFromEvent(Actions.EVENT_ODK_NEW_REPEAT, this, repeatContextRef, this);
// Trigger actions nested in the new repeat
getChild(index).getActionController().triggerActionsFromEvent(Action.EVENT_ODK_NEW_REPEAT, this, repeatContextRef, this);
getChild(index).getActionController().triggerActionsFromEvent(Actions.EVENT_ODK_NEW_REPEAT, this, repeatContextRef, this);

dagImpl.createRepeatInstance(getMainInstance(), getEvaluationContext(), repeatContextRef, newNode);
}

@Override
public void processResultOfAction(TreeReference refSetByAction, String event) {
if (Action.EVENT_JR_INSERT.equals(event)) {
if (Actions.EVENT_JR_INSERT.equals(event)) {
// CommCare has an implementation if needed
}
}
Expand Down Expand Up @@ -1133,7 +1133,7 @@ public void preloadInstance(TreeElement node) {
}

public boolean postProcessInstance() {
actionController.triggerActionsFromEvent(Action.EVENT_XFORMS_REVALIDATE, this);
actionController.triggerActionsFromEvent(Actions.EVENT_XFORMS_REVALIDATE, elementsWithActionTriggeredByToplevelEvent, this);
return postProcessInstance(mainInstance.getRoot());
}

Expand Down Expand Up @@ -1298,16 +1298,15 @@ public void initialize(boolean newInstance, InstanceInitializationFactory factor
}

if (newInstance) {
actionController.triggerActionsFromEvent(Action.EVENT_ODK_INSTANCE_FIRST_LOAD, this);
for (IFormElement element : elementsWithActionTriggeredByToplevelEvent) {
element.getActionController().triggerActionsFromEvent(Action.EVENT_ODK_INSTANCE_FIRST_LOAD, this, ((TreeReference) element.getBind().getReference()).getParentRef(), null);
}
actionController.triggerActionsFromEvent(Actions.EVENT_ODK_INSTANCE_FIRST_LOAD, elementsWithActionTriggeredByToplevelEvent, this);

// xforms-ready is marked as deprecated as of JavaRosa 2.14.0 but is still dispatched for compatibility with
// old form definitions
actionController.triggerActionsFromEvent(Action.EVENT_XFORMS_READY, this);
actionController.triggerActionsFromEvent(Actions.EVENT_XFORMS_READY, elementsWithActionTriggeredByToplevelEvent, this);
}

actionController.triggerActionsFromEvent(Actions.EVENT_ODK_INSTANCE_LOAD, elementsWithActionTriggeredByToplevelEvent, this);

Collection<QuickTriggerable> qts = initializeTriggerables(TreeReference.rootRef());
dagImpl.publishSummary("Form initialized", null, qts);
}
Expand Down
46 changes: 4 additions & 42 deletions src/main/java/org/javarosa/core/model/actions/Action.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,54 +3,25 @@
*/
package org.javarosa.core.model.actions;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import org.javarosa.core.model.FormDef;
import org.javarosa.core.model.instance.TreeReference;
import org.javarosa.core.util.externalizable.DeserializationException;
import org.javarosa.core.util.externalizable.ExtUtil;
import org.javarosa.core.util.externalizable.Externalizable;
import org.javarosa.core.util.externalizable.PrototypeFactory;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;

/**
* @author ctsims
*
*/
public abstract class Action implements Externalizable {
// Events that can trigger an action

/**
* Dispatched the first time a form instance is loaded.
*/
public static final String EVENT_ODK_INSTANCE_FIRST_LOAD = "odk-instance-first-load";

/**
* @deprecated because as W3C XForms defines it, it should be dispatched any time the XForms engine is ready. In
* JavaRosa, it was dispatched only on first load of a form instance. Use
* {@link #EVENT_ODK_INSTANCE_FIRST_LOAD} instead.
*/
@Deprecated
public static final String EVENT_XFORMS_READY = "xforms-ready";
public static final String EVENT_XFORMS_REVALIDATE = "xforms-revalidate";

public static final String EVENT_ODK_NEW_REPEAT = "odk-new-repeat";

/**
* @deprecated because it was never documented. Use {@link #EVENT_ODK_NEW_REPEAT} instead.
*/
public static final String EVENT_JR_INSERT = "jr-insert";

public static final String EVENT_QUESTION_VALUE_CHANGED = "xforms-value-changed";

private static final String[] allEvents = new String[]{EVENT_ODK_INSTANCE_FIRST_LOAD, EVENT_XFORMS_READY,
EVENT_ODK_NEW_REPEAT, EVENT_JR_INSERT, EVENT_QUESTION_VALUE_CHANGED, EVENT_XFORMS_REVALIDATE};

private String name;

public Action() {

// for serialization
}

public Action(String name) {
Expand Down Expand Up @@ -80,13 +51,4 @@ public void readExternal(DataInputStream in, PrototypeFactory pf) throws IOExcep
public void writeExternal(DataOutputStream out) throws IOException {
ExtUtil.writeString(out, name);
}

public static boolean isValidEvent(String actionEventAttribute) {
for (String event : allEvents) {
if (event.equals(actionEventAttribute)) {
return true;
}
}
return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Set;
import org.javarosa.core.model.FormDef;
import org.javarosa.core.model.GroupDef;
import org.javarosa.core.model.IFormElement;
import org.javarosa.core.model.condition.EvaluationContext;
import org.javarosa.core.model.instance.TreeReference;
import org.javarosa.core.util.externalizable.DeserializationException;
import org.javarosa.core.util.externalizable.ExtUtil;
Expand Down Expand Up @@ -74,8 +78,19 @@ public void registerEventListener(List<String> eventList, Action action) {
}
}

public void triggerActionsFromEvent(String event, FormDef model) {
public void triggerActionsFromEvent(String event, Set<IFormElement> nestedElements, FormDef model) {
triggerActionsFromEvent(event, model, null, null);

for (IFormElement element : nestedElements) {
TreeReference elementReference = (TreeReference) element.getBind().getReference();
TreeReference unqualifiedContext = element instanceof GroupDef ? elementReference : elementReference.getParentRef();
EvaluationContext context = new EvaluationContext(model.getEvaluationContext(), unqualifiedContext);
List<TreeReference> allContextRefs = context.expandReference(unqualifiedContext);

for (TreeReference contextRef : allContextRefs) {
element.getActionController().triggerActionsFromEvent(event, model, contextRef, null);
}
}
}

public void triggerActionsFromEvent(String event, FormDef model, TreeReference contextForAction,
Expand Down
57 changes: 57 additions & 0 deletions src/main/java/org/javarosa/core/model/actions/Actions.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package org.javarosa.core.model.actions;

import java.util.Arrays;

public class Actions {
private Actions() { }

//region Event names
/**
* Dispatched the first time a form instance is loaded.
*/
public static final String EVENT_ODK_INSTANCE_FIRST_LOAD = "odk-instance-first-load";

/**
* Dispatched any time a form instance is loaded.
*/
public static final String EVENT_ODK_INSTANCE_LOAD = "odk-instance-load";

/**
* @deprecated because as W3C XForms defines it, it should be dispatched any time the XForms engine is ready. In
* JavaRosa, it was dispatched only on first load of a form instance. Use
* {@link #EVENT_ODK_INSTANCE_FIRST_LOAD} instead.
*/
@Deprecated
public static final String EVENT_XFORMS_READY = "xforms-ready";
public static final String EVENT_XFORMS_REVALIDATE = "xforms-revalidate";

public static final String EVENT_ODK_NEW_REPEAT = "odk-new-repeat";

/**
* @deprecated because it was never documented. Use {@link #EVENT_ODK_NEW_REPEAT} instead.
*/
public static final String EVENT_JR_INSERT = "jr-insert";

public static final String EVENT_QUESTION_VALUE_CHANGED = "xforms-value-changed";
//endregion

private static final String[] ALL_EVENTS = new String[]{EVENT_ODK_INSTANCE_FIRST_LOAD, EVENT_ODK_INSTANCE_LOAD, EVENT_XFORMS_READY,
EVENT_ODK_NEW_REPEAT, EVENT_JR_INSERT, EVENT_QUESTION_VALUE_CHANGED, EVENT_XFORMS_REVALIDATE};

private static final String[] INSTANCE_LOAD_EVENTS = new String[]{EVENT_ODK_INSTANCE_FIRST_LOAD, EVENT_ODK_INSTANCE_LOAD, EVENT_XFORMS_READY};

private static final String[] TOP_LEVEL_EVENTS = new String[]{EVENT_ODK_INSTANCE_FIRST_LOAD, EVENT_ODK_INSTANCE_LOAD, EVENT_XFORMS_READY,
EVENT_XFORMS_REVALIDATE};

public static boolean isValidEvent(String actionEventAttribute) {
return Arrays.asList(ALL_EVENTS).contains(actionEventAttribute);
}

public static boolean isTopLevelEvent(String actionEventAttribute) {
return Arrays.asList(TOP_LEVEL_EVENTS).contains(actionEventAttribute);
}

public static boolean isInstanceLoadEvent(String actionEventAttribute) {
return Arrays.asList(INSTANCE_LOAD_EVENTS).contains(actionEventAttribute);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package org.javarosa.core.model.actions.recordaudio;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import org.javarosa.core.model.FormDef;
import org.javarosa.core.model.actions.Action;
import org.javarosa.core.model.instance.TreeReference;
import org.javarosa.core.util.externalizable.DeserializationException;
import org.javarosa.core.util.externalizable.ExtUtil;
import org.javarosa.core.util.externalizable.ExtWrapNullable;
import org.javarosa.core.util.externalizable.PrototypeFactory;

public class RecordAudioAction extends Action {
private TreeReference targetReference;
private String quality;

public RecordAudioAction(TreeReference targetReference, String quality) {
super(RecordAudioActionHandler.ELEMENT_NAME);
this.targetReference = targetReference;
this.quality = quality;
}

public RecordAudioAction() {
// empty body for serialization
}

@Override
public TreeReference processAction(FormDef model, TreeReference contextRef) {
TreeReference contextualizedTargetReference = contextRef == null ? this.targetReference
: this.targetReference.contextualize(contextRef);

if (RecordAudioActions.getRecordAudioListener() != null) {
RecordAudioActions.getRecordAudioListener().recordAudioTriggered(contextualizedTargetReference, quality);
}

return contextualizedTargetReference;
}

//region serialization
@Override
public void readExternal(DataInputStream in, PrototypeFactory pf) throws IOException, DeserializationException {
super.readExternal(in, pf);

targetReference = (TreeReference) ExtUtil.read(in, new ExtWrapNullable(TreeReference.class), pf);
quality = (String) ExtUtil.read(in, new ExtWrapNullable(String.class), pf);
}

@Override
public void writeExternal(DataOutputStream out) throws IOException {
super.writeExternal(out);

ExtUtil.write(out, new ExtWrapNullable(targetReference));
ExtUtil.write(out, new ExtWrapNullable(quality));
}
//endregion
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package org.javarosa.core.model.actions.recordaudio;

import static org.javarosa.xform.parse.XFormParser.EVENT_ATTR;
import static org.javarosa.xform.parse.XFormParser.getValidEventNames;

import java.util.List;
import org.javarosa.core.model.FormDef;
import org.javarosa.core.model.IDataReference;
import org.javarosa.core.model.IFormElement;
import org.javarosa.core.model.actions.Actions;
import org.javarosa.core.model.instance.FormInstance;
import org.javarosa.core.model.instance.TreeReference;
import org.javarosa.model.xform.XPathReference;
import org.javarosa.xform.parse.IElementHandler;
import org.javarosa.xform.parse.XFormParseException;
import org.javarosa.xform.parse.XFormParser;
import org.kxml2.kdom.Element;

public class RecordAudioActionHandler implements IElementHandler {
public static final String ELEMENT_NAME = "recordaudio";

@Override
public void handle(XFormParser p, Element e, Object parent) {
if (!e.getNamespace().equals(XFormParser.NAMESPACE_ODK)) {
throw new XFormParseException("recordaudio action must be in http://www.opendatakit.org/xforms namespace");
}

String quality = e.getAttributeValue("http://www.opendatakit.org/xforms", "quality");
String ref = e.getAttributeValue(null, "ref");

if (ref == null) {
throw new XFormParseException("odk:recordaudio action must specify a ref");
}

IDataReference dataRef = FormDef.getAbsRef(new XPathReference(ref), TreeReference.rootRef());
TreeReference target = FormInstance.unpackReference(dataRef);
p.registerActionTarget(target);

List<String> validEventNames = getValidEventNames(e.getAttributeValue(null, EVENT_ATTR));
for (String eventName : validEventNames) {
if (!Actions.isInstanceLoadEvent(eventName)) {
throw new XFormParseException("odk:recordaudio action may only be triggered by instance load events (e.g. odk-instance-load)");
}
}

RecordAudioAction action = new RecordAudioAction(target, quality);

// XFormParser.parseAction already ensures parent is an IFormElement so we can safely cast
((IFormElement) parent).getActionController().registerEventListener(validEventNames, action);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package org.javarosa.core.model.actions.recordaudio;

import org.javarosa.core.model.instance.TreeReference;

public interface RecordAudioActionListener {
void recordAudioTriggered(TreeReference absoluteTargetRef, String quality);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package org.javarosa.core.model.actions.recordaudio;

public class RecordAudioActions {
private RecordAudioActions() {

}

/**
* Global reference to a client class that want to get updates about triggered record audio actions. Recording audio
* needs to be handled entirely client-side and there's no convenient object to hook into to get information
* about triggered actions.
*/
private static RecordAudioActionListener recordAudioListener;

public static void setRecordAudioListener(RecordAudioActionListener listener) {
recordAudioListener = listener;
}

public static RecordAudioActionListener getRecordAudioListener() {
return recordAudioListener;
}
}
Loading