/*
* Copyright (C) 2010-2016 Stichting Akvo (Akvo Foundation)
*
* This file is part of Akvo Flow.
*
* Akvo Flow is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Akvo Flow is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Akvo Flow. If not, see <http://www.gnu.org/licenses/>.
*/
package org.akvo.flow.ui.view;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.text.Html;
import android.text.Spanned;
import android.text.TextUtils;
import android.view.View;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.RadioButton;
import android.widget.RadioGroup;
import android.widget.RadioGroup.OnCheckedChangeListener;
import android.widget.TextView;
import android.widget.TextView.BufferType;
import org.akvo.flow.R;
import org.akvo.flow.domain.AltText;
import org.akvo.flow.domain.Option;
import org.akvo.flow.domain.Question;
import org.akvo.flow.domain.QuestionResponse;
import org.akvo.flow.event.SurveyListener;
import org.akvo.flow.serialization.response.value.OptionValue;
import org.akvo.flow.util.ConstantUtil;
import java.util.ArrayList;
import java.util.List;
/**
* Question type that supports the selection of a single option from a list of
* choices (i.e. a radio button group).
*
* @author Christopher Fagiani
*/
public class OptionQuestionView extends QuestionView {
private static final String OTHER_CODE = "OTHER";
private final String OTHER_TEXT;
private RadioGroup mOptionGroup;
private List<CheckBox> mCheckBoxes;
private TextView mOtherText;
private List<Option> mOptions;
private volatile boolean mSuppressListeners = false;
private String mLatestOtherText;
public OptionQuestionView(Context context, Question q, SurveyListener surveyListener) {
super(context, q, surveyListener);
OTHER_TEXT = getResources().getString(R.string.othertext);
init();
}
private void init() {
// Just inflate the header. Options will be added dynamically
setQuestionView(R.layout.question_header);
mOptions = mQuestion.getOptions();
if (mOptions == null) {
return;
}
mSuppressListeners = true;
// Append 'other' option, if necessary
if (mQuestion.isAllowOther()) {
Option other = new Option();
other.setIsOther(true);
// Only add code if codes are defined
if (!mOptions.isEmpty() && !TextUtils.isEmpty(mOptions.get(0).getCode())) {
other.setCode(OTHER_CODE);
}
mOptions.add(other);
}
if (mQuestion.isAllowMultiple()) {
mCheckBoxes = new ArrayList<>();
} else {
mOptionGroup = new RadioGroup(getContext());
mOptionGroup.setOnCheckedChangeListener(new OnCheckedChangeListener() {
public void onCheckedChanged(RadioGroup group, int checkedId) {
handleSelection(checkedId, true);
}
});
addView(mOptionGroup);
}
for (int i = 0; i < mOptions.size(); i++) {
Option option = mOptions.get(i);
View view;
if (mQuestion.isAllowMultiple()) {
view = newCheckbox(option);
mCheckBoxes.add((CheckBox)view);
addView(view);
} else {
view = newRadioButton(option);
mOptionGroup.addView(view);
}
view.setEnabled(!isReadOnly());
view.setId(i);// View ID will match option position within the array
}
if (mQuestion.isAllowOther()) {
mOtherText = new TextView(getContext());
mOtherText.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
addView(mOtherText);
}
mSuppressListeners = false;
}
private View newRadioButton(Option option) {
RadioButton rb = new RadioButton(getContext());
rb.setLayoutParams(new RadioGroup.LayoutParams(RadioGroup.LayoutParams.MATCH_PARENT,
LayoutParams.WRAP_CONTENT));
rb.setLongClickable(true);
rb.setOnLongClickListener(new OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
onClearAnswer();
return true;
}
});
if (option.isOther()) {
rb.setText(OTHER_TEXT);
} else {
rb.setText(formOptionText(option), BufferType.SPANNABLE);
}
return rb;
}
private View newCheckbox(Option option) {
CheckBox box = new CheckBox(getContext());
box.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
box.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
handleSelection(buttonView.getId(), isChecked);
}
});
if (option.isOther()) {
box.setText(OTHER_TEXT);
} else {
box.setText(formOptionText(option), BufferType.SPANNABLE);
}
return box;
}
@Override
public void notifyOptionsChanged() {
super.notifyOptionsChanged();
if (mQuestion.isAllowMultiple()) {
for (int i = 0; i < mCheckBoxes.size(); i++) {
// make sure we have a corresponding option (i.e. not the OTHER option)
if (!mOptions.get(i).isOther()) {
mCheckBoxes.get(i).setText(formOptionText(mOptions.get(i)), BufferType.SPANNABLE);
}
}
} else {
for (int i = 0; i < mOptionGroup.getChildCount(); i++) {
// make sure we have a corresponding option (i.e. not the OTHER option)
if (!mOptions.get(i).isOther()) {
((RadioButton) (mOptionGroup.getChildAt(i))).setText(formOptionText(mOptions.get(i)));
}
}
}
}
/**
* forms the text for an option based on the visible languages
*/
private Spanned formOptionText(Option option) {
boolean isFirst = true;
StringBuilder text = new StringBuilder();
final String[] langs = getLanguages();
for (int i = 0; i < langs.length; i++) {
if (getDefaultLang().equalsIgnoreCase(langs[i])) {
if (!isFirst) {
text.append(" / ");
} else {
isFirst = false;
}
text.append(TextUtils.htmlEncode(option.getText()));
} else {
AltText txt = option.getAltText(langs[i]);
if (txt != null) {
if (!isFirst) {
text.append(" / ");
} else {
isFirst = false;
}
text.append("<font color='")
.append(sColors[i])
.append("'>")
.append(TextUtils.htmlEncode(txt.getText()))
.append("</font>");
}
}
}
return Html.fromHtml(text.toString());
}
/**
* populates the QuestionResponse object based on the current state of the
* selected option(s)
*/
private void handleSelection(int checkedId, boolean isChecked) {
if (mSuppressListeners) {
return;
}
if (mOptions.get(checkedId).isOther() && isChecked) {
displayOtherDialog(checkedId);
return;
}
captureResponse();
}
/**
* Forms a delimited string containing all selected options not including OTHER
*/
private List<Option> getSelection() {
List<Option> options = new ArrayList<>();
if (mQuestion.isAllowMultiple()) {
for (CheckBox cb: mCheckBoxes) {
if (cb.isChecked()) {
Option option = mOptions.get(cb.getId());
options.add(option);
}
}
} else {
int checked = mOptionGroup.getCheckedRadioButtonId();
if (checked != -1) {
options.add(mOptions.get(checked));
}
}
return options;
}
/**
* displays a pop-up dialog where the user can enter in a specific value for
* the "OTHER" option in a freetext view.
*/
private void displayOtherDialog(final int otherId) {
LinearLayout main = new LinearLayout(getContext());
main.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
main.setOrientation(LinearLayout.VERTICAL);
final EditText inputView = new EditText(getContext());
inputView.setSingleLine();
if (!TextUtils.isEmpty(mLatestOtherText)) {
inputView.append(mLatestOtherText);
}
main.addView(inputView);
AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
builder.setTitle(R.string.otherinstructions);
builder.setView(main);
builder.setPositiveButton(R.string.okbutton,
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
mLatestOtherText = inputView.getText().toString().trim();
mOptions.get(otherId).setText(mLatestOtherText);
captureResponse();
// update the UI with the other text
if (mOtherText != null) {
mOtherText.setText(mLatestOtherText);
}
}
});
builder.setNegativeButton(R.string.cancelbutton,
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
// Deselect 'other'
handleSelection(otherId, false);
}
});
builder.show();
}
/**
* checks off the correct option based on the response value
*/
@Override
public void rehydrate(QuestionResponse resp) {
super.rehydrate(resp);
if (resp == null || TextUtils.isEmpty(resp.getValue())) {
return;
}
List<Option> selectedOptions = OptionValue.deserialize(resp.getValue());
if (selectedOptions == null || selectedOptions.isEmpty()) {
return;
}
mSuppressListeners = true;
for (Option selectedOption : selectedOptions) {
for (int i=0; i<mOptions.size(); i++) {
Option option = mOptions.get(i);
boolean match = selectedOption.equals(option);
if (!match && option.isOther()) {
// Assume this is the OTHER value. A more reliable indicator would be to check
// selected response's `isOther` flag, but this is not guaranteed to be present
// in old responses.
match = true;
mLatestOtherText = selectedOption.getText();
mOtherText.setText(mLatestOtherText);
option.setText(mLatestOtherText);
}
if (match) {
if (mQuestion.isAllowMultiple()) {
mCheckBoxes.get(i).setChecked(true);
} else {
mOptionGroup.check(i);
}
break;// TODO: Break outer loop in single-choice responses?
}
}
}
mSuppressListeners = false;
}
/**
* clears the selected option
*/
@Override
public void resetQuestion(boolean fireEvent) {
super.resetQuestion(fireEvent);
mSuppressListeners = true;
if (mOptionGroup != null) {
mOptionGroup.clearCheck();
}
if (mCheckBoxes != null) {
for (int i = 0; i < mCheckBoxes.size(); i++) {
mCheckBoxes.get(i).setChecked(false);
}
}
mSuppressListeners = false;
}
@Override
public void setTextSize(float size) {
super.setTextSize(size);
if (mOptionGroup != null && mOptionGroup.getChildCount() > 0) {
for (int i = 0; i < mOptionGroup.getChildCount(); i++) {
((RadioButton) (mOptionGroup.getChildAt(i))).setTextSize(size);
}
} else if (mCheckBoxes != null && mCheckBoxes.size() > 0) {
for (int i = 0; i < mCheckBoxes.size(); i++) {
mCheckBoxes.get(i).setTextSize(size);
}
}
}
@Override
public void captureResponse(boolean suppressListeners) {
String response = null;
List<Option> values = getSelection();
if (!values.isEmpty()) {
response = OptionValue.serialize(values);
}
setResponse(new QuestionResponse(response, ConstantUtil.OPTION_RESPONSE_TYPE,
getQuestion().getId()), suppressListeners);
}
}