package org.sana.android.procedure;
import java.net.URISyntaxException;
import org.sana.R;
import org.sana.android.activity.BaseRunner;
import org.sana.android.activity.ProcedureRunner;
import org.sana.android.media.AudioPlayer;
import org.sana.android.media.EducationResource;
import org.sana.android.util.SanaUtil;
import org.w3c.dom.Node;
import com.google.gson.Gson;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.text.InputType;
import android.text.TextUtils;
import android.util.Log;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup.LayoutParams;
import android.widget.Button;
import android.widget.Gallery;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
/**
* A ProcedureElement is an item that can be placed on a page in a Sana
* procedure. Typically there will only be one ProcedureElement per page, but
* this style suggestion is not enforced, and users can make XML procedure
* definitions that contain several ProcedureElements per page.
* <p/>
* A ProcedureElement, generally speaking, asks a question and may allow for an
* answer. For example, a RadioElement poses a question and allows a user to
* choose among response buttons.
*
* ProcedureElements are defined in XML and dynamically created from the XML in
* Sana.
* <p/>
* <ul type="none">
* <li><b>Clinical Use </b> Defined by subclasses.</li>
* <li><b>Collects </b> Defined by subclasses.</li>
* </ul>
* The <em>Collects</em> documentation should declare the format of any returned
* answer strings
*
* @author Sana Development Team
*/
public abstract class ProcedureElement {
public static String TAG = ProcedureElement.class.getSimpleName();
/**
* An enumeration of the valid ProcedureElement types.
*
* @author Sana Development Team
*/
public static enum ElementType {
/** Provides text display */
TEXT(""),
/** Provides text capture. */
ENTRY(""),
/** Provides exclusive option selector as a dropdown box. */
SELECT(""),
/** An entry element for displaying/entering a patient identifier */
PATIENT_ID(""),
/** A non-exclusive option selector as a list of checkboxes. */
MULTI_SELECT(""),
/** An exclusive multi-option selector as a list of radio buttons */
RADIO(""),
/** Provides capture of one or more images */
PICTURE("image.jpg"),
/** Provides capture of a single audio resource. */
SOUND("sound.3gp"),
/** Provides attachment of a binary file for upload */
BINARYFILE("binary.bin"),
/** A marker for invalid elements */
INVALID(""),
/** Provides capture of GPS coordinates */
GPS(""),
/** Provides capture of a date */
DATE(""),
/** Provides a viewable resource for patient education. */
EDUCATION_RESOURCE(""),
/** Provides access to 3rd party tools for data capture where the data
* is returned directly.
*/
PLUGIN(""),
/** Provides access to 3rd party tools for data capture where the data
* is not returned directly and must be manually entered by the user.
*/
ENTRY_PLUGIN(""),
HIDDEN(""),
AGE(""),
TRUTH;
private String filename;
private ElementType(){ this(""); }
private ElementType(String filename) {
this.filename = filename;
}
/**
* Returns the default filename for a given ElementType
* @return
*/
public String getFilename() {
return filename;
}
}
protected String id;
protected String question;
protected String answer;
protected String concept;
protected String action = null;
// Resource of a corresponding figure for this element.
protected String figure;
// Resource of a corresponding audio prompt for this element.
protected String audioPrompt;
// Whether a null answer is allowed
private boolean bRequired = false;
// Optional attributes - specific element types must implement as necessary
protected String defaultValue = null;
protected String defaultPrompt = null;
private Procedure procedure;
private Context cachedContext;
private View cachedView;
private AudioPlayer mAudioPlayer;
private String helpText;
/**
* Constructs the view of this ProcedureElement
*
* @param c the current Context
*/
protected abstract View createView(Context c);
void clearCachedView() {
cachedView = null;
}
/**
* Constructs a new Instance.
*
* @param id The unique identifier of this element within its procedure.
* @param question The text that will be displayed to the user as a question
* @param answer The result of data capture.
* @param concept A required categorization of the type of data captured.
* @param figure An optional figure to display to the user.
* @param audioPrompt An optional audio prompt to play for the user.
*/
protected ProcedureElement(String id, String question, String answer,
String concept, String figure, String audioPrompt)
{
this.id = id;
this.question = question;
this.answer = answer;
this.concept = concept;
this.figure = figure;
this.audioPrompt = audioPrompt;
}
/**
* A reference to the enclosing Procedure
* @return A Procedure instance.
*/
protected Procedure getProcedure() { // set the ImageView bounds to match the Drawable's dimensions
return procedure;
}
/**
* Sets the enclosing procedure
* @param procedure the new enclosing procedure.
*/
public void setProcedure(Procedure procedure) {
this.procedure = procedure;
}
/**
* Whether this element is considered active.
* @return
*/
protected boolean isViewActive() {
return !(cachedView == null);
}
/**
* A cached Context.
* @return The Context this element holds a reference to.
*/
protected Context getContext() {
return cachedContext;
}
/**
* A visible representation of this object.
* @param c The Context which will be used in the View constructors for this
* object's representation.
* @return A new view of this object or cached view if it exists.
*/
public View toView(Context c) {
if(cachedView == null || cachedContext != c) {
cachedView = createView(c);
cachedContext = c;
}
return cachedView;
}
/**
* Returns the ElementType of this element as defined in the
* ProcedureElement ElementType enum.
*/
public abstract ElementType getType();
/**
* Gets the value of the answer attribute.
*
* @return A String representation of collected data.
*/
public String getAnswer(){
return answer;
}
/**
* Set the value of the answer attribute as a String representation of
* collected data.
*
* @param answer the new answer
*/
public void setAnswer(String answer){
this.answer = answer;
}
/**
* Whether this element is considered required
* @return
*/
public boolean isRequired() {
return bRequired;
}
/**
* Sets the required state of this element.
* @param required The new required state.
*/
public void setRequired(boolean required) {
this.bRequired = required;
}
/**
* Help text associated with this element
* @return An informative string.
*/
public String getHelpText() {
return helpText;
}
/**
* Sets the help text for this instance.
* @param helpText the new help string.
*/
public void setHelpText(String helpText) {
this.helpText = helpText;
}
public String getAction(){
return action;
}
/**
* Whether this element is valid.
* @return true if not required or required and answer is not empty
* @throws ValidationError
*/
public boolean validate() throws ValidationError {
if (bRequired && "".equals(getAnswer().trim())) {
String msg = TextUtils.isEmpty(helpText)?
getString(R.string.general_input_required): helpText;
throw new ValidationError(msg);
}
return true;
}
/**
* Tell the element's widget to refresh itself.
*/
public void refreshWidget() {
}
/**
* Writes a string representation of this object to a StringBuilder.
* Extending classes should override appendOptionalAttributes if they
* require attributes beyond those defined in this class.
*
* @param sb the builder to write to.
*/
public void buildXML(StringBuilder sb){
sb.append("<Element ");
sb.append("type=\"" + getType().name() + "\" ");
sb.append("id=\"" + getId()+ "\" ");
sb.append("question=\"" + getQuestion()+ "\" ");
sb.append("answer=\"" + getAnswer()+ "\" ");
sb.append("figure=\"" + getFigure()+ "\" ");
sb.append("concept=\"" + getConcept()+ "\" ");
sb.append("audio=\"" + getAudioPrompt()+ "\" ");
sb.append("required=\"" + isRequired()+ "\" ");
appendOptionalAttributes(sb);
sb.append("/>\n");
}
protected void appendOptionalAttributes(StringBuilder sb){
if(!TextUtils.isEmpty(action))
sb.append("action=\"" + action+ "\" ");
if(hasDefault())
sb.append("default=\"" + getDefault()+ "\" ");
return;
}
/**
* Build the XML representation of this ProcedureElement. Should only use
* this if you intend to use only the XML for this element. If you are
* building the XML for this Procedure, then prefer buildXML with a
* StringBuilder since String operations are slow.
*/
public String toXML() {
StringBuilder sb = new StringBuilder();
buildXML(sb);
return sb.toString();
}
/**
* Create an element from an XML element node of a procedure definition.
* @param node a Node object containing a ProcedureElement representation
*/
public static ProcedureElement createElementfromXML(Node node) throws
ProcedureParseException
{
//Log.i(TAG, "fromXML(" + node.getNodeName() + ")");
if(!node.getNodeName().equals("Element")) {
throw new ProcedureParseException("Element got NodeName "
+ node.getNodeName());
}
String questionStr = SanaUtil.getNodeAttributeOrDefault(node,
"question", "");
String answerStr = SanaUtil.getNodeAttributeOrDefault(node,
"answer", null);
String typeStr = SanaUtil.getNodeAttributeOrDefault(node,
"type", "INVALID");
String conceptStr = SanaUtil.getNodeAttributeOrDefault(node,
"concept", "");
String idStr = SanaUtil.getNodeAttributeOrFail(node, "id",
new ProcedureParseException("Element doesn't have id number"));
String figureStr = SanaUtil.getNodeAttributeOrDefault(node,
"figure", "");
String audioStr = SanaUtil.getNodeAttributeOrDefault(node,
"audio", "");
ElementType etype = ElementType.valueOf(typeStr);
ProcedureElement el = null;
switch(etype) {
case TEXT:
el = TextElement.fromXML(idStr, questionStr, answerStr, conceptStr,
figureStr, audioStr, node);
break;
case ENTRY:
el = TextEntryElement.fromXML(idStr, questionStr, answerStr,
conceptStr, figureStr, audioStr, node);
break;
case SELECT:
el = SelectElement.fromXML(idStr, questionStr, answerStr,
conceptStr, figureStr, audioStr, node);
break;
case MULTI_SELECT:
el = MultiSelectElement.fromXML(idStr, questionStr, answerStr,
conceptStr, figureStr, audioStr, node);
break;
case RADIO:
el = RadioElement.fromXML(idStr, questionStr, answerStr,
conceptStr, figureStr, audioStr, node);
break;
case PICTURE:
el = PictureElement.fromXML(idStr, questionStr, answerStr,
conceptStr, figureStr, audioStr, node);
break;
case SOUND:
el = SoundElement.fromXML(idStr, questionStr, answerStr,
conceptStr, figureStr, audioStr, node);
break;
case GPS:
el = GpsElement.fromXML(idStr, questionStr, answerStr,
conceptStr, figureStr, audioStr, node);
break;
case BINARYFILE:
el = BinaryUploadElement.fromXML(idStr, questionStr, answerStr,
conceptStr, figureStr, audioStr, node);
break;
case PATIENT_ID:
el = PatientIdElement.fromXML(idStr, questionStr, answerStr,
conceptStr, figureStr, audioStr, node);
break;
case DATE:
el = DateElement.fromXML(idStr, questionStr, answerStr, conceptStr,
figureStr, audioStr, node);
break;
case EDUCATION_RESOURCE:
el = EducationResourceElement.fromXML(idStr, questionStr, answerStr,
conceptStr, figureStr, audioStr, node);
break;
case PLUGIN:
el = PluginElement.fromXML(idStr, questionStr, answerStr,
conceptStr, figureStr, audioStr, node);
break;
case ENTRY_PLUGIN:
el = PluginEntryElement.fromXML(idStr, questionStr, answerStr,
conceptStr, figureStr, audioStr, node);
break;
case HIDDEN:
el = HiddenElement.fromXML(idStr, questionStr, answerStr, conceptStr,
figureStr, audioStr, node);
break;
case AGE:
el = AgeElement.fromXML(idStr, questionStr, answerStr, conceptStr,
figureStr, audioStr, node);
break;
case TRUTH:
el = TruthElement.fromXML(idStr, questionStr, answerStr, conceptStr,
figureStr, audioStr, node);
break;
case INVALID:
default:
throw new ProcedureParseException("Got invalid node type : "
+ etype);
}
if (el == null) {
throw new ProcedureParseException("Failed to parse node with id "
+ idStr);
}
String helpStr = SanaUtil.getNodeAttributeOrDefault(node, "helpText",
"");
el.setHelpText(helpStr);
String requiredStr = SanaUtil.getNodeAttributeOrDefault(node,
"required", "false");
if ("true".equals(requiredStr)) {
el.setRequired(true);
} else if ("false".equals(requiredStr)) {
el.setRequired(false);
} else {
throw new ProcedureParseException("Argument to \'required\' "+
"attribute invalid for id " + idStr
+ ". Must be \'true\' or \'false\'");
}
return el;
}
public static void parseOptionalAttributes(Node node, ProcedureElement el)
throws ProcedureParseException {
Log.i(TAG, "parseOptionalAttributes(Node,ProcedureElement)");
String attr = null;
// action
attr = SanaUtil.getNodeAttributeOrDefault(node,
"action", "");
if(!TextUtils.isEmpty(attr))
el.action = new String(attr);
// default
attr = SanaUtil.getNodeAttributeOrDefault(node,
"default", "");
el.setDefault(new String(attr));
// helpText
attr = SanaUtil.getNodeAttributeOrDefault(node, "helpText",
"");
el.setHelpText(new String(attr));
// required
attr = SanaUtil.getNodeAttributeOrDefault(node,
"required", "false");
if ("true".equals(attr)) {
el.setRequired(true);
} else if ("false".equals(attr)) {
el.setRequired(false);
} else {
throw new ProcedureParseException("Argument to \'required\' " +
"attribute invalid for id " + el.getId()+
". Must be \'true\' or \'false\'");
}
}
/** @return The value of the id attribute */
public String getId() {
return id;
}
/** @return Gets the identifier for any associated education resources */
public String mediaId(){
return EducationResource.toId(concept+question);
}
/**
* @return the question string originally defined in the XML procedure
* definition.
*/
public String getQuestion() {
return question;
}
/**
* @return the medical concept associated with this ProcedureElement
*/
public String getConcept() {
return concept;
}
/**
* @return the figure URL associated with this ProcedureElement
*/
public String getFigure() {
return figure;
}
/** @return true if this instance has an audio prompt */
boolean hasAudioPrompt() {
return !"".equals(audioPrompt);
}
/** plays this instance's audio prompt */
void playAudioPrompt() {
if (mAudioPlayer != null)
mAudioPlayer.play();
}
/** @return the audioPrompt string */
public String getAudioPrompt() {
return audioPrompt;
}
/**
* Returns a localized String from the application package's default string
* table.
*
* @param resId Resource Id for the string
* @return
*/
public String getString(int resId){
return getContext().getString(resId);
}
/**
* Appends another View object a view of this object to a new View .
* @param c A valid Context.
* @param v The view to append first.
* @return A new View containing the parameter View and a View of this
* object.
*/
public View encapsulateQuestion(Context c, View v) {
// Add question view
TextView textView = new TextView(c);
textView.setSingleLine(false);
textView.setGravity(Gravity.LEFT);
String q = question.replace("\\n", "\n");
Log.d(TAG, "...show question id = " + getProcedure().idsShown());
if(getProcedure().idsShown() && !getType().equals(ElementType.TEXT)){
textView.setText(String.format("%s: %s", getId(), q));
}else{
textView.setText(q);
}
//textView.setGravity(Gravity.CENTER_HORIZONTAL);
textView.setTextAppearance(c, android.R.style.TextAppearance_Large);
View questionView = textView;
questionView.setPadding(10,5,10,5);
// Add image if provided
ImageView imageView = null;
//Set accompanying figure
if(!TextUtils.isEmpty(figure)) {
try{
String imagePath = c.getPackageName() + ":" + figure;
Log.d(TAG, "Using figure: " + figure);
int resID = c.getResources().getIdentifier(figure, null,
null);
Log.d(TAG, "Using figure id: " + resID);
imageView = new ImageView(c);
imageView.setImageResource(resID);
imageView.setAdjustViewBounds(true);
// set the ImageView bounds to match the Drawable's dimensions
imageView.setLayoutParams(new Gallery.LayoutParams(
LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
imageView.setPadding(10,10,10,10);
}
catch(Exception e){
Log.e(TAG, "Couldn't find resource figure " + e.toString());
}
}
// Add audio prompt if provided
if (hasAudioPrompt()) {
try {
String resourcePath = c.getPackageName() + ":" + audioPrompt;
int resID = c.getResources().getIdentifier(resourcePath,
null, null);
Log.i(TAG, "Looking up ID for resource: " + resourcePath + ", "
+ "got " + resID);
if (resID != 0) {
mAudioPlayer = new AudioPlayer(resID);
View playerView = mAudioPlayer.createView(c);
playerView.setLayoutParams(new LayoutParams(
LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
LinearLayout audioPromptView = new LinearLayout(c);
audioPromptView.setOrientation(LinearLayout.HORIZONTAL);
audioPromptView.setLayoutParams(new LayoutParams(
LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
audioPromptView.setGravity(Gravity.CENTER);
// Insert the play button to the left of the current
// question view.
audioPromptView.addView(playerView);
audioPromptView.addView(questionView);
questionView = audioPromptView;
}
} catch (Exception e) {
Log.e(TAG, "Couldn't find resource for audio prompt: "
+ e.toString());
}
}
//Log.d(TAG, "Loaded: " +this.toString());
LinearLayout ll = new LinearLayout(c);
ll.setOrientation(LinearLayout.VERTICAL);
//Add to layout
ll.addView(questionView);
if (imageView != null)
ll.addView(imageView);
// Add Buttons if provided
if(!TextUtils.isEmpty(action)){
View actionView = getActions(c);
actionView.setPadding(5,5,5,5);
ll.addView(actionView);
} else {
//Log.w(TAG, "Empty action string!");
}
if(v != null){
LinearLayout viewHolder = new LinearLayout(c);
viewHolder.addView(v);
viewHolder.setGravity(Gravity.CENTER_HORIZONTAL);
ll.addView(viewHolder, new LayoutParams(LayoutParams.MATCH_PARENT,
LayoutParams.WRAP_CONTENT));
}
ll.setGravity(Gravity.CENTER);
ll.setPadding(5, 0, 5, 0);
ll.setLayoutParams(new LayoutParams(LayoutParams.FILL_PARENT,
LayoutParams.WRAP_CONTENT));
return ll;
}
@Override
public String toString(){
/*
Gson gson = new Gson();
return gson.toJson(this);
*/
return String.format("ProcedureElement: type=%s, concept=%s, "
+"required=%s, id=%s, question=%s, figure=%s, audio=%s, answer=%s,"
+"action=%s",
getType(), concept, bRequired, id, question, figure, audioPrompt,
answer,action);
}
/**
* Returns a View containing a set of buttons, each of which can be
* used as a launch point for another activity.
*
* @see {@link #getAction()} for more on action String format
* @return a View containing a list of action buttons
*/
public View getActions(Context c){
Log.d(TAG, "action=" + action);
LinearLayout ll = new LinearLayout(c);
ll.setOrientation(LinearLayout.VERTICAL);
ll.setGravity(Gravity.CENTER);
ll.setPadding(5, 0, 5, 0);
ll.setLayoutParams(new LayoutParams(LayoutParams.FILL_PARENT,
LayoutParams.WRAP_CONTENT));
// Get the intent
for(String intentStr: action.split(",")){
Log.d(TAG, intentStr);
Button button = new Button(c);
button.setText("????");
try {
Intent buttonAction = Intent.parseUri(intentStr, Intent.URI_INTENT_SCHEME);
button.setTag(buttonAction);
button.setText(buttonAction.getStringExtra(Intent.EXTRA_TITLE));
button.setOnClickListener(new View.OnClickListener(){
@Override
public void onClick(View v) {
Intent intent = (Intent) v.getTag();
startActivity(intent);
}});
} catch (URISyntaxException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
ll.addView(button, new LayoutParams(LayoutParams.MATCH_PARENT,
LayoutParams.WRAP_CONTENT));
}
return ll;
}
protected final Activity getActivity(){
return (Activity) getContext();
}
protected final void startActivity(Intent intent){
Activity activity = getActivity();
Intent launcher = new Intent(getContext(), activity.getClass());
launcher.putExtra(Intent.EXTRA_INTENT, intent);
launcher.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
getContext().startActivity(launcher);
}
public String getDefault(){
Log.i(TAG, "getDefault()");
return defaultValue;
}
public void setDefault(String defaultValue){
Log.i(TAG, "setDefault(String)");
this.defaultValue = defaultValue;
}
public boolean hasDefault(){
Log.i(TAG, "hasDefault()");
return !TextUtils.isEmpty(defaultValue);
}
/**
* Creates the element from an XML procedure definition.
*
* @param id The unique identifier of this element within its procedure.
* @param question The text that will be displayed to the user as a question
* @param answer The result of data capture.
* @param concept A required categorization of the type of data captured.
* @param figure An optional figure to display to the user.
* @param audio An optional audio prompt to play for the user.
* @param node The source xml node.
* @return A new element.
* @throws ProcedureParseException if an error occurred while parsing
* additional information from the Node
*/
public static ProcedureElement fromXML(String id, String question,
String answer, String concept, String figure, String audio, Node node)
throws ProcedureParseException
{
throw new UnsupportedOperationException();
}
}