package org.commcare.android.view;
import java.io.IOException;
import java.util.Hashtable;
import java.util.Vector;
import org.commcare.android.models.Entity;
import org.commcare.android.util.StringUtils;
import org.commcare.dalvik.R;
import org.commcare.suite.model.Detail;
import org.commcare.suite.model.graph.GraphData;
import org.javarosa.core.reference.InvalidReferenceException;
import org.javarosa.core.reference.ReferenceManager;
import org.odk.collect.android.views.media.AudioButton;
import org.odk.collect.android.views.media.AudioController;
import org.odk.collect.android.views.media.ViewId;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.speech.tts.TextToSpeech;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.Spanned;
import android.text.TextUtils;
import android.text.style.BackgroundColorSpan;
import android.view.View;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.RelativeLayout;
import android.widget.TextView;
/**
* @author ctsims
*
*/
public class EntityView extends LinearLayout {
private View[] views;
private String[] forms;
private TextToSpeech tts;
private String[] searchTerms;
private Context context;
private AudioController controller;
private Hashtable<Integer, Hashtable<Integer, View>> renderedGraphsCache; // index => { orientation => GraphView }
private long rowId;
public static final String FORM_AUDIO = "audio";
public static final String FORM_IMAGE = "image";
public static final String FORM_GRAPH = "graph";
private boolean mFuzzySearchEnabled = true;
/*
* Constructor for row/column contents
*/
public EntityView(Context context, Detail d, Entity e, TextToSpeech tts,
String[] searchTerms, AudioController controller, long rowId, boolean mFuzzySearchEnabled) {
super(context);
this.context = context;
this.searchTerms = searchTerms;
this.tts = tts;
this.setWeightSum(1);
this.controller = controller;
this.renderedGraphsCache = new Hashtable<Integer, Hashtable<Integer, View>>();
this.rowId = rowId;
views = new View[e.getNumFields()];
forms = d.getTemplateForms();
float[] weights = calculateDetailWeights(d.getTemplateSizeHints());
for (int i = 0; i < views.length; ++i) {
if (weights[i] != 0) {
Object uniqueId = new ViewId(rowId, i, false);
views[i] = initView(e.getField(i), forms[i], uniqueId, e.getSortField(i));
views[i].setId(i);
}
}
refreshViewsForNewEntity(e, false, rowId);
for (int i = 0; i < views.length; i++) {
LayoutParams l = new LinearLayout.LayoutParams(0, LayoutParams.FILL_PARENT, weights[i]);
if (views[i] != null) {
addView(views[i], l);
}
}
this.mFuzzySearchEnabled = mFuzzySearchEnabled;
}
/*
* Constructor for row/column headers
*/
public EntityView(Context context, Detail d, String[] headerText) {
super(context);
this.context = context;
this.setWeightSum(1);
views = new View[headerText.length];
float[] lengths = calculateDetailWeights(d.getHeaderSizeHints());
String[] headerForms = d.getHeaderForms();
for (int i = 0 ; i < views.length ; ++i) {
if (lengths[i] != 0) {
LayoutParams l = new LinearLayout.LayoutParams(0, LayoutParams.FILL_PARENT, lengths[i]);
ViewId uniqueId = new ViewId(rowId, i, false);
views[i] = initView(headerText[i], headerForms[i], uniqueId, null);
views[i].setId(i);
addView(views[i], l);
}
}
}
/*
* Creates up a new view in the view with ID uniqueid, based upon
* the entity's text and form
*/
private View initView(Object data, String form, Object uniqueId, String sortField) {
View retVal;
if (FORM_IMAGE.equals(form)) {
ImageView iv = (ImageView)View.inflate(context, R.layout.entity_item_image, null);
retVal = iv;
}
else if (FORM_AUDIO.equals(form)) {
String text = (String) data;
AudioButton b;
if (text != null & text.length() > 0) {
b = new AudioButton(context, text, uniqueId, controller, true);
}
else {
b = new AudioButton(context, text, uniqueId, controller, false);
}
retVal = b;
}
else if (FORM_GRAPH.equals(form) && data instanceof GraphData) {
View layout = View.inflate(context, R.layout.entity_item_graph, null);
retVal = layout;
}
else {
View layout = View.inflate(context, R.layout.component_audio_text, null);
setupTextAndTTSLayout(layout, (String) data, sortField);
retVal = layout;
}
return retVal;
}
public void setSearchTerms(String[] terms) {
this.searchTerms = terms;
}
public void refreshViewsForNewEntity(Entity e, boolean currentlySelected, long rowId) {
for (int i = 0; i < e.getNumFields() ; ++i) {
Object field = e.getField(i);
View view = views[i];
String form = forms[i];
if (view == null) { continue; }
if (FORM_AUDIO.equals(form)) {
ViewId uniqueId = new ViewId(rowId, i, false);
setupAudioLayout(view, (String) field, uniqueId);
}
else if(FORM_IMAGE.equals(form)) {
setupImageLayout(view, (String) field);
}
else if (FORM_GRAPH.equals(form) && field instanceof GraphData) {
int orientation = getResources().getConfiguration().orientation;
GraphView g = new GraphView(context, "");
View rendered = null;
if (renderedGraphsCache.get(i) != null) {
rendered = renderedGraphsCache.get(i).get(orientation);
}
else {
renderedGraphsCache.put(i, new Hashtable<Integer, View>());
}
if (rendered == null) {
rendered = g.getView((GraphData) field);
renderedGraphsCache.get(i).put(orientation, rendered);
}
((LinearLayout) view).removeAllViews();
((LinearLayout) view).addView(rendered, g.getLayoutParams());
view.setVisibility(VISIBLE);
}
else { //text to speech
setupTextAndTTSLayout(view, (String) field, e.getSortField(i));
}
}
if (currentlySelected) {
this.setBackgroundResource(R.drawable.grey_bordered_box);
} else {
this.setBackgroundDrawable(null);
}
}
/*
* Updates the AudioButton layout that is passed in, based on the
* new id and source
*/
private void setupAudioLayout(View layout, String source, ViewId uniqueId) {
AudioButton b = (AudioButton)layout;
if (source != null && source.length() > 0) {
b.modifyButtonForNewView(uniqueId, source, true);
}
else {
b.modifyButtonForNewView(uniqueId, source, false);
}
}
/*
* Updates the text layout that is passed in, based on the new text
*/
private void setupTextAndTTSLayout(View layout, final String text, String searchField) {
TextView tv = (TextView)layout.findViewById(R.id.component_audio_text_txt);
tv.setVisibility(View.VISIBLE);
tv.setText(highlightSearches(text == null ? "" : text, searchField));
ImageButton btn = (ImageButton)layout.findViewById(R.id.component_audio_text_btn_audio);
btn.setFocusable(false);
btn.setOnClickListener(new OnClickListener(){
/*
* (non-Javadoc)
* @see android.view.View.OnClickListener#onClick(android.view.View)
*/
@Override
public void onClick(View v) {
String textToRead = text;
tts.speak(textToRead, TextToSpeech.QUEUE_FLUSH, null);
}
});
if (tts == null || text == null || text.equals("")) {
btn.setVisibility(View.INVISIBLE);
RelativeLayout.LayoutParams params = (android.widget.RelativeLayout.LayoutParams) btn.getLayoutParams();
params.width = 0;
btn.setLayoutParams(params);
} else {
btn.setVisibility(View.VISIBLE);
RelativeLayout.LayoutParams params = (android.widget.RelativeLayout.LayoutParams) btn.getLayoutParams();
params.width = LayoutParams.WRAP_CONTENT;
btn.setLayoutParams(params);
}
}
/*
* Updates the ImageView layout that is passed in, based on the
* new id and source
*/
public void setupImageLayout(View layout, final String source) {
ImageView iv = (ImageView) layout;
Bitmap b;
if (!source.equals("")) {
try {
b = BitmapFactory.decodeStream(ReferenceManager._().DeriveReference(source).getStream());
if (b == null) {
//Input stream could not be used to derive bitmap, so showing error-indicating image
iv.setImageDrawable(getResources().getDrawable(R.drawable.ic_menu_archive));
}
else {
iv.setImageBitmap(b);
}
} catch (IOException ex) {
ex.printStackTrace();
//Error loading image
iv.setImageDrawable(getResources().getDrawable(R.drawable.ic_menu_archive));
} catch (InvalidReferenceException ex) {
ex.printStackTrace();
//No image
iv.setImageDrawable(getResources().getDrawable(R.drawable.ic_menu_archive));
}
}
else {
iv.setImageDrawable(getResources().getDrawable(R.drawable.white));
}
}
private Spannable highlightSearches(String displayString, String backgroundString) {
Spannable raw = new SpannableString(displayString);
String normalizedDisplayString = StringUtils.normalize(displayString);
if (searchTerms == null) {
return raw;
}
//Zero out the existing spans
BackgroundColorSpan[] spans=raw.getSpans(0,raw.length(), BackgroundColorSpan.class);
for (BackgroundColorSpan span : spans) {
raw.removeSpan(span);
}
Vector<int[]> matches = new Vector<int[]>();
//Highlight direct substring matches
for (String searchText : searchTerms) {
if ("".equals(searchText)) { continue;}
int index = TextUtils.indexOf(normalizedDisplayString, searchText);
while (index >= 0) {
raw.setSpan(new BackgroundColorSpan(this.getContext().getResources().getColor(R.color.yellow)), index, index
+ searchText.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
matches.add(new int[] {index, index + searchText.length() } );
index=TextUtils.indexOf(raw, searchText, index + searchText.length());
//we have a non-fuzzy match, so make sure we don't fuck with it
}
}
//now insert the spans for any fuzzy matches (if enabled)
if(mFuzzySearchEnabled && backgroundString != null) {
backgroundString = StringUtils.normalize(backgroundString).trim() + " ";
for (String searchText : searchTerms) {
if ("".equals(searchText)) { continue;}
int curStart = 0;
int curEnd = backgroundString.indexOf(" ", curStart);
while(curEnd != -1) {
boolean skip = matches.size() != 0;
//See whether the fuzzy match overlaps at all with the concrete matches
for(int[] textMatch : matches) {
if(curStart < textMatch[0] && curEnd <= textMatch[0]) {
skip = false;
} else if(curStart >= textMatch[1] && curEnd > textMatch[1]) {
skip = false;
}
}
if(!skip) {
//Walk the string to find words that are fuzzy matched
if(StringUtils.fuzzyMatch(backgroundString.substring(curStart, curEnd), searchText)) {
raw.setSpan(new BackgroundColorSpan(this.getContext().getResources().getColor(R.color.green)), curStart,
curEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
curStart = curEnd + 1;
curEnd = backgroundString.indexOf(" ", curStart);
}
}
}
return raw;
}
private float[] calculateDetailWeights(int[] hints) {
float[] weights = new float[hints.length];
int fullSize = 100;
int sharedBetween = 0;
for(int hint : hints) {
if(hint != -1) {
fullSize -= hint;
} else {
sharedBetween ++;
}
}
double average = ((double)fullSize) / (double)sharedBetween;
for(int i = 0; i < hints.length; ++i) {
int hint = hints[i];
weights[i] = hint == -1? (float)(average/100.0) : (float)(((double)hint)/100.0);
}
return weights;
}
}