package org.odk.collect.android.widgets; import java.io.File; import org.javarosa.core.model.FormIndex; import org.javarosa.core.model.data.AnswerDataFactory; import org.javarosa.core.model.data.IAnswerData; import org.javarosa.form.api.FormEntryPrompt; import org.odk.collect.android.R; import org.odk.collect.android.R.color; import org.odk.collect.android.application.Collect; import org.odk.collect.android.listeners.WidgetChangedListener; import org.odk.collect.android.preferences.PreferencesActivity; import org.odk.collect.android.utilities.FileUtils; import org.odk.collect.android.utilities.StringUtils; import org.odk.collect.android.views.ShrinkingTextView; import org.odk.collect.android.views.media.MediaLayout; import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; import android.content.SharedPreferences; import android.graphics.Rect; import android.graphics.Typeface; import android.preference.PreferenceManager; import android.text.Spannable; import android.text.TextPaint; import android.text.style.URLSpan; import android.text.util.Linkify; import android.util.TypedValue; import android.view.Gravity; import android.view.View; import android.view.animation.Animation; import android.view.animation.Transformation; import android.widget.FrameLayout; import android.widget.LinearLayout; import android.widget.ScrollView; import android.widget.TextView; public abstract class QuestionWidget extends LinearLayout { @SuppressWarnings("unused") private final static String t = "QuestionWidget"; private LinearLayout.LayoutParams mLayout; protected FormEntryPrompt mPrompt; protected final int mQuestionFontsize; protected final int mAnswerFontsize; protected final static String ACQUIREFIELD = "acquire"; //the height of the "Frame" available to this widget. The frame //is the size of the parent that is available (it is roughly //the window without the keyboard/top bars/etc.) //Note that this value is only populated after the widget is //drawn for now. protected int mFrameHeight = -1; private TextView mQuestionText; private FrameLayout helpPlaceholder; private ShrinkingTextView mHelpText; protected boolean hasListener; private View toastView; //Whether this question widget needs to request focus on //its next draw, due to a new element having been added (which couldn't have //requested focus yet due to having not been layed out) protected boolean focusPending = false; protected WidgetChangedListener widgetChangedListener; public QuestionWidget(Context context, FormEntryPrompt p) { this(context, p, null); } public QuestionWidget(Context context, FormEntryPrompt p, WidgetChangedListener w){ super(context); if(w!=null){ hasListener = false; widgetChangedListener = w; } this.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { QuestionWidget.this.acceptFocus(); } }); hasListener = (w != null); SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(context.getApplicationContext()); String question_font = settings.getString(PreferencesActivity.KEY_FONT_SIZE, Collect.DEFAULT_FONTSIZE); mQuestionFontsize = new Integer(question_font).intValue(); mAnswerFontsize = mQuestionFontsize + 2; mPrompt = p; setOrientation(LinearLayout.VERTICAL); setGravity(Gravity.TOP); setPadding(0, 7, 0, 0); mLayout = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.FILL_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT); mLayout.setMargins(10, 0, 10, 0); addQuestionText(p); addHelpText(p); addHelpPlaceholder(p); } protected void acceptFocus() { } private void addHelpPlaceholder(FormEntryPrompt p) { helpPlaceholder = new FrameLayout(this.getContext()); helpPlaceholder.setLayoutParams(new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.WRAP_CONTENT)); if("help".equals(p.getSpecialFormQuestionText("help"))) { String specialHelpText = p.getSpecialFormQuestionText("help-text"); String specialHelpImage = p.getSpecialFormQuestionText("help-image"); String specialHelpVideo = p.getSpecialFormQuestionText("help-video"); TextView helpText = new TextView(getContext()); helpText.setText(specialHelpText); helpText.setTextSize(TypedValue.COMPLEX_UNIT_DIP, mQuestionFontsize); helpText.setPadding(0, 0, 0, 7); helpText.setId(38475483); // assign random id MediaLayout helpLayout = new MediaLayout(getContext()); helpLayout.setAVT(helpText, null, specialHelpImage, specialHelpVideo, null); helpLayout.setPadding(15, 15, 15, 15); helpLayout.setBackgroundResource(color.very_light_blue); helpPlaceholder.addView(helpLayout); } this.addView(helpPlaceholder); helpPlaceholder.setVisibility(View.GONE); } public FormEntryPrompt getPrompt() { return mPrompt; } // Abstract methods public abstract IAnswerData getAnswer(); public abstract void clearAnswer(); public abstract void setFocus(Context context); public abstract void setOnLongClickListener(OnLongClickListener l); private class URLSpanNoUnderline extends URLSpan { public URLSpanNoUnderline(String url) { super(url); } /* * (non-Javadoc) * @see android.text.style.ClickableSpan#updateDrawState(android.text.TextPaint) */ @Override public void updateDrawState(TextPaint ds) { super.updateDrawState(ds); ds.setUnderlineText(false); } } public void notifyOnScreen(String text, boolean strong){ notifyOnScreen(text, strong, true); } /** * Add notification (e.g., validation error) to this question. * @param text Text of message. * @param strong If true, display a visually stronger, negative background. * @param requestFocus If true, bring focus to this question. */ public void notifyOnScreen(String text, boolean strong, boolean requestFocus){ if(strong){ this.setBackgroundDrawable(this.getContext().getResources().getDrawable(R.drawable.bubble_invalid)); } else{ this.setBackgroundDrawable(this.getContext().getResources().getDrawable(R.drawable.bubble_warn)); } if(this.toastView == null) { this.toastView = View.inflate(this.getContext(), R.layout.toast_view, this).findViewById(R.id.toast_view_root); focusPending = requestFocus; } else { if(this.toastView.getVisibility() != View.VISIBLE) { this.toastView.setVisibility(View.VISIBLE); focusPending = requestFocus; } } TextView messageView = (TextView)this.toastView.findViewById(R.id.message); messageView.setText(text); //If the toastView already exists, we can just scroll to it right now //if not, we actually have to do it later, when we lay this all back out if(!focusPending && requestFocus) { requestChildViewOnScreen(messageView); } } public void notifyWarning(String text) { notifyOnScreen(text, false); } public void notifyInvalid(String text) { notifyInvalid(text, true); } public void notifyInvalid(String text, boolean requestFocus) { notifyOnScreen(text, true, requestFocus); } /* * Use to signal that there's a portion of this view that wants to be * visible to the user on the screen. This method will place the sub * view on the screen, and will also place as much of this view as possible * on the screen. If this view is smaller than the viewable area available, it * will be fully visible in addition to the subview. */ private void requestChildViewOnScreen(View child) { //Take focus so the user can be prepared to interact with this question, since //they will need to be fixing the input acceptFocus(); //Get the rectangle that wants to put itself on the screen Rect vitalPortion = new Rect(); child.getDrawingRect(vitalPortion); //Save a reference to it in case we have to manipulate it later. Rect vitalPortionSaved = new Rect(); child.getDrawingRect(vitalPortionSaved); //Then get the bounding rectangle for this whole view. Rect wholeView = new Rect(); this.getDrawingRect(wholeView); //If we don't know enough about the screen, just default to asking to see the //subview that was requested. if(mFrameHeight == -1){ child.requestRectangleOnScreen(vitalPortion); return; } //If the whole view fits, just request that we display the whole thing. if(wholeView.height() < mFrameHeight) { this.requestRectangleOnScreen(wholeView); return; } //The whole view will not fit, we need to scale down our requested focus. //Trying to construct the "ideal" rectangle here is actually pretty hard //but the base case is just to see if we can get the view onto the screen from //the bottom or the top int topY = wholeView.top; int bottomY = wholeView.bottom; //shrink the view to contain only the current frame size. wholeView.inset(0, (wholeView.height() - mFrameHeight) / 2); wholeView.offsetTo(wholeView.left, topY); //The view is now the size of the frame and anchored back at the top. //Now let's contextualize where the child view actually is in this frame. this.offsetDescendantRectToMyCoords(child, vitalPortion); //If the newly transformed view now contains the child portion, we're good if(wholeView.contains(vitalPortion)) { this.requestRectangleOnScreen(wholeView); return; } //otherwise, move to the requested frame to be at the bottom of this view wholeView.offsetTo(wholeView.left, bottomY - wholeView.height()); //now see if the transformed view contains the vital portion if(wholeView.contains(vitalPortion)) { this.requestRectangleOnScreen(wholeView); return; } //Otherwise the child is hidden in the frame, so it won't matter which //we choose. child.requestRectangleOnScreen(vitalPortionSaved); } protected void onLayout(boolean changed, int l, int t, int r, int b) { super.onLayout(changed, l, t, r, b); //If we're coming back in after we just laid out adding a new element that needs //focus, we can now scroll to it, since it's actually had its spacing declared. if(changed && focusPending) { focusPending = false; if(this.toastView == null) { //NOTE: This shouldn't be possible, but if it doesn't happen //we don't wanna crash. Look here if focus isn't getting grabbed //for some reason (there's no other negative consequence) } else { TextView messageView = (TextView)this.toastView.findViewById(R.id.message); requestChildViewOnScreen(messageView); } } } private void stripUnderlines(TextView textView) { Spannable s = (Spannable)textView.getText(); URLSpan[] spans = s.getSpans(0, s.length(), URLSpan.class); for (URLSpan span: spans) { int start = s.getSpanStart(span); int end = s.getSpanEnd(span); s.removeSpan(span); span = new URLSpanNoUnderline(span.getURL()); s.setSpan(span, start, end, 0); } textView.setText(s); } /** * Add a Views containing the question text, audio (if applicable), and image (if applicable). * To satisfy the RelativeLayout constraints, we add the audio first if it exists, then the * TextView to fit the rest of the space, then the image if applicable. */ protected void addQuestionText(final FormEntryPrompt p) { String imageURI = p.getImageText(); String audioURI = p.getAudioText(); String videoURI = p.getSpecialFormQuestionText("video"); String qrCodeContent = p.getSpecialFormQuestionText("qrcode"); // shown when image is clicked String bigImageURI = p.getSpecialFormQuestionText("big-image"); // Add the text view. Textview always exists, regardless of whether there's text. mQuestionText = new TextView(getContext()); mQuestionText.setText(p.getLongText()); mQuestionText.setTextSize(TypedValue.COMPLEX_UNIT_DIP, mQuestionFontsize); mQuestionText.setTypeface(null, Typeface.BOLD); mQuestionText.setPadding(0, 0, 0, 7); mQuestionText.setId(38475483); // assign random id if(p.getLongText()!= null){ if(p.getLongText().contains("\u260E")){ if(Linkify.addLinks(mQuestionText,Linkify.PHONE_NUMBERS)){ stripUnderlines(mQuestionText); } else{ System.out.println("this should be an error I'm thinking?"); } } } // Wrap to the size of the parent view mQuestionText.setHorizontallyScrolling(false); if (p.getLongText() == null) { mQuestionText.setVisibility(GONE); } // Create the layout for audio, image, text MediaLayout mediaLayout = new MediaLayout(getContext()) { protected void onHelpPressed() { fireHelpText(p); } }; String helpText = p.getSpecialFormQuestionText("help"); if("help".equals(helpText)) { videoURI = helpText; } mediaLayout.setAVT(mQuestionText, audioURI, imageURI, videoURI, bigImageURI, qrCodeContent); addView(mediaLayout, mLayout); } private void fireHelpText(FormEntryPrompt prompt) { if(!PreferenceManager.getDefaultSharedPreferences(this.getContext().getApplicationContext()). getBoolean(PreferencesActivity.KEY_HELP_MODE_TRAY, false)) { AlertDialog mAlertDialog = new AlertDialog.Builder(this.getContext()).create(); mAlertDialog.setIcon(android.R.drawable.ic_dialog_info); mAlertDialog.setTitle(""); String specialHelpText = prompt.getSpecialFormQuestionText("help-text"); String specialHelpImage = prompt.getSpecialFormQuestionText("help-image"); String specialHelpVideo = prompt.getSpecialFormQuestionText("help-video"); ScrollView scrollView = new ScrollView(this.getContext()); TextView helpText = new TextView(getContext()); helpText.setText(specialHelpText); helpText.setTextSize(TypedValue.COMPLEX_UNIT_DIP, mQuestionFontsize); helpText.setPadding(0, 0, 0, 7); helpText.setId(38475483); // assign random id MediaLayout helpLayout = new MediaLayout(getContext()); helpLayout.setAVT(helpText, null, specialHelpImage, specialHelpVideo, null); helpLayout.setPadding(15, 15, 15, 15); scrollView.addView(helpLayout); mAlertDialog.setView(scrollView); //mAlertDialog.setMessage(); DialogInterface.OnClickListener errorListener = new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int i) { switch (i) { case DialogInterface.BUTTON1: dialog.cancel(); break; } } }; mAlertDialog.setCancelable(true); mAlertDialog.setButton(StringUtils.getStringRobust(this.getContext(), R.string.ok), errorListener); mAlertDialog.show(); } else { if(helpPlaceholder.getVisibility() == View.GONE) { expand(helpPlaceholder); } else { collapse(helpPlaceholder); } } } public static void expand(final View v) { v.measure(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); final int targetHeight = v.getMeasuredHeight(); v.getLayoutParams().height = 0; v.setVisibility(View.VISIBLE); Animation a = new Animation() { @Override protected void applyTransformation(float interpolatedTime, Transformation t) { v.getLayoutParams().height = interpolatedTime == 1 ? LayoutParams.WRAP_CONTENT : (int)(targetHeight * interpolatedTime); v.requestLayout(); } @Override public boolean willChangeBounds() { return true; } }; // 1dp/ms a.setDuration((int)(targetHeight / v.getContext().getResources().getDisplayMetrics().density)); v.startAnimation(a); } public static void collapse(final View v) { final int initialHeight = v.getMeasuredHeight(); Animation a = new Animation() { @Override protected void applyTransformation(float interpolatedTime, Transformation t) { if(interpolatedTime == 1){ v.setVisibility(View.GONE); }else{ v.getLayoutParams().height = initialHeight - (int)(initialHeight * interpolatedTime); v.requestLayout(); } } @Override public boolean willChangeBounds() { return true; } }; // 1dp/ms a.setDuration((int)(initialHeight / v.getContext().getResources().getDisplayMetrics().density)); v.startAnimation(a); } public void updateFrameSize(int width, int height) { int maxHelpHeight = height / 4; if(mHelpText != null) { mHelpText.updateMaxHeight(maxHelpHeight); } mFrameHeight = height; } /** * Add a TextView containing the help text. */ private void addHelpText(FormEntryPrompt p) { String s = p.getHelpText(); if (s != null && !s.equals("")) { mHelpText = new ShrinkingTextView(getContext(),this.getMaxHintHeight()); mHelpText.setTextSize(TypedValue.COMPLEX_UNIT_DIP, mQuestionFontsize - 3); mHelpText.setPadding(0, -5, 0, 7); // wrap to the widget of view mHelpText.setHorizontallyScrolling(false); mHelpText.setText(s); mHelpText.setTypeface(null, Typeface.ITALIC); addView(mHelpText, mLayout); } } protected int getMaxHintHeight() { return -1; } /** * Every subclassed widget should override this, adding any views they may contain, and calling * super.cancelLongPress() */ public void cancelLongPress() { super.cancelLongPress(); if (mQuestionText != null) { mQuestionText.cancelLongPress(); } if (mHelpText != null) { mHelpText.cancelLongPress(); } } protected IAnswerData getCurrentAnswer() { IAnswerData current = mPrompt.getAnswerValue(); if(current == null) { return null; } return getTemplate().cast(current.uncast()); } protected IAnswerData getTemplate() { return AnswerDataFactory.template(mPrompt.getControlType(), mPrompt.getDataType()); } public void hideHintText() { mHelpText.setVisibility(View.GONE); } public FormIndex getFormId(){ return mPrompt.getIndex(); } public void setChangedListener(WidgetChangedListener wcl){ widgetChangedListener = wcl; hasListener = true; } public void widgetEntryChanged(){ if(this.toastView != null) { this.toastView.setVisibility(View.GONE); this.setBackgroundDrawable(null); } if(hasListener){ widgetChangedListener.widgetEntryChanged(); } } public void checkFileSize(File file){ if(FileUtils.isFileOversized(file)){ this.notifyWarning(StringUtils.getStringRobust(getContext(), R.string.attachment_oversized, FileUtils.getFileSize(file)+"")); } } public void checkFileSize(String filepath){ checkFileSize(new File(filepath)); } }