package org.commcare.views; import android.app.Activity; import android.content.Context; import android.content.Intent; import android.graphics.Bitmap; import android.os.Build; import android.support.annotation.IdRes; import android.view.Display; import android.view.GestureDetector; import android.view.Gravity; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.view.WindowManager; import android.webkit.WebView; import android.widget.Button; import android.widget.FrameLayout; import android.widget.ImageButton; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.ProgressBar; import android.widget.TextView; import org.commcare.activities.CommCareGraphActivity; import org.commcare.cases.entity.Entity; import org.commcare.dalvik.R; import org.commcare.graph.model.GraphData; import org.commcare.graph.util.GraphException; import org.commcare.graph.view.GraphLoader; import org.commcare.graph.view.GraphView; import org.commcare.logging.AndroidLogger; import org.commcare.preferences.CommCarePreferences; import org.commcare.suite.model.CalloutData; import org.commcare.suite.model.Detail; import org.commcare.utils.DetailCalloutListener; import org.commcare.utils.FileUtil; import org.commcare.utils.GeoUtils; import org.commcare.utils.MediaUtil; import org.commcare.views.media.AudioPlaybackButton; import org.commcare.views.media.ViewId; import org.javarosa.core.reference.InvalidReferenceException; import org.javarosa.core.reference.ReferenceManager; import org.javarosa.core.services.Logger; import org.javarosa.core.services.locale.Localization; import java.util.HashMap; import java.util.HashSet; import java.util.Hashtable; import java.util.Set; import java.util.Timer; /** * @author ctsims */ public class EntityDetailView extends FrameLayout { private final TextView label; private final TextView data; private final TextView spacer; private final Button callout; private final View addressView; private final Button addressButton; private final TextView addressText; private final ImageView imageView; private final View calloutView; private final Button calloutButton; private final TextView calloutText; private final ImageButton calloutImageButton; private final AspectRatioLayout graphLayout; private final Hashtable<Integer, Hashtable<Integer, View>> graphViewsCache; // index => { orientation => GraphView } private final Hashtable<Integer, Intent> graphIntentsCache; // index => intent private final Set<Integer> graphsWithErrors; private final ImageButton videoButton; private final AudioPlaybackButton audioButton; private final View valuePane; private View currentView; private final LinearLayout detailRow; private final LinearLayout.LayoutParams origValue; private final LinearLayout.LayoutParams origLabel; private final LinearLayout.LayoutParams fill; // Potential "forms" of a detail field private static final String FORM_VIDEO = MediaUtil.FORM_VIDEO; private static final String FORM_AUDIO = MediaUtil.FORM_AUDIO; private static final String FORM_PHONE = "phone"; private static final String FORM_ADDRESS = "address"; private static final String FORM_IMAGE = MediaUtil.FORM_IMAGE; private static final String FORM_GRAPH = "graph"; private static final String FORM_CALLOUT = "callout"; @IdRes private static final int IMAGE_VIEW_ID = 23422634; @IdRes private static final int CALLOUT_BUTTON_ID = 23422634; private static final int TEXT = 0; private static final int PHONE = 1; private static final int ADDRESS = 2; private static final int IMAGE = 3; private static final int VIDEO = 4; private static final int AUDIO = 5; private static final int GRAPH = 6; private static final int CALLOUT = 7; private int current = TEXT; private DetailCalloutListener listener; public EntityDetailView(Context context, Detail d, Entity e, int index, int detailNumber) { super(context); detailRow = (LinearLayout)View.inflate(context, R.layout.component_entity_detail_item, null); label = (TextView)detailRow.findViewById(R.id.detail_type_text); spacer = (TextView)detailRow.findViewById(R.id.entity_detail_spacer); data = (TextView)detailRow.findViewById(R.id.detail_value_text); currentView = data; valuePane = detailRow.findViewById(R.id.detail_value_pane); videoButton = (ImageButton)detailRow.findViewById(R.id.detail_video_button); ViewId uniqueId = ViewId.buildTableViewId(detailNumber, index, true); String audioText = e.getFieldString(index); audioButton = new AudioPlaybackButton(context, audioText, uniqueId, false); detailRow.addView(audioButton); audioButton.setVisibility(View.GONE); callout = (Button)detailRow.findViewById(R.id.detail_value_phone); addressView = detailRow.findViewById(R.id.detail_address_view); addressText = (TextView)addressView.findViewById(R.id.detail_address_text); addressButton = (Button)addressView.findViewById(R.id.detail_address_button); imageView = (ImageView)detailRow.findViewById(R.id.detail_value_image); int height; if (CommCarePreferences.isSmartInflationEnabled()) { // If using smart inflation, we don't want to do any other artificial resizing of images height = LayoutParams.WRAP_CONTENT; } else { // otherwise, should let the image view stretch to fill the height of the row height = LayoutParams.MATCH_PARENT; } FrameLayout.LayoutParams imageViewParams = new FrameLayout.LayoutParams(LayoutParams.WRAP_CONTENT, height); imageView.setLayoutParams(imageViewParams); graphLayout = (AspectRatioLayout)detailRow.findViewById(R.id.graph); calloutView = detailRow.findViewById(R.id.callout_view); calloutText = (TextView)detailRow.findViewById(R.id.callout_text); calloutButton = (Button)detailRow.findViewById(R.id.callout_button); calloutImageButton = (ImageButton)detailRow.findViewById(R.id.callout_image_button); graphViewsCache = new Hashtable<>(); graphsWithErrors = new HashSet<>(); graphIntentsCache = new Hashtable<>(); origLabel = (LinearLayout.LayoutParams)label.getLayoutParams(); origValue = (LinearLayout.LayoutParams)valuePane.getLayoutParams(); fill = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT); this.addView(detailRow, FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.WRAP_CONTENT); setParams(d, e, index, detailNumber); } public void setCallListener(final DetailCalloutListener listener) { this.listener = listener; } public void setParams(Detail d, Entity e, int index, int detailNumber) { String labelText = d.getFields()[index].getHeader().evaluate(); label.setText(labelText); spacer.setText(labelText); Object field = e.getField(index); String textField = e.getFieldString(index); boolean veryLong = false; String form = d.getTemplateForms()[index]; if (FORM_PHONE.equals(form)) { setupPhoneNumber(textField); } else if (FORM_CALLOUT.equals(form) && (field instanceof CalloutData)) { veryLong = setupCallout((CalloutData)field); } else if (FORM_ADDRESS.equals(form)) { setupAddress(textField); } else if (FORM_IMAGE.equals(form)) { veryLong = setupImage(textField); } else if (FORM_GRAPH.equals(form) && field instanceof GraphData) { // if graph parsing had errors, they'll be stored as a string setupGraph(index, labelText, field); } else if (FORM_AUDIO.equals(form)) { ViewId uniqueId = ViewId.buildTableViewId(detailNumber, index, true); audioButton.modifyButtonForNewView(uniqueId, textField, true); updateCurrentView(AUDIO, audioButton); } else if (FORM_VIDEO.equals(form)) { //TODO: Why is this given a special string? setupVideo(textField); } else { data.setText((textField)); if (textField != null && textField.length() > this.getContext().getResources().getInteger(R.integer.detail_size_cutoff)) { veryLong = true; } updateCurrentView(TEXT, data); } if (veryLong) { detailRow.setOrientation(LinearLayout.VERTICAL); spacer.setVisibility(View.GONE); label.setLayoutParams(fill); valuePane.setLayoutParams(fill); } else { if (detailRow.getOrientation() != LinearLayout.HORIZONTAL) { detailRow.setOrientation(LinearLayout.HORIZONTAL); spacer.setVisibility(View.INVISIBLE); label.setLayoutParams(origLabel); valuePane.setLayoutParams(origValue); } } } private void setupPhoneNumber(String textField) { callout.setText(textField); if (current != PHONE) { callout.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { listener.callRequested(callout.getText().toString()); } }); this.removeView(currentView); updateCurrentView(PHONE, callout); } } private boolean setupCallout(final CalloutData callout) { boolean veryLong = false; String imagePath = callout.getImage(); if (imagePath != null) { // use image as button, if available calloutButton.setVisibility(View.GONE); calloutText.setVisibility(View.GONE); Bitmap b = MediaUtil.inflateDisplayImage(getContext(), imagePath); if (b == null) { calloutImageButton.setImageDrawable(null); } else { // Figure out whether our image small or large. if (b.getWidth() > (getScreenWidth() / 2)) { veryLong = true; } calloutImageButton.setPadding(10, 10, 10, 10); calloutImageButton.setAdjustViewBounds(true); calloutImageButton.setImageBitmap(b); calloutImageButton.setId(CALLOUT_BUTTON_ID); } calloutImageButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { listener.performCallout(callout, CALLOUT); } }); } else { calloutImageButton.setVisibility(View.GONE); calloutText.setVisibility(View.GONE); String displayName = callout.getDisplayName(); // use display name if available, otherwise use URI if (displayName != null) { calloutButton.setText(displayName); } else { String actionName = callout.getActionName(); calloutButton.setText(actionName); } calloutButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { listener.performCallout(callout, CALLOUT); } }); } updateCurrentView(CALLOUT, calloutView); return veryLong; } private void setupAddress(final String address) { addressText.setText(address); if (current != ADDRESS) { addressButton.setText(Localization.get("select.address.show")); addressButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { listener.addressRequested(GeoUtils.getGeoIntentURI(address)); } }); updateCurrentView(ADDRESS, addressView); } } private boolean setupImage(String textField) { boolean veryLong = false; Bitmap b = MediaUtil.inflateDisplayImage(getContext(), textField); if (b == null) { imageView.setImageDrawable(null); } else { //Ok, so. We should figure out whether our image is large or small. if (b.getWidth() > (getScreenWidth() / 2)) { veryLong = true; } imageView.setPadding(10, 10, 10, 10); imageView.setAdjustViewBounds(true); imageView.setImageBitmap(b); imageView.setId(IMAGE_VIEW_ID); } updateCurrentView(IMAGE, imageView); return veryLong; } private void setupGraph(int index, String labelText, Object field) { // Get graph view and intent int orientation = getResources().getConfiguration().orientation; boolean cached = true; View graphView = getGraphViewFromCache(index, orientation); if (graphView == null) { cached = false; graphView = getGraphView(index, labelText, (GraphData)field, orientation); } final Intent finalIntent = getGraphIntent(index, labelText, (GraphData) field); // Open full-screen graph intent on double tap if (!graphsWithErrors.contains(index)) { enableGraphIntent((WebView) graphView, finalIntent); } // Add graph child views to graph layout graphLayout.removeAllViews(); graphLayout.addView(graphView, GraphView.getLayoutParams()); if (!cached && !graphsWithErrors.contains(index)) { addSpinnerToGraph((WebView) graphView, graphLayout); } if (current != GRAPH) { // Hide field label and expand value to take up full screen width LinearLayout.LayoutParams graphValueLayout = new LinearLayout.LayoutParams((ViewGroup.LayoutParams)origValue); graphValueLayout.weight = 10; valuePane.setLayoutParams(graphValueLayout); label.setVisibility(View.GONE); data.setVisibility(View.GONE); updateCurrentView(GRAPH, graphLayout); } } private void setupVideo(String textField) { String localLocation = null; try { localLocation = ReferenceManager.instance().DeriveReference(textField).getLocalURI(); if (localLocation.startsWith("/")) { //TODO: This should likely actually be happening with the getLocalURI _anyway_. localLocation = FileUtil.getGlobalStringUri(localLocation); } } catch (InvalidReferenceException ire) { Logger.log(AndroidLogger.TYPE_ERROR_CONFIG_STRUCTURE, "Couldn't understand video reference format: " + localLocation + ". Error: " + ire.getMessage()); } final String location = localLocation; videoButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { listener.playVideo(location); } }); if (location == null) { videoButton.setEnabled(false); Logger.log(AndroidLogger.TYPE_ERROR_CONFIG_STRUCTURE, "No local video reference available for ref: " + textField); } else { videoButton.setEnabled(true); } updateCurrentView(VIDEO, videoButton); } private void updateCurrentView(int newCurrent, View newView) { if (newCurrent != current) { currentView.setVisibility(View.GONE); newView.setVisibility(View.VISIBLE); currentView = newView; current = newCurrent; } if (current != GRAPH) { label.setVisibility(View.VISIBLE); LinearLayout.LayoutParams graphValueLayout = new LinearLayout.LayoutParams((ViewGroup.LayoutParams)origValue); graphValueLayout.weight = 10; valuePane.setLayoutParams(origValue); } } private int getScreenWidth() { Display display = ((WindowManager)this.getContext().getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay(); return display.getWidth(); } @SuppressWarnings("AddJavascriptInterface") private void addSpinnerToGraph(WebView graphView, ViewGroup graphLayout) { // WebView.addJavascriptInterface should not be called with minSdkVersion < 17 // for security reasons: JavaScript can use reflection to manipulate application if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) { return; } final ProgressBar spinner = new ProgressBar(this.getContext(), null, android.R.attr.progressBarStyleLarge); spinner.setLayoutParams(new FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.CENTER)); GraphLoader graphLoader = new GraphLoader((Activity) this.getContext(), spinner); // Set up interface that JavaScript will call to hide the spinner // once the graph has finished rendering. graphView.addJavascriptInterface(graphLoader, "Android"); // The above JavaScript interface doesn't load properly 100% of the time. // Worst case, hide the spinner after ten seconds. Timer spinnerTimer = new Timer(); spinnerTimer.schedule(graphLoader, 10000); graphLayout.addView(spinner); } private View getGraphViewFromCache(int index, int orientation) { if (graphViewsCache.get(index) != null) { return graphViewsCache.get(index).get(orientation); } graphViewsCache.put(index, new Hashtable<Integer, View>()); return null; } /** * Generate graph view. May return WebView displaying graph, or TextView displaying error. */ private View getGraphView(int index, String title, GraphData field, int orientation) { Context context = getContext(); View graphView; GraphView g = new GraphView(context, title, false); try { String graphHTML = field.getGraphHTML(title); graphView = g.getView(graphHTML); graphLayout.setRatio((float)g.getRatio(field), (float)1); } catch (GraphException ex) { graphView = new TextView(context); int padding = (int)context.getResources().getDimension(R.dimen.spacer_small); graphView.setPadding(padding, padding, padding, padding); ((TextView)graphView).setText(ex.getMessage()); graphsWithErrors.add(index); } graphViewsCache.get(index).put(orientation, graphView); return graphView; } /** * Fetch full-screen graph intent from cache, or create it. */ private Intent getGraphIntent(int index, String title, GraphData field) { Intent graphIntent = graphIntentsCache.get(index); if (graphIntent == null && !graphsWithErrors.contains(index)) { GraphView g = new GraphView(this.getContext(), title, true); try { String html = field.getGraphHTML(title); graphIntent = g.getIntent(html, CommCareGraphActivity.class); graphIntentsCache.put(index, graphIntent); } catch (GraphException ex) { graphsWithErrors.add(index); } } return graphIntent; } /** * Set up event handling so that full-screen graph intent opens on double tap of given view. */ private void enableGraphIntent(WebView graphView, final Intent graphIntent) { final Context context = this.getContext(); final GestureDetector detector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() { @Override public boolean onDown(MotionEvent e) { return true; } @Override public boolean onDoubleTap(MotionEvent e) { context.startActivity(graphIntent); return true; } }); graphView.setOnTouchListener(new OnTouchListener() { @Override public boolean onTouch(View view, MotionEvent event) { return detector.onTouchEvent(event); } }); } }