com.toughra.mlearnplayer.idevices.MCQIdevice.java Source code

Java tutorial

Introduction

Here is the source code for com.toughra.mlearnplayer.idevices.MCQIdevice.java

Source

/*
 * Ustad Mobile (Micro Edition App)
 * 
 * Copyright 2011-2014 UstadMobile Inc. All rights reserved.
 * www.ustadmobile.com
 *
 * Ustad Mobile 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 with the following additional terms:
 * 
 * All names, links, and logos of Ustad Mobile and Toughra Technologies FZ
 * LLC must be kept as they are in the original distribution.  If any new
 * screens are added you must include the Ustad Mobile logo as it has been
 * used in the original distribution.  You may not create any new
 * functionality whose purpose is to diminish or remove the Ustad Mobile
 * Logo.  You must leave the Ustad Mobile logo as the logo for the
 * application to be used with any launcher (e.g. the mobile app launcher).
 * 
 * If you want a commercial license to remove the above restriction you must
 * contact us and purchase a license without these restrictions.
     
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
     
 * Ustad Mobile 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.
 */
package com.toughra.mlearnplayer.idevices;

import com.toughra.mlearnplayer.xml.XmlNode;
import com.sun.lwuit.events.ActionEvent;
import com.toughra.mlearnplayer.Idevice;
import com.sun.lwuit.*;
import com.sun.lwuit.events.ActionListener;
import com.sun.lwuit.events.FocusListener;
import com.sun.lwuit.geom.Dimension;
import com.sun.lwuit.html.HTMLComponent;
import com.sun.lwuit.layouts.GridLayout;
import com.sun.lwuit.plaf.Border;
import com.sun.lwuit.table.TableLayout;
import com.toughra.mlearnplayer.EXEStrMgr;
import java.util.*;
import com.toughra.mlearnplayer.FeedbackDialog;
import com.toughra.mlearnplayer.MLearnPlayerMidlet;
import com.toughra.mlearnplayer.MLearnUtils;
import com.toughra.mlearnplayer.UMTinCan;
import org.json.me.JSONArray;
import org.json.me.JSONException;
import org.json.me.JSONObject;

/**
 * The MCQIdevice will render a set of Multiple Choice Questions
 * 
 * @author mike
 */
public class MCQIdevice extends Idevice implements ActionListener {

    /** Vector of html text of questions to be shown */
    private Vector questions;

    /** 
     * Vector containing child Vector objects, the children contain Answer objects
     * for the questionHTML at the same index in questions
     */
    private Vector answers;

    /** title of the question*/
    private String title;

    /**used to help determine layout*/
    private int totalQuestions = 0;

    /**used to help determine layout*/
    private int totalAnswers = 0;

    /**HTML to show before questions */
    private String htmlIntro;

    /**used to track which answer the user has currently selected*/
    private int[] selectedAnswers;

    /**the HTMLComponent that will be used to show feedback - one for each question*/
    private HTMLComponent[] fbackAreas;

    /**if there is an audio file to play when first loading*/
    private String audioURL;

    /** The main LWUIT form */
    Form form;

    //false if question not yet answered/attempted by student
    boolean[] qAttempted;

    /** width of border when an item is hilighted to be moved */
    static int borderWidthHilight = 3;

    /** normal width of border of item*/
    static int borderWidthNormal = 1;

    /** padding between the button and it's html contents */
    static int padding = 5;

    /** color of border normally */
    static int normalColor = 0x000000;

    /** color of border when hilighted to be moved */
    static int activeColor = 0xff0000;

    /** 
     * Constructor - setup the for, dig through the questions and answers
     * that are there.
     * 
     * @param host the midlet host
     * @param data the XML data to construct with
     */
    public MCQIdevice(MLearnPlayerMidlet host, XmlNode data) {
        super(host);

        questions = new Vector();
        answers = new Vector();

        Vector dataQuestions = data.findChildrenByTagName("question", true);
        hadCorrect = MLearnUtils.makeBooleanArray(false, dataQuestions.size());
        qAttempted = MLearnUtils.makeBooleanArray(false, dataQuestions.size());

        fbackAreas = new HTMLComponent[dataQuestions.size()];

        for (int i = 0; i < dataQuestions.size(); i++) {
            XmlNode currentNode = (XmlNode) dataQuestions.elementAt(i);
            if (i == 0 && currentNode.hasAttribute("audio")) {
                audioURL = currentNode.getAttribute("audio");
            }

            //CDATA text section will always be the first child after question
            String questionTextHTML = currentNode.getTextChildContent(0);
            XmlNode tinCanNode = currentNode.findFirstChildByTagName("tincan", true);
            String tinCanId = null;
            String tinCanDef = null;
            JSONObject tcDefObj = null;

            if (tinCanNode != null) {
                tinCanDef = currentNode.findFirstChildByTagName("activitydef", true).getTextChildContent(0);
                tinCanId = tinCanNode.getAttribute("id");
            } else {
                tinCanId = MLearnUtils.TINCAN_PREFIX + "/" + MLearnPlayerMidlet.getInstance().getTinCanPage()
                        + "/mcq" + i;
                tcDefObj = new JSONObject();
                try {
                    String qText = MLearnUtils.removeHTMLTags(questionTextHTML);
                    String qName = "MCQ: " + i + ": " + MLearnPlayerMidlet.getInstance().getTinCanPage();
                    tcDefObj.put("name", UMTinCan.makeLangMapVal("en-US", qName));
                    tcDefObj.put("description", UMTinCan.makeLangMapVal("en-US", qName + " " + qText));
                    tcDefObj.put("type", "http://adlnet.gov/expapi/activities/cmi.interaction");
                    tcDefObj.put("interactionType", "choice");

                } catch (JSONException e) {

                }
            }

            QuestionItem thisItem = new QuestionItem(questionTextHTML, tinCanId, tinCanDef);
            questions.addElement(thisItem);

            Vector questionAnswers = currentNode.findChildrenByTagName("answer", true);
            Vector answerObjVector = new Vector();
            String[][] ansInfo = new String[questionAnswers.size()][2];
            int correctIndex = 0;
            for (int j = 0; j < questionAnswers.size(); j++) {
                XmlNode currentAnswer = (XmlNode) questionAnswers.elementAt(j);
                String answerText = currentAnswer.getTextChildContent(0);
                XmlNode feedbackNode = (XmlNode) currentAnswer.findChildrenByTagName("feedback", true).elementAt(0);
                String feedback = feedbackNode.getTextChildContent(0);
                String responseId = currentAnswer.getAttribute("id");
                if (responseId == null) {
                    responseId = "opt" + j;
                }

                boolean thisAnswerCorrect = currentAnswer.getAttribute("iscorrect").equals("true");
                Answer answer = new Answer(answerText, thisAnswerCorrect, new Integer(i), feedback, j, responseId);
                answer.questionId = i;
                if (thisAnswerCorrect) {
                    correctIndex = j;
                }

                if (feedbackNode.hasAttribute("audio")) {
                    answer.fbAudio = feedbackNode.getAttribute("audio");
                }

                answerObjVector.addElement(answer);
                ansInfo[j][0] = "opt" + j;
                ansInfo[j][1] = MLearnUtils.removeHTMLTags(answerText).trim();
                totalAnswers++;
            }

            //complete definition if applicable
            if (tcDefObj != null) {
                JSONArray correctArr = new JSONArray();
                correctArr.put(ansInfo[correctIndex][0]);
                try {
                    tcDefObj.put("correctResponsesPattern", correctArr);
                    JSONArray choicesArr = new JSONArray();
                    for (int j = 0; j < questionAnswers.size(); j++) {
                        JSONObject thisAnsObj = new JSONObject();
                        thisAnsObj.put("id", ansInfo[j][0]);
                        thisAnsObj.put("description", UMTinCan.makeLangMapVal("en-US", ansInfo[j][1]));
                        choicesArr.put(thisAnsObj);
                    }
                    tcDefObj.put("choices", choicesArr);

                    String jsonStr = tcDefObj.toString();
                    thisItem.tinCanDefinition = jsonStr;
                    int x = 0;
                } catch (JSONException e) {

                }
            }

            answers.addElement(answerObjVector);
            questionAnswers = null;
            totalQuestions++;
        }
        int done = 0;
    }

    public String getDeviceTypeName() {
        return "mcq";
    }

    /**
     * This is an LWUIT mode idevice 
     * @return Idevice.MODE_LWUIT_FORM
     */
    public int getMode() {
        return Idevice.MODE_LWUIT_FORM;
    }

    /**
     * Will make a table layout including questions, answers and feedback
     * 
     * @return 
     */
    public Form getForm() {
        form = new Form();

        //GridLayout gLayout = new GridLayout(totalQuestions + totalAnswers, 1);
        TableLayout tLayout = new TableLayout(totalQuestions + totalAnswers, 1);
        /*TableLayout tLayout = new TableLayout((totalQuestions*2) + totalAnswers,
            2);
        TableLayout.setMinimumSizePerColumn(20);
        form.setLayout(tLayout);
        */
        form.setLayout(tLayout);

        selectedAnswers = new int[totalQuestions];

        Component toFocus = null;

        for (int i = 0; i < questions.size(); i++) {

            HTMLComponent qHTML = hostMidlet.makeHTMLComponent(questions.elementAt(i).toString());
            qHTML.setFocusable(false);
            form.addComponent(qHTML);

            Vector questionAnswers = (Vector) answers.elementAt(i);
            String qId = String.valueOf(i);

            MCQAnswerItem[] itemArr = new MCQAnswerItem[questionAnswers.size()];

            for (int j = 0; j < questionAnswers.size(); j++) {
                Answer currentAnswer = (Answer) questionAnswers.elementAt(j);
                HTMLComponent answerComp = hostMidlet.makeHTMLComponent(currentAnswer.answerText);
                MCQAnswerItem thisItem = new MCQAnswerItem(this, answerComp, currentAnswer);

                thisItem.updateStyle();
                itemArr[j] = thisItem;
                TableLayout.Constraint tConst = tLayout.createConstraint();
                tConst.setWidthPercentage(100);
                form.addComponent(tConst, thisItem);

                if (i == 0 && j == 0) {
                    form.setFocused(thisItem);
                    thisItem.setFocus(true);
                    thisItem.updateStyle();
                }
                thisItem.addActionListener(this);
            }

            for (int j = 0; j < questionAnswers.size(); j++) {
                if (j > 0) {
                    itemArr[j].setNextFocusUp(itemArr[j - 1]);
                }
                if (j < questionAnswers.size() - 1) {
                    itemArr[j].setNextFocusDown(itemArr[j + 1]);
                }
            }

        }

        return form;
    }

    /**
     * Recognizes when the user has select an answer - shows the appropriate
     * feedback
     * 
     * @param ae  ActionEvent
     */
    public void actionPerformed(ActionEvent ae) {
        if (ae.getComponent() instanceof MCQAnswerItem) {
            ae.consume();
            MCQAnswerItem ansItem = (MCQAnswerItem) ae.getComponent();

            FeedbackDialog fbDialog = new FeedbackDialog(hostMidlet);
            Answer selectedAnswer = ansItem.answer;

            JSONObject stmtObject = new JSONObject();
            JSONObject actorObj = EXEStrMgr.getInstance().getTinCanActor();
            QuestionItem question = (QuestionItem) questions.elementAt(selectedAnswer.questionId);

            if (actorObj != null && question.activityId != null) {
                try {
                    stmtObject.put("actor", actorObj);
                    JSONObject activityDef = new JSONObject(question.tinCanDefinition);
                    JSONObject objectDef = new JSONObject();
                    objectDef.put("id", question.activityId);
                    objectDef.put("definition", activityDef);
                    objectDef.put("objectType", "Activity");
                    stmtObject.put("object", objectDef);

                    JSONObject verbDef = new JSONObject();
                    verbDef.put("id", "http://adlnet.gov/expapi/verbs/answered");
                    JSONObject verbDisplay = new JSONObject();
                    verbDisplay.put("en-US", "answered");
                    verbDef.put("display", verbDisplay);
                    stmtObject.put("verb", verbDef);

                    JSONObject resultDef = new JSONObject();
                    resultDef.put("success", selectedAnswer.isCorrect);
                    resultDef.put("response", selectedAnswer.responseId);
                    stmtObject.put("result", resultDef);

                    JSONObject ctxObj = new JSONObject();
                    JSONObject ctxActivities = new JSONObject();
                    String parentActId = question.activityId.substring(0, question.activityId.lastIndexOf('/'));
                    JSONObject parentActObj = new JSONObject();
                    parentActObj.put("id", parentActId);
                    JSONArray parentArr = new JSONArray();
                    parentArr.put(parentActObj);
                    ctxActivities.put("parent", parentArr);
                    ctxObj.put("contextActivities", ctxActivities);
                    stmtObject.put("context", ctxObj);

                } catch (JSONException je) {
                    EXEStrMgr.lg(180, "Exception creating json tincan statement", je);
                }

                String totalStmtStr = stmtObject.toString();
                EXEStrMgr.getInstance().queueTinCanStmt(stmtObject);
            }

            fbDialog.showFeedback(ansItem, selectedAnswer.feedback, selectedAnswer.isCorrect);
            int qId = selectedAnswer.questionId;

            if (!qAttempted[qId]) {
                qAttempted[qId] = true;
                if (correctFirst == -1) {
                    correctFirst = 0;
                }

                if (selectedAnswer.isCorrect) {
                    correctFirst++;
                }
            }

            if (selectedAnswer.isCorrect) {
                hadCorrect[qId] = true;
            }

            if (selectedAnswer.fbAudio != null) {
                hostMidlet.playMedia(selectedAnswer.fbAudio);
            }
        }

    }

    /**
     * Start the idevice and play the starting sound if there is one
     */
    public void start() {
        super.start();
        if (audioURL != null) {
            hostMidlet.playMedia(audioURL);
        }
    }

    /**
     * Stop the idevice - stop all media
     */
    public void stop() {

        super.stop();

        EXEStrMgr.lg(this, //idevice
                0, //question id
                getTimeOnDevice(), //time on device in ms
                MLearnUtils.countBoolValues(hadCorrect, true), //num correctly answered
                Math.max(0, correctFirst), //num answered correct first attempt
                MLearnUtils.countBoolValues(qAttempted, true), //num questions attempted
                Idevice.LOGDEVCOMPLETE, //verb
                0, //score
                0, //maxScorePossible
                Idevice.BLANK, //answer given 
                Idevice.BLANK);//remarks
    }

    /**
     * Dispose of components
     */
    public void dispose() {
        form.removeAll();
        super.dispose();
    }

    /**
     * This is a quiz mode idevice
     * @return Idevice.LOG_QUIZ
     */
    public int getLogType() {
        return Idevice.LOG_QUIZ;
    }

    /**
     * Provide the quiz scores for logging purposes
     * 
     * @return array of quiz scores 
     */
    public int[] getScores() {
        return new int[] { correctFirst, MLearnUtils.countBoolValues(hadCorrect, true), questions.size() };
    }

}

/**
 * Utility container class representing a question and its tincanid and
 * tincan activity definition
 */
class QuestionItem {

    /** Activity TinCan Id - might be null*/
    String activityId;

    /** Activity TinCan Definition (JSON string)  - might be null*/
    String tinCanDefinition;

    /** HTML text of question */
    String questionHTML;

    QuestionItem(String questionHTML, String activityId, String tinCanDefinition) {
        this.questionHTML = questionHTML;
        this.activityId = activityId;
        this.tinCanDefinition = tinCanDefinition;
    }

    public String toString() {
        return this.questionHTML;
    }

}

/**
 * Utility container class to represent an MCQ Answer item.  Replaces the 
 * RadioButton based version so that it's easier to select and has fewer
 * focus issues
 * 
 * 
 * @author mike
 */
class MCQAnswerItem extends Container implements FocusListener {

    /** our host idevice*/
    private MCQIdevice idevice;

    protected Answer answer;

    HTMLComponent realComp;

    /**
     * Constructor 
     * 
     * @param idevice our host idevice
     */
    MCQAnswerItem(MCQIdevice idevice, HTMLComponent realComp, Answer answer) {
        this.idevice = idevice;
        this.answer = answer;
        addFocusListener(this);
        GridLayout layout = new GridLayout(1, 1);
        setLayout(layout);
        this.realComp = realComp;

        realComp.setFocusable(false);
        realComp.setScrollable(false);
        addComponent(realComp);
        setFocusable(true);
        Form f;
    }

    public Dimension getPreferredSize() {
        Dimension d = realComp.getPreferredSize();

        return new Dimension(d.getWidth() + (idevice.borderWidthHilight * 2) + (idevice.padding * 2),
                d.getHeight() + (idevice.borderWidthHilight * 2) + (idevice.padding * 2));
    }

    /**
     * Comparator method to be used to check for correct ordering by the user
     * 
     * @param other
     * @return true if this is the same object, false otherwise
     */
    public boolean equals(Object other) {
        if (other == this) {
            return true;
        }

        return false;
    }

    public void keyReleased(int k) {
        //the big button
        if (k == -5) {
            fireActionEvent(k);
        }
        super.keyReleased(k); //To change body of generated methods, choose Tools | Templates.
    }

    /**
     * When we get focus - tell the parent idevice about this
     * 
     * @param cmpnt 
     */
    public void focusGained(Component cmpnt) {
        int x = 0;
    }

    public void focusLost(Component cmpnt) {
        int x = 0;
    }

    /**item is held down / ready to move*/
    boolean active = false;

    /** action listeners - used for going active/inactive*/
    Vector actionListeners;

    /**
     * Add ActionListener for when this item goes active/inactive
     * @param l ActionListener
     */
    public void addActionListener(ActionListener l) {
        if (actionListeners == null) {
            actionListeners = new Vector();
        }
        actionListeners.addElement(l);
    }

    /**
     * Remove an ActionListener for when this item goes active/inactive
     * 
     * @param l ActionListener
     */
    public void removeActionListener(ActionListener l) {
        actionListeners.removeElement(l);
    }

    /**
     * Fire event for keycode
     * @param k keycode
     */
    void fireActionEvent(int k) {
        ActionEvent evt = new ActionEvent(this, k);
        MLearnUtils.fireActionEventToAll(actionListeners, evt);
    }

    /**
     * Update the style to set the border color and width for this item according
     * to whether it is currently active or inactive
     */
    public void updateStyle() {
        for (int j = 0; j < SortIdevice.allDirs.length; j++) {
            //TODO: remove dependency on SortIdevice
            getStyle().setPadding(SortIdevice.allDirs[j], idevice.padding, true);
            getSelectedStyle().setPadding(SortIdevice.allDirs[j], idevice.padding, true);
        }

        int color = (active == true) ? idevice.activeColor : 0x000000;

        getStyle().setBorder(Border.createLineBorder(idevice.borderWidthNormal, color));

        getSelectedStyle().setBorder(Border.createLineBorder(idevice.borderWidthHilight, color));

    }

    /**
     * Set whether this so it can be moved around (or not) 
     * @param active true if it should be 
     */
    public void setActive(boolean active) {
        if (active) {
            getSelectedStyle()
                    .setBorder(Border.createLineBorder(MCQIdevice.borderWidthHilight, SortIdevice.activeColor));
        } else {
            getSelectedStyle()
                    .setBorder(Border.createLineBorder(MCQIdevice.borderWidthHilight, SortIdevice.normalColor));
        }

        this.active = active;
        repaint();
    }

}