/*
* Copyright (C) 2014-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.animation.LayoutTransition;
import android.content.Context;
import android.os.Build;
import android.os.Bundle;
import android.view.View;
import android.widget.LinearLayout;
import android.widget.ScrollView;
import android.widget.TextView;
import org.akvo.flow.R;
import org.akvo.flow.domain.Dependency;
import org.akvo.flow.domain.Question;
import org.akvo.flow.domain.QuestionGroup;
import org.akvo.flow.domain.QuestionResponse;
import org.akvo.flow.event.QuestionInteractionListener;
import org.akvo.flow.event.SurveyListener;
import org.akvo.flow.util.ConstantUtil;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class QuestionGroupTab extends LinearLayout implements RepetitionHeader.OnDeleteListener {
private QuestionGroup mQuestionGroup;
private QuestionInteractionListener mQuestionListener;
private SurveyListener mSurveyListener;
private Map<String, QuestionView> mQuestionViews;
private final Set<String> mQuestions;// Map group's questions for a quick look-up
private LinearLayout mContainer;
private ScrollView mScroller;
private boolean mLoaded;
private TextView mRepetitionsText;
private Map<Integer, RepetitionHeader> mHeaders;
private Repetitions mRepetitions;// Repetition IDs
public QuestionGroupTab(Context context, QuestionGroup group, SurveyListener surveyListener,
QuestionInteractionListener questionListener) {
super(context);
mQuestionGroup = group;
mSurveyListener = surveyListener;
mQuestionListener = questionListener;
mQuestionViews = new HashMap<>();
mHeaders = new HashMap<>();
mRepetitions = new Repetitions();
mLoaded = false;
mQuestions = new HashSet<>();
for (Question q : mQuestionGroup.getQuestions()) {
mQuestions.add(q.getId());
}
init();
}
private void init() {
setOrientation(VERTICAL);
setDescendantFocusability(FOCUS_BEFORE_DESCENDANTS);
setFocusable(true);
setFocusableInTouchMode(true);
inflate(getContext(), R.layout.question_group_tab, this);
mScroller = (ScrollView) findViewById(R.id.scroller);
mContainer = (LinearLayout) findViewById(R.id.question_list);
mRepetitionsText = (TextView) findViewById(R.id.repeat_header);
// Animate view additions/removals if possible
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
mContainer.setLayoutTransition(new LayoutTransition());
}
View next = findViewById(R.id.next_btn);
next.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
mSurveyListener.nextTab();
}
});
next.setVisibility(mSurveyListener.isReadOnly() ? GONE : VISIBLE);
if (mQuestionGroup.isRepeatable()) {
findViewById(R.id.repeat_header).setVisibility(VISIBLE);
View repeatBtn = findViewById(R.id.repeat_btn);
repeatBtn.setVisibility(mSurveyListener.isReadOnly() ? GONE : VISIBLE);
repeatBtn.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
loadGroup();
setupDependencies();
}
});
}
}
/**
* Pre-load all the QuestionViews in memory. getView() will simply
* retrieve them from the corresponding position in mQuestionViews.
*/
public void load() {
mLoaded = true;
loadGroup();
}
public void notifyOptionsChanged() {
for (QuestionView qv : mQuestionViews.values()) {
qv.notifyOptionsChanged();
}
}
public void onQuestionComplete(String questionId, Bundle data) {
QuestionView qv = mQuestionViews.get(questionId);
if (qv != null) {
qv.questionComplete(data);
}
}
/**
* Checks to make sure the mandatory questions in this tab have a response
*/
public List<Question> checkInvalidQuestions() {
List<Question> missingQuestions = new ArrayList<>();
for (QuestionView qv : mQuestionViews.values()) {
qv.checkMandatory();
if (!qv.isValid() && qv.areDependenciesSatisfied()) {
// Only considered invalid if the dependencies are fulfilled
missingQuestions.add(qv.getQuestion());
}
}
return missingQuestions;
}
public void loadState() {
for (QuestionView qv : mQuestionViews.values()) {
qv.resetQuestion(false);// Clean start
}
// FIXME: Call loadGroup here, remove original one
// If the group is repeatable, delete multiple iterations
if (mQuestionGroup.isRepeatable()) {
mContainer.removeAllViews();
mQuestionViews.clear();
// Load existing iterations. If no iteration is available, show one by default.
mRepetitions.loadIDs();
int iterCount = Math.max(mRepetitions.size(), 1);
for (int i = 0; i < iterCount; i++) {
loadGroup(i);
}
}
displayResponses();
}
private void displayResponses() {
Map<String, QuestionResponse> responses = mSurveyListener.getResponses();
for (QuestionView qv : mQuestionViews.values()) {
final String questionId = qv.getQuestion().getId();
if (responses.containsKey(questionId)) {
// Update the question view to reflect the loaded data
qv.rehydrate(responses.get(questionId));
}
}
}
public QuestionView getQuestionView(String questionId) {
return mQuestionViews.get(questionId);
}
/**
* Attempt to display a particular question, based on the given question ID.
*/
public boolean displayQuestion(String questionId) {
QuestionView qv = getQuestionView(questionId);
if (qv != null) {
mScroller.scrollTo(qv.getLeft(), qv.getTop());
return true;
}
return false;
}
public void onPause() {
// Propagate onPause callback
for (QuestionView qv : mQuestionViews.values()) {
qv.onPause();
}
}
public void onResume() {
// Propagate onResume callback
for (QuestionView qv : mQuestionViews.values()) {
qv.onResume();
}
}
public void onDestroy() {
// Propagate onDestroy callback
for (QuestionView qv : mQuestionViews.values()) {
qv.onDestroy();
}
}
public boolean isLoaded() {
return mLoaded;
}
private void updateRepetitionsHeader() {
mRepetitionsText
.setText(getContext().getString(R.string.repetitions) + mRepetitions.size());
}
private void loadGroup() {
loadGroup(mRepetitions.size());
}
private void loadGroup(int index) {
final int repetitionId =
mRepetitions.size() <= index ?
mRepetitions.next() :
mRepetitions.getRepetitionId(index);
final int position = index + 1;// Visual indicator.
if (mQuestionGroup.isRepeatable()) {
updateRepetitionsHeader();
RepetitionHeader header =
new RepetitionHeader(getContext(), mQuestionGroup.getHeading(), repetitionId,
position,
mSurveyListener.isReadOnly() ? null : this);
mHeaders.put(repetitionId, header);
mContainer.addView(header);
}
final Context context = getContext();
for (Question q : mQuestionGroup.getQuestions()) {
if (mQuestionGroup.isRepeatable()) {
q = Question
.copy(q, q.getId() + "|" + repetitionId);// compound id. (qid|repetition)
}
QuestionView questionView;
if (ConstantUtil.OPTION_QUESTION_TYPE.equalsIgnoreCase(q.getType())) {
questionView = new OptionQuestionView(context, q, mSurveyListener);
} else if (ConstantUtil.FREE_QUESTION_TYPE.equalsIgnoreCase(q.getType())) {
questionView = new FreetextQuestionView(context, q, mSurveyListener);
} else if (ConstantUtil.PHOTO_QUESTION_TYPE.equalsIgnoreCase(q.getType())) {
questionView = new MediaQuestionView(context, q, mSurveyListener,
ConstantUtil.PHOTO_QUESTION_TYPE);
} else if (ConstantUtil.VIDEO_QUESTION_TYPE.equalsIgnoreCase(q.getType())) {
questionView = new MediaQuestionView(context, q, mSurveyListener,
ConstantUtil.VIDEO_QUESTION_TYPE);
} else if (ConstantUtil.GEO_QUESTION_TYPE.equalsIgnoreCase(q.getType())) {
questionView = new GeoQuestionView(context, q, mSurveyListener);
} else if (ConstantUtil.SCAN_QUESTION_TYPE.equalsIgnoreCase(q.getType())) {
questionView = new BarcodeQuestionView(context, q, mSurveyListener);
} else if (ConstantUtil.DATE_QUESTION_TYPE.equalsIgnoreCase(q.getType())) {
questionView = new DateQuestionView(context, q, mSurveyListener);
} else if (ConstantUtil.CASCADE_QUESTION_TYPE.equalsIgnoreCase(q.getType())) {
questionView = new CascadeQuestionView(context, q, mSurveyListener);
} else if (ConstantUtil.GEOSHAPE_QUESTION_TYPE.equalsIgnoreCase(q.getType())) {
questionView = new GeoshapeQuestionView(context, q, mSurveyListener);
} else if (ConstantUtil.SIGNATURE_QUESTION_TYPE.equalsIgnoreCase(q.getType())) {
questionView = new SignatureQuestionView(context, q, mSurveyListener);
} else if (ConstantUtil.CADDISFLY_QUESTION_TYPE.equalsIgnoreCase(q.getType())) {
questionView = new CaddisflyQuestionView(context, q, mSurveyListener);
} else {
questionView = new QuestionHeaderView(context, q, mSurveyListener);
}
// Add question interaction listener
questionView.addQuestionInteractionListener(mQuestionListener);
mQuestionViews.put(q.getId(), questionView);// Store the reference to the View
// Add divider (within the View)
inflate(getContext(), R.layout.divider, questionView);
mContainer.addView(questionView);
}
}
@Override
public void onDeleteRepetition(Integer repetitionID) {
// Delete question views and corresponding responses
for (String qid : mQuestions) {
qid += "|" + repetitionID;
QuestionView qv = mQuestionViews.get(qid);
if (qv != null) {
qv.onDestroy();
mQuestionViews.remove(qid);
mContainer.removeView(qv);
}
mSurveyListener.deleteResponse(qid);
}
// Rearrange header positions (just the visual indicator).
for (Integer id : mRepetitions) {
if (id.intValue() == repetitionID.intValue()) {
View header = mHeaders.remove(repetitionID);
if (header != null) {
mContainer.removeView(header);
}
} else if (id > repetitionID && mHeaders.containsKey(id)) {
mHeaders.get(id).decreasePosition();
}
}
// Remove the ID from the repetitions list.
mRepetitions.remove(repetitionID);
updateRepetitionsHeader();
}
public void setupDependencies() {
for (QuestionView qv : mQuestionViews.values()) {
setupDependencies(qv);
}
}
private void setupDependencies(QuestionView qv) {
final List<Dependency> dependencies = qv.getQuestion().getDependencies();
if (dependencies == null) {
return;// No dependencies for this question
}
for (Dependency dependency : dependencies) {
QuestionView parentQ;
String parentQId = dependency.getQuestion();
if (mQuestionGroup.isRepeatable() && mQuestions.contains(parentQId)) {
// Internal dependencies need to compound the inner question ID (questionId|iteration)
parentQId += "|" + parseRepetitionId(qv.getQuestion().getId());
dependency.setQuestion(parentQId);
parentQ = getQuestionView(parentQId);// Local search
} else {
parentQ = mSurveyListener.getQuestionView(parentQId);// Global search
}
if (parentQ != null && qv != parentQ) {
parentQ.addQuestionInteractionListener(qv);
qv.checkDependencies();
}
}
}
private int parseRepetitionId(String questionId) {
String[] qid = questionId.split("\\|", -1);
if (qid.length == 2) {
return Integer.parseInt(qid[1]);
}
return -1;
}
class Repetitions implements Iterable<Integer> {
List<Integer> mIDs = new ArrayList<>();
/**
* For the given form instance, load the list of repetitions IDs.
* The populated list will contain the IDs of existing repetitions.
* Although IDs are autoincremented numeric values, there might be
* gaps caused by deleted iterations.
*/
void loadIDs() {
Set<Integer> reps = new HashSet<>();
for (QuestionResponse qr : mSurveyListener.getResponses().values()) {
String[] qid = qr.getQuestionId().split("\\|", -1);
if (qid.length == 2 && mQuestions.contains(qid[0])) {
reps.add(Integer.valueOf(qid[1]));
}
}
mIDs = new ArrayList<>(reps);
Collections.sort(mIDs);
}
/**
* Create and return the next repetition's ID.
*/
int next() {
int id = 0;
if (!mIDs.isEmpty()) {
id = mIDs.get(mIDs.size() - 1) + 1;// Increment last item's ID
}
mIDs.add(id);
return id;
}
int getRepetitionId(int index) {
return mIDs.get(index);
}
int size() {
return mIDs.size();
}
void remove(Integer repetitionID) {
mIDs.remove(repetitionID);
}
@Override
public Iterator<Integer> iterator() {
return mIDs.iterator();
}
}
}