org.odk.collect.android.views.ODKView.java Source code

Java tutorial

Introduction

Here is the source code for org.odk.collect.android.views.ODKView.java

Source

/*
 * Copyright (C) 2011 University of Washington
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
 * in compliance with the License. You may obtain a copy of the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software distributed under the License
 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and limitations under
 * the License.
 */

package org.odk.collect.android.views;

import android.animation.ArgbEvaluator;
import android.animation.ValueAnimator;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.support.annotation.NonNull;
import android.support.v4.widget.NestedScrollView;
import android.text.TextUtils;
import android.util.TypedValue;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnLongClickListener;
import android.widget.Button;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.TableLayout;
import android.widget.TextView;

import org.javarosa.core.model.Constants;
import org.javarosa.core.model.FormIndex;
import org.javarosa.core.model.IFormElement;
import org.javarosa.core.model.QuestionDef;
import org.javarosa.core.model.data.IAnswerData;
import org.javarosa.core.model.instance.TreeReference;
import org.javarosa.form.api.FormEntryCaption;
import org.javarosa.form.api.FormEntryPrompt;
import org.odk.collect.android.R;
import org.odk.collect.android.application.Collect;
import org.odk.collect.android.exception.ExternalParamsException;
import org.odk.collect.android.exception.JavaRosaException;
import org.odk.collect.android.external.ExternalAppsUtils;
import org.odk.collect.android.logic.FormController;
import org.odk.collect.android.utilities.ThemeUtils;
import org.odk.collect.android.utilities.ToastUtils;
import org.odk.collect.android.utilities.ViewIds;
import org.odk.collect.android.widgets.QuestionWidget;
import org.odk.collect.android.widgets.WidgetFactory;
import org.odk.collect.android.widgets.interfaces.BinaryWidget;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import timber.log.Timber;

import static org.odk.collect.android.utilities.ApplicationConstants.RequestCodes;

/**
 * This class is
 *
 * @author carlhartung
 */
@SuppressLint("ViewConstructor")
public class ODKView extends FrameLayout implements OnLongClickListener {

    private final LinearLayout view;
    private final LinearLayout.LayoutParams layout;
    private final ArrayList<QuestionWidget> widgets;

    public static final String FIELD_LIST = "field-list";

    public ODKView(Context context, final FormEntryPrompt[] questionPrompts, FormEntryCaption[] groups,
            boolean advancingPage) {
        super(context);

        inflate(getContext(), R.layout.nested_scroll_view, this); // keep in an xml file to enable the vertical scrollbar

        widgets = new ArrayList<>();

        view = new LinearLayout(getContext());
        view.setOrientation(LinearLayout.VERTICAL);
        view.setGravity(Gravity.TOP);
        view.setPadding(0, 7, 0, 0);

        layout = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT,
                LinearLayout.LayoutParams.WRAP_CONTENT);
        layout.setMargins(10, 0, 10, 0);

        // display which group you are in as well as the question

        addGroupText(groups);

        // when the grouped fields are populated by an external app, this will get true.
        boolean readOnlyOverride = false;

        // get the group we are showing -- it will be the last of the groups in the groups list
        if (groups != null && groups.length > 0) {
            final FormEntryCaption c = groups[groups.length - 1];
            final String intentString = c.getFormElement().getAdditionalAttribute(null, "intent");
            if (intentString != null && intentString.length() != 0) {

                readOnlyOverride = true;

                final String buttonText;
                final String errorString;
                String v = c.getSpecialFormQuestionText("buttonText");
                buttonText = (v != null) ? v : context.getString(R.string.launch_app);
                v = c.getSpecialFormQuestionText("noAppErrorString");
                errorString = (v != null) ? v : context.getString(R.string.no_app);

                TableLayout.LayoutParams params = new TableLayout.LayoutParams();
                params.setMargins(7, 5, 7, 5);

                // set button formatting
                Button launchIntentButton = new Button(getContext());
                launchIntentButton.setId(ViewIds.generateViewId());
                launchIntentButton.setText(buttonText);
                launchIntentButton.setTextSize(TypedValue.COMPLEX_UNIT_DIP, Collect.getQuestionFontsize() + 2);
                launchIntentButton.setPadding(20, 20, 20, 20);
                launchIntentButton.setLayoutParams(params);

                launchIntentButton.setOnClickListener(new View.OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        String intentName = ExternalAppsUtils.extractIntentName(intentString);
                        Map<String, String> parameters = ExternalAppsUtils.extractParameters(intentString);

                        Intent i = new Intent(intentName);
                        try {
                            ExternalAppsUtils.populateParameters(i, parameters, c.getIndex().getReference());

                            for (FormEntryPrompt p : questionPrompts) {
                                IFormElement formElement = p.getFormElement();
                                if (formElement instanceof QuestionDef) {
                                    TreeReference reference = (TreeReference) formElement.getBind().getReference();
                                    IAnswerData answerValue = p.getAnswerValue();
                                    Object value = answerValue == null ? null : answerValue.getValue();
                                    switch (p.getDataType()) {
                                    case Constants.DATATYPE_TEXT:
                                    case Constants.DATATYPE_INTEGER:
                                    case Constants.DATATYPE_DECIMAL:
                                        i.putExtra(reference.getNameLast(), (Serializable) value);
                                        break;
                                    }
                                }
                            }

                            ((Activity) getContext()).startActivityForResult(i, RequestCodes.EX_GROUP_CAPTURE);
                        } catch (ExternalParamsException e) {
                            Timber.e(e, "ExternalParamsException");

                            ToastUtils.showShortToast(e.getMessage());
                        } catch (ActivityNotFoundException e) {
                            Timber.d(e, "ActivityNotFoundExcept");

                            ToastUtils.showShortToast(errorString);
                        }
                    }
                });

                View divider = new View(getContext());
                divider.setBackgroundResource(new ThemeUtils(getContext()).getDivider());
                divider.setMinimumHeight(3);
                view.addView(divider);

                view.addView(launchIntentButton, layout);
            }
        }

        boolean first = true;
        for (FormEntryPrompt p : questionPrompts) {
            if (!first) {
                View divider = new View(getContext());
                divider.setBackgroundResource(new ThemeUtils(getContext()).getDivider());
                divider.setMinimumHeight(3);
                view.addView(divider);
            } else {
                first = false;
            }

            // if question or answer type is not supported, use text widget
            QuestionWidget qw = WidgetFactory.createWidgetFromPrompt(p, getContext(), readOnlyOverride);
            qw.setLongClickable(true);
            qw.setOnLongClickListener(this);
            qw.setId(ViewIds.generateViewId());

            widgets.add(qw);
            view.addView(qw, layout);
        }

        ((NestedScrollView) findViewById(R.id.odk_view_container)).addView(view);

        // see if there is an autoplay option.
        // Only execute it during forward swipes through the form
        if (advancingPage && widgets.size() == 1) {
            final String playOption = widgets.get(0).getFormEntryPrompt().getFormElement()
                    .getAdditionalAttribute(null, "autoplay");
            if (playOption != null) {
                Handler handler = new Handler();
                handler.postDelayed(new Runnable() {
                    @Override
                    public void run() {
                        if (playOption.equalsIgnoreCase("audio")) {
                            widgets.get(0).playAudio();
                        } else if (playOption.equalsIgnoreCase("video")) {
                            widgets.get(0).playVideo();
                        }
                    }
                }, 150);
            }
        }
    }

    public Bundle getState() {
        Bundle state = new Bundle();
        for (QuestionWidget qw : getWidgets()) {
            state.putAll(qw.getCurrentState());
        }

        return state;
    }

    /**
     * http://code.google.com/p/android/issues/detail?id=8488
     */
    public void recycleDrawables() {
        this.destroyDrawingCache();
        view.destroyDrawingCache();
        for (QuestionWidget q : widgets) {
            q.recycleDrawables();
        }
    }

    /**
     * @return a HashMap of answers entered by the user for this set of widgets
     */
    public HashMap<FormIndex, IAnswerData> getAnswers() {
        HashMap<FormIndex, IAnswerData> answers = new LinkedHashMap<>();
        for (QuestionWidget q : widgets) {
            /*
             * The FormEntryPrompt has the FormIndex, which is where the answer gets stored. The
             * QuestionWidget has the answer the user has entered.
             */
            FormEntryPrompt p = q.getFormEntryPrompt();
            answers.put(p.getIndex(), q.getAnswer());
        }

        return answers;
    }

    /**
     * // * Add a TextView containing the hierarchy of groups to which the question belongs. //
     */
    private void addGroupText(FormEntryCaption[] groups) {
        String path = getGroupsPath(groups);

        // build view
        if (!path.isEmpty()) {
            TextView tv = new TextView(getContext());
            tv.setText(path);
            tv.setTextSize(TypedValue.COMPLEX_UNIT_DIP, Collect.getQuestionFontsize() - 4);
            tv.setPadding(0, 0, 0, 5);
            view.addView(tv, layout);
        }
    }

    /**
     * @see #getGroupsPath(FormEntryCaption[], boolean)
     */
    @NonNull
    public static String getGroupsPath(FormEntryCaption[] groups) {
        return getGroupsPath(groups, false);
    }

    /**
     * Builds a string representing the 'path' of the list of groups.
     * Each level is separated by `>`.
     *
     * Some views (e.g. the repeat picker) may want to hide the multiplicity of the last item,
     * i.e. show `Friends` instead of `Friends > 1`.
     */
    @NonNull
    public static String getGroupsPath(FormEntryCaption[] groups, boolean hideLastMultiplicity) {
        if (groups == null) {
            return "";
        }

        List<String> segments = new ArrayList<>();
        int index = 1;
        for (FormEntryCaption group : groups) {
            String text = group.getLongText();

            if (text != null) {
                segments.add(text);

                boolean isMultiplicityAllowed = !(hideLastMultiplicity && index == groups.length);
                if (group.repeats() && isMultiplicityAllowed) {
                    segments.add(Integer.toString(group.getMultiplicity() + 1));
                }
            }

            index++;
        }

        return TextUtils.join(" > ", segments);
    }

    public void setFocus(Context context) {
        if (!widgets.isEmpty()) {
            widgets.get(0).setFocus(context);
        }
    }

    /**
     * Called when another activity returns information to answer this question.
     */
    public void setBinaryData(Object answer) {
        boolean set = false;
        for (QuestionWidget q : widgets) {
            if (q instanceof BinaryWidget) {
                BinaryWidget binaryWidget = (BinaryWidget) q;
                if (binaryWidget.isWaitingForData()) {
                    try {
                        binaryWidget.setBinaryData(answer);
                        binaryWidget.cancelWaitingForData();
                    } catch (Exception e) {
                        Timber.e(e);
                        ToastUtils.showLongToast(
                                getContext().getString(R.string.error_attaching_binary_file, e.getMessage()));
                    }
                    set = true;
                    break;
                }
            }
        }

        if (!set) {
            Timber.w("Attempting to return data to a widget or set of widgets not looking for data");
        }
    }

    public void setDataForFields(Bundle bundle) throws JavaRosaException {
        if (bundle == null) {
            return;
        }
        FormController formController = Collect.getInstance().getFormController();
        Set<String> keys = bundle.keySet();
        for (String key : keys) {
            for (QuestionWidget questionWidget : widgets) {
                FormEntryPrompt prompt = questionWidget.getFormEntryPrompt();
                TreeReference treeReference = (TreeReference) prompt.getFormElement().getBind().getReference();

                if (treeReference.getNameLast().equals(key)) {

                    switch (prompt.getDataType()) {
                    case Constants.DATATYPE_TEXT:
                        formController.saveAnswer(prompt.getIndex(),
                                ExternalAppsUtils.asStringData(bundle.get(key)));
                        break;
                    case Constants.DATATYPE_INTEGER:
                        formController.saveAnswer(prompt.getIndex(),
                                ExternalAppsUtils.asIntegerData(bundle.get(key)));
                        break;
                    case Constants.DATATYPE_DECIMAL:
                        formController.saveAnswer(prompt.getIndex(),
                                ExternalAppsUtils.asDecimalData(bundle.get(key)));
                        break;
                    default:
                        throw new RuntimeException(getContext().getString(R.string.ext_assign_value_error,
                                treeReference.toString(false)));
                    }

                    break;
                }
            }
        }
    }

    public void cancelWaitingForBinaryData() {
        int count = 0;
        for (QuestionWidget q : widgets) {
            if (q instanceof BinaryWidget) {
                if (q.isWaitingForData()) {
                    q.cancelWaitingForData();
                    ++count;
                }
            }
        }

        if (count != 1) {
            Timber.w("Attempting to cancel waiting for binary data to a widget or set of widgets "
                    + "not looking for data");
        }
    }

    public boolean suppressFlingGesture(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
        for (QuestionWidget q : widgets) {
            if (q.suppressFlingGesture(e1, e2, velocityX, velocityY)) {
                return true;
            }
        }
        return false;
    }

    /**
     * @return true if the answer was cleared, false otherwise.
     */
    public boolean clearAnswer() {
        // If there's only one widget, clear the answer.
        // If there are more, then force a long-press to clear the answer.
        if (widgets.size() == 1 && !widgets.get(0).getFormEntryPrompt().isReadOnly()) {
            widgets.get(0).clearAnswer();
            return true;
        } else {
            return false;
        }
    }

    public ArrayList<QuestionWidget> getWidgets() {
        return widgets;
    }

    @Override
    public void setOnFocusChangeListener(OnFocusChangeListener l) {
        for (int i = 0; i < widgets.size(); i++) {
            QuestionWidget qw = widgets.get(i);
            qw.setOnFocusChangeListener(l);
        }
    }

    @Override
    public boolean onLongClick(View v) {
        return false;
    }

    @Override
    public void cancelLongPress() {
        super.cancelLongPress();
        for (QuestionWidget qw : widgets) {
            qw.cancelLongPress();
        }
    }

    public void stopAudio() {
        widgets.get(0).stopAudio();
    }

    /**
     * Releases widget resources, such as {@link android.media.MediaPlayer}s
     */
    public void releaseWidgetResources() {
        for (QuestionWidget w : widgets) {
            w.release();
        }
    }

    public void highlightWidget(FormIndex formIndex) {
        QuestionWidget qw = getQuestionWidget(formIndex);

        if (qw != null) {
            // postDelayed is needed because otherwise scrolling may not work as expected in case when
            // answers are validated during form finalization.
            new Handler().postDelayed(() -> {
                findViewById(R.id.odk_view_container).scrollTo(0, qw.getTop());

                ValueAnimator va = new ValueAnimator();
                if (Build.VERSION.SDK_INT > Build.VERSION_CODES.KITKAT) {
                    va.setIntValues(getResources().getColor(R.color.red_500), getDrawingCacheBackgroundColor());
                } else {
                    // Avoid fading to black on certain devices and Android versions that may not support transparency
                    TypedValue typedValue = new TypedValue();
                    getContext().getTheme().resolveAttribute(android.R.attr.windowBackground, typedValue, true);
                    if (typedValue.type >= TypedValue.TYPE_FIRST_COLOR_INT
                            && typedValue.type <= TypedValue.TYPE_LAST_COLOR_INT) {
                        va.setIntValues(getResources().getColor(R.color.red_500), typedValue.data);
                    } else {
                        va.setIntValues(getResources().getColor(R.color.red_500), getDrawingCacheBackgroundColor());
                    }
                }

                va.setEvaluator(new ArgbEvaluator());
                va.addUpdateListener(
                        valueAnimator -> qw.setBackgroundColor((int) valueAnimator.getAnimatedValue()));
                va.setDuration(2500);
                va.start();
            }, 100);
        }
    }

    private QuestionWidget getQuestionWidget(FormIndex formIndex) {
        for (QuestionWidget qw : widgets) {
            if (formIndex.equals(qw.getFormEntryPrompt().getIndex())) {
                return qw;
            }
        }
        return null;
    }
}