/*
* Copyright (C) 2014-2017 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.content.Context;
import android.graphics.Paint;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.LinearLayout;
import android.widget.Spinner;
import android.widget.SpinnerAdapter;
import android.widget.TextView;
import org.akvo.flow.R;
import org.akvo.flow.data.CascadeDB;
import org.akvo.flow.domain.Level;
import org.akvo.flow.domain.Node;
import org.akvo.flow.domain.Question;
import org.akvo.flow.domain.QuestionResponse;
import org.akvo.flow.domain.response.value.CascadeNode;
import org.akvo.flow.event.SurveyListener;
import org.akvo.flow.serialization.response.value.CascadeValue;
import org.akvo.flow.util.ConstantUtil;
import org.akvo.flow.util.FileUtil;
import org.akvo.flow.util.FileUtil.FileType;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import timber.log.Timber;
public class CascadeQuestionView extends QuestionView
implements AdapterView.OnItemSelectedListener {
private static final int POSITION_NONE = -1;// no spinner position id
private static final long ID_NONE = -1;// no node id
private static final long ID_ROOT = 0;// root node id
private String[] mLevels;
private LinearLayout mSpinnerContainer;
private boolean mFinished;
private CascadeDB mDatabase;
public CascadeQuestionView(Context context, Question q, SurveyListener surveyListener) {
super(context, q, surveyListener);
init();
}
private void init() {
setQuestionView(R.layout.cascade_question_view);
mSpinnerContainer = (LinearLayout) findViewById(R.id.cascade_content);
// Load level names
List<Level> levels = getQuestion().getLevels();
if (levels != null) {
mLevels = new String[levels.size()];
for (int i = 0; i < levels.size(); i++) {
mLevels[i] = levels.get(i).getText();
}
}
// Construct local filename (src refers to remote location of the resource)
String src = getQuestion().getSrc();
if (!TextUtils.isEmpty(src)) {
File db = new File(FileUtil.getFilesDir(FileType.RES), src);
if (db.exists()) {
mDatabase = new CascadeDB(getContext(), db.getAbsolutePath());
mDatabase.open();
}
}
updateSpinners(POSITION_NONE);
}
@Override
public void onResume() {
if (mDatabase != null && !mDatabase.isOpen()) {
mDatabase.open();
}
}
@Override
public void onPause() {
if (mDatabase != null) {
mDatabase.close();
}
}
private void updateSpinners(int updatedSpinnerIndex) {
if (mDatabase == null) {
return;
}
final int nextLevel = updatedSpinnerIndex + 1;
// First, clean up descendant spinners (if any)
while (nextLevel < mSpinnerContainer.getChildCount()) {
mSpinnerContainer.removeViewAt(nextLevel);
}
long parent = ID_ROOT;
if (updatedSpinnerIndex != POSITION_NONE) {
Node node = (Node) getSpinner(updatedSpinnerIndex).getSelectedItem();
if (node.getId() == ID_NONE) {
// if this is the first level, it means we've got no answer at all
mFinished = false;
return; // Do not load more levels
} else {
parent = node.getId();
}
}
List<Node> values = mDatabase.getValues(parent);
if (!values.isEmpty()) {
addLevelView(nextLevel, values, POSITION_NONE);
mFinished = updatedSpinnerIndex == POSITION_NONE;
} else {
mFinished = true;// no more levels
}
}
private void addLevelView(int position, List<Node> values, int selection) {
if (values.isEmpty()) {
return;
}
LayoutInflater inflater = LayoutInflater.from(getContext());
View view = inflater.inflate(R.layout.cascading_level_item, mSpinnerContainer, false);
final TextView text = (TextView) view.findViewById(R.id.text);
final Spinner spinner = (Spinner) view.findViewById(R.id.spinner);
text.setText(mLevels != null && mLevels.length > position ? mLevels[position] : "");
// Insert a fake 'Select' value
Node node = new Node(ID_NONE, getContext().getString(R.string.select), null);
values.add(0, node);
SpinnerAdapter adapter = new CascadeAdapter(getContext(), values);
spinner.setAdapter(adapter);
spinner.setTag(position);// Tag the spinner with its position within the container
spinner.setEnabled(!isReadOnly());
if (selection != POSITION_NONE) {
spinner.setSelection(selection + 1);// Skip level title item
}
// Attach listener asynchronously, preventing selection event from being fired off right away
spinner.post(new Runnable() {
public void run() {
spinner.setOnItemSelectedListener(CascadeQuestionView.this);
}
});
mSpinnerContainer.addView(view);
}
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
final int index = (Integer) parent.getTag();
updateSpinners(index);
captureResponse();
setError(null);
}
@Override
public void onNothingSelected(AdapterView<?> parent) {
}
@Override
public void rehydrate(QuestionResponse resp) {
super.rehydrate(resp);
String answer = resp != null ? resp.getValue() : null;
if (mDatabase == null || TextUtils.isEmpty(answer)) {
return;
}
mSpinnerContainer.removeAllViews();
List<CascadeNode> values = CascadeValue.deserialize(answer);
// For each existing value, we load the corresponding level nodes, and create a spinner
// view, automatically selecting the token. On each iteration, we keep track of selected
// value's id, in order to fetch the descendant nodes from the DB.
int index = 0;
long parentId = 0;
while (index < values.size()) {
int valuePosition = POSITION_NONE;
List<Node> spinnerValues = mDatabase.getValues(parentId);
for (int pos = 0; pos < spinnerValues.size(); pos++) {
Node node = spinnerValues.get(pos);
CascadeNode v = values.get(index);
if (node.getName().equals(v.getName())) {
valuePosition = pos;
parentId = node.getId();
break;
}
}
if (valuePosition == POSITION_NONE || spinnerValues.isEmpty()) {
mSpinnerContainer.removeAllViews();
return;// Cannot reassemble response
}
addLevelView(index, spinnerValues, valuePosition);
index++;
}
if (!isReadOnly()) {
updateSpinners(index - 1);// Last updated item position
}
}
@Override
public void resetQuestion(boolean fireEvent) {
super.resetQuestion(fireEvent);
updateSpinners(POSITION_NONE);
if (mDatabase == null) {
String error = "Cannot load cascade resource: " + getQuestion().getSrc();
Timber.e(new IllegalStateException(error), error);
setError(error);
}
}
@Override
public void captureResponse(boolean suppressListeners) {
List<CascadeNode> values = new ArrayList<>();
for (int i = 0; i < mSpinnerContainer.getChildCount(); i++) {
Node node = (Node) getSpinner(i).getSelectedItem();
if (node.getId() != ID_NONE) {
CascadeNode v = new CascadeNode();
v.setName(node.getName());
v.setCode(node.getCode());
values.add(v);
}
}
String response = CascadeValue.serialize(values);
setResponse(new QuestionResponse(response, ConstantUtil.CASCADE_RESPONSE_TYPE,
getQuestion().getId()), suppressListeners);
}
private Spinner getSpinner(int position) {
return (Spinner) mSpinnerContainer.getChildAt(position).findViewById(R.id.spinner);
}
@Override
public boolean isValid() {
boolean valid = super.isValid() && mFinished;
if (!valid) {
setError(getResources().getString(R.string.error_question_mandatory));
}
return valid;
}
private static class CascadeAdapter extends ArrayAdapter<Node> {
CascadeAdapter(Context context, List<Node> objects) {
super(context, R.layout.cascade_spinner_item, R.id.cascade_spinner_item_text, objects);
setDropDownViewResource(R.layout.cascade_spinner_item);
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
View view = super.getView(position, convertView, parent);
setStyle(view, position);
return view;
}
@Override
public View getDropDownView(final int position, View convertView, ViewGroup parent) {
View view = super.getDropDownView(position, convertView, parent);
setStyle(view, position);
return view;
}
private void setStyle(View view, int position) {
try {
TextView text = (TextView) view.findViewById(R.id.cascade_spinner_item_text);
int flags = text.getPaintFlags();
if (position == 0) {
flags |= Paint.FAKE_BOLD_TEXT_FLAG;
} else {
flags &= ~Paint.FAKE_BOLD_TEXT_FLAG;
}
text.setPaintFlags(flags);
} catch (ClassCastException e) {
Timber.e("View cannot be casted to TextView!");
}
}
}
}