com.toughra.mlearnplayer.MLearnPlayerMidlet.java Source code

Java tutorial

Introduction

Here is the source code for com.toughra.mlearnplayer.MLearnPlayerMidlet.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;

import com.sun.lwuit.events.ActionEvent;
import com.sun.lwuit.events.ActionListener;
import com.sun.lwuit.plaf.UIManager;
import com.sun.lwuit.util.Resources;
import com.sun.lwuit.*;
import com.sun.lwuit.animations.CommonTransitions;
import com.sun.lwuit.html.HTMLCallback;
import com.sun.lwuit.html.HTMLComponent;
import com.sun.lwuit.layouts.BorderLayout;
import com.sun.lwuit.plaf.Border;
import java.io.*;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Random;
import java.util.Vector;
import javax.microedition.io.Connector;
import javax.microedition.io.file.FileConnection;
import javax.microedition.media.Manager;
import javax.microedition.media.Player;
import javax.microedition.media.PlayerListener;
import javax.microedition.media.control.VolumeControl;
import javax.microedition.midlet.*;
import com.toughra.mlearnplayer.datatx.MLObjectPusher;
import com.toughra.mlearnplayer.datatx.MLServerThread;
import com.toughra.mlearnplayer.idevices.EXERequestHandler;
import com.toughra.mlearnplayer.idevices.HTMLIdevice;
import com.toughra.mlearnplayer.xml.XmlNode;
import org.json.me.JSONException;
import org.json.me.JSONObject;

/**
 * 
 * This is the main MIDLet that really runs the show of Ustad Mobil.  
 * 
 * @author mike
 */
public class MLearnPlayerMidlet extends MIDlet implements ActionListener, Runnable, PlayerListener {

    /**Base folder name for the learning package that the user is currently running*/
    public String currentPackageURI;

    /** The table of contents and idevice/href cache*/
    public EXETOC myTOC = null;

    /** The Table of contents form */
    private com.sun.lwuit.Form TOCForm = null;

    /**the id of the idevice that the user is currently on*/
    public String currentIdevice = null;

    /**The index of the current idevice in the page*/
    public int currentIdeviceIndex = -1;

    /*a reference to the idevice currently shown*/
    public Idevice currentIdeviceObj = null;

    /*the idevices on the current page*/
    protected String[] ideviceIdList;

    /*the titles that are on the current page*/
    private String[] ideviceTitleList;

    /** Href of the currently loaded page */
    public String currentHref;

    /** The system clock time when activity on the current page started */
    public long currentPageStartTime = 0;

    /**next page href to show*/
    public String nextHref;

    /*prev page href to show*/
    public String prevHref;

    /*the current collection id (as defined by packagefolder/execollectionid flat file*/
    public String currentColId;

    /*The current package ID - normally the folder name of the package*/
    public String currentPkgId;

    //the cached next and previous links for the url given by nextPrevLinksCachedForURL
    //public String[] cachedURLNextPrevLinks;
    /** Cached next and previous links*/
    public String nextPrevLinksCachedForURL;

    /** 
     * The ID of the cached idevice (e.g. next device) if it has been cached,
     * otherwise null
     */
    public String cachedIdeviceId;

    /** Media player object to be used*/
    private Player player;

    /** InputStream used for the player as required*/
    private InputStream playerInputStream;

    /** The browse form used to select the first piece of content*/
    public ContentBrowseForm contentBrowser = null;

    /** The main options menu */
    MLearnMenu menuFrm;

    /**the current idevice form*/
    Form deviceForm;

    /** The form showing right now (be it TOC,device, or collection) */
    public Form curFrm;

    /**Default time to show feedback (in ms)*/
    public int fbTime = 3500;

    /** The length of the currently playing media*/
    public long currentMediaLength = -1;

    /** Default shortcut keycode - for next (*) button */
    public static final int KEY_NEXT = 42;

    /** Default shortcut keycode - for previous (#) button */
    public static final int KEY_PREV = 35;

    /** Command ID for going next HREF*/
    final int NEXT_HREF = 0;

    /** Command ID for going to next idevice*/
    final int NEXT_DEVICEID = 1;

    /** String used with method to indicate we should open the first idevice
     * in a package*/
    final String firstDevice = ":FIRST";

    /** String used with method to indicate we should open the last idevice
     * in a package
     */
    final String lastDevice = ":LAST";

    /*
     * The preprocessor = if the preprocessor is media capable set to the static
     * variable as required.  If this is set to false then any calls to play
     * media will be ignored.
     */
    //#ifdef MEDIAENABLED
    //#    public static final boolean mediaEnabled = true;
    //#else
    public static final boolean mediaEnabled = false;
    //#endif

    /** Array of sound file names that can be used for positive feedback*/
    public String[] posFbURIs;

    /** Array of sound file names that can be used for negative feedback*/
    public String[] negFbURIs;

    /**time for maintenance thread to sleep*/
    final int tickTime = 2000;

    /**transition time for animated stuff - e.g. slide to slide*/
    public final int transitionTime = 1000;

    /** time to wait before/after transition before starting an idevice*/
    public final int transitionWait = 100;

    /** currently unused */
    public NavigationActionListener navListener;

    /** controls main run thread - should almost always be true*/
    boolean running = true;

    /** the main volume - used when instantiating players (from 0-100) */
    public int volume = 80;

    /** Animated image for a loading icon*/
    Image loadingImg;

    /** Dialog to house animated loading icon*/
    Dialog loadingDialog;

    /** Control if the debug log is enabled */
    boolean debugLogEnabled = true;

    /** Not really used now*/
    String debugLogFile = "log.txt";
    /** Not really used now*/
    PrintStream debugStrm;

    /** Localization resource */
    public Resources localeRes;
    /** Hashtable of keys to lookup to find string*/
    public Hashtable localeHt;

    /** Reference to self*/
    static MLearnPlayerMidlet hostMidlet;

    /**the thread running to do bluetooth push to teacher phone*/
    MLObjectPusher pushThread;

    /**whether or not to just open the next chapter's first page right away*/
    boolean autoPageOpen = true;

    /**whether we have already done an auto open on return*/
    boolean returnPosDone = false;

    public static final String versionInfo = "0.9.9b";

    /** Set the RTL Mode on the basis of the package language */
    public static final int RTLMODE_PACKAGE = 0;

    /** Set the RTL Mode on the basis of the settings for the app */
    public static final int RTLMODE_SETTINGS = 1;

    /** Decide on RTL on the basis of settings language or package language */
    public static int rtlMode = RTLMODE_SETTINGS;

    /** If an idevice needs some to be focused after a form is shown - set me here*/
    public Component focusMeAfterFormShows;

    /** The main xAPI server that we talk to for all operations - _MUST_ include port hostname:port only*/
    //#ifndef SERVER
    public final static String masterServer = "svr2.ustadmobile.com:8007";
    //#endif

    /** The main Course and Cloud server that we talk to for all operations - _MUST_ include port hostname:port only*/
    //#ifndef SERVER
    public final static String cloudServer = "umcloud1.ustadmobile.com:8010";
    //#endif

    //#ifdef SERVER
    //#expand public final static String masterServer = "%SERVER%";
    //#endif

    //#ifndef LOGINSKIP
    public final static boolean canSkipLogin = true;
    //#endif

    /** If server login skip is allowed or not */
    //#ifdef LOGINSKIP
    //#expand public final static boolean canSkipLogin = %LOGINSKIP%;
    //#endif

    /** Controls if it is possible to navigate or not - e.g. 
     * when System.currentTime - lastNavTime < transitionTime do nothing
     */
    long lastNavTime = 0;

    /** Lock to use for thread safety purposes */
    Object navigateLock = new Object();

    /**
     * Not really used - using EXEStrMgr instead
     * @see EXEStrMgr
     * 
     * @param msg 
     */
    public void logMsg(String msg) {
        System.out.println(msg);
    }

    /**
     * Give a reference to self
     * @return the running MIDlet
     */
    public static MLearnPlayerMidlet getInstance() {
        return hostMidlet;
    }

    /**
     * startApp will:
     *  Load theme (/theme2.res from jar file) - see build.xml
     *  Load locale res (/localisation.res from jar file) - see build.xml
     *  Setup forms and action listeners for them
     *  Display the ContentBrowseForm to allow the user to select which learning
     *  package they want to start with.
     */
    public void startApp() {
        hostMidlet = this;//uses for getInstance

        com.sun.lwuit.Display.init(this);

        EXEStrMgr mgr = EXEStrMgr.getInstance(this);

        EXEStrMgr.lg(10, "Log file opened by Ustad Mobile ");
        EXEStrMgr.writeDebugInfo();

        navListener = new NavigationActionListener(this);

        //check to see about running the teacher server
        MLServerThread.getInstance().checkServer();

        try {
            ///make sure that we are doing TextFields correctly
            ServerLoginForm.setTextFieldDefaults();

            Display.getInstance().setBidiAlgorithm(true);
            Display.getInstance().setArabizeAlgorithm(true);
            Resources r = Resources.open("/theme2.res");
            UIManager.getInstance().setThemeProps(r.getTheme("Makeover"));

            EXEStrMgr.lg(10, "Ustad Mobile version: " + MLearnPlayerMidlet.versionInfo);
            EXEStrMgr.lg(11, "Loaded theme");

            //setup locale management
            EXEStrMgr.getInstance().initLocale();
            EXEStrMgr.lg(11, "Loaded locale");

            //#ifdef CRAZYDEBUG
            //#             EXEStrMgr.lg(69, "Loading base image");
            //#endif
            loadingImg = r.getImage("loadingb64");

            //#ifdef CRAZYDEBUG
            //#             EXEStrMgr.lg(69, "Loaded base image");
            //#endif
        } catch (Exception e) {
            EXEStrMgr.lg(310, e.toString());
        }

        menuFrm = new MLearnMenu(this);

        //#ifdef CRAZYDEBUG
        //#         EXEStrMgr.lg(69, "Instantiated Menu");
        //#endif

        myTOC = new EXETOC(this);

        EXEStrMgr.lg(11, "Created menu and table of contents");

        currentPackageURI = "/mxml1";

        //TODO: If not logged in show login form
        String autoOpenContentItem = System.getProperty("com.ustadmobile.packagedir");

        if (EXEStrMgr.getInstance().getCloudUser() == null && autoOpenContentItem == null) {
            showLoginForm();
            EXEStrMgr.lg(11, "Showed login form");
        } else {
            contentBrowser = new ContentBrowseForm(this);
            contentBrowser.makeForm();
            contentBrowser.show();

            if (autoOpenContentItem != null) {
                String packageDir = contentBrowser.getPathForAutoOpenItem(autoOpenContentItem);
                openPackageDir(packageDir, true);
            }

            EXEStrMgr.lg(11, "Show content browser");
        }

        pushThread = new MLObjectPusher();
        pushThread.start();

        CompatibilityEngine.doDetection();

        /*
         * This is Nokia specific code that is used to stop the lights from dimming
         */
        if (CompatibilityEngine.nokiaUI) {
            com.nokia.mid.ui.DeviceControl.setLights(0, 100);
            new Thread(this).start();
        }

    }

    public void showLoginForm() {
        showLoginForm(false);
    }

    /**
     * 
     * @param forceLogout if true will nullify current cloud credentials
     */
    public void showLoginForm(boolean forceLogout) {
        if (forceLogout) {
            EXEStrMgr.getInstance().doCloudLogout();
        }
        ServerLoginForm loginForm = new ServerLoginForm(this);
        loginForm.addActionListener(this);
        loginForm.show();
    }

    /**
     * Set the volume being used, and if a player is running right now, update
     * the volume for it
     * @param vol int from 0-100 for volume
     */
    public void setVolume(int vol) {
        this.volume = vol;
        if (this.player != null) {
            int state = player.getState();
            if (state != Player.UNREALIZED && state != Player.CLOSED) {
                ((VolumeControl) player.getControl("VolumeControl")).setLevel(vol);
            }
        }
    }

    /**
     * the main run thread - only sleeps and wakes up the lights for Nokias
     */
    public void run() {
        while (running) {
            try {
                Thread.sleep(tickTime);
            } catch (InterruptedException e) {
            }
            if (CompatibilityEngine.isNokiaUI()) {
                //com.nokia.mid.ui.DeviceControl.setLights(0, 100);
            }
        }
    }

    /**
     * Opens a package (folder containing exetoc.xml) and shows the user the
     * list of pages in it if showTOC is true
     * 
     * @param dirName - The dirname containing the exetoc.xml file
     * @param showTOC - True if we should show table of contents (default) false otherwise
     */
    public void openPackageDir(String dirName, boolean showTOC) {
        //contentBrowser = null;
        currentPackageURI = dirName.substring(0, dirName.length() - 1);
        int slashPos = currentPackageURI.lastIndexOf('/', dirName.length() - 1);
        currentPkgId = currentPackageURI.substring(slashPos + 1);

        System.out.println("Package URI: " + currentPackageURI);
        if (showTOC) {
            showTOC();
        }

        logMsg("started");
    }

    /**
     * Overloaded function
     * 
     * @param dirName - The dirname containing the exetoc.xml file
     */
    public void openPackageDir(String dirName) {
        openPackageDir(dirName, true);
    }

    /**
     * Opens a collection dir (containing execollection.xml) .
     * If autoPageOpen is true then show the last page that the learner
     * was using from before.
     * 
     * @param dirName 
     */
    public void openCollectionDir(String dirName) {
        //dirname comes with trailing /
        stopCurrentIdevice();
        myTOC.colBaseHREF = dirName.substring(0, dirName.length() - 1);
        myTOC.readCollection(dirName + "execollection.xml");
        Form colForm = myTOC.getCollectionForm();
        curFrm = colForm;
        currentColId = MLearnUtils.getCollectionID(dirName);
        colForm.show();

        //check and see if there is a saved last position
        String lastPos = EXEStrMgr.getInstance().getPref("lastpage." + currentColId);
        if (lastPos != null && returnPosDone == false) {
            //we need to go to where they last were...
            EXEStrMgr.lg(12, "Last position attempting to open " + lastPos);
            try {
                returnPosDone = true;
                int slashPos = lastPos.indexOf('/');
                String lastPkg = lastPos.substring(0, slashPos);
                String lastHref = lastPos.substring(slashPos + 1);

                String pkgDir = myTOC.colBaseHREF + "/" + lastPkg + "/";
                hostMidlet.openPackageDir(pkgDir, false);
                loadTOC(true);

                hostMidlet.loadPage(lastHref);
            } catch (Exception e) {
                EXEStrMgr.lg(311, "Error attempting to load last page : " + lastPos, e);
            }
        }
        returnPosDone = true;

    }

    /**
     * Utility method that could be used to request opening a FileConnection
     * resource a few times.
     * 
     * @param URL URL to pass to the FileConnection object
     * @return the InputStream once its open
     * @throws IOException 
     */
    public InputStream retryGetFile(String URL) throws IOException {
        IOException lastException = null;
        int maxTries = 3;
        int retryWait = 200;
        for (int attmpt = 0; attmpt < maxTries; attmpt++) {
            try {
                InputStream is = Connector.openInputStream(URL);
                return is;
            } catch (IOException e) {
                e.printStackTrace();
                logMsg("Exception attempting to open fs attempt " + attmpt + ": " + e.toString());
                try {
                    Thread.sleep(retryWait);
                } catch (InterruptedException ie) {
                }
                System.gc();
                lastException = e;
            }
        }
        throw lastException;
    }

    /**
     * Returns an input stream by checking the URL.  
     * URL starts file:// - will use IOWatcher to preload the file contents
     *  if this is not a media file ending with mp3, 3gp or wav
     * If URL starts file:// and is a media file will return FileConnection.openInputStream
     * If URL starts with / will return getClass().getResourceAsStream
     * Else will interpret as a relative link, prefix the currentPackageURI, and apply
     * the same logic as if for file://
     * 
     * @param URL URL as above
     * @return InputStream for the URL
     * @throws IOException if there is an IOException dealing with the file system
     */
    public InputStream getInputStreamReaderByURL(String URL) throws IOException {
        boolean isMedia = URL.endsWith("mp3") || URL.endsWith("3gp") || URL.endsWith("wav") || URL.endsWith("mpg")
                || URL.endsWith("mp4");

        EXEStrMgr.lg(13, "Opening inputstream for " + URL);

        if (URL.startsWith("file://")) {
            if (isMedia) {
                //logMsg("Opening " + URL + " Direct");
                return Connector.openInputStream(URL);
            } else {
                //logMsg("Opening " + URL + " through Byte Array");
                return IOWatcher.makeWatchedInputStream(URL);
            }
        } else if (URL.startsWith("/")) {
            return getClass().getResourceAsStream(URL);
        } else {
            //this is a relative link
            System.out.println("Opening File" + URL);
            if (isMedia) {
                //logMsg("Opening " + URL + " direct");
                return Connector.openInputStream(currentPackageURI + "/" + URL);
            } else {
                //logMsg("Opening " + URL + " through Byte Array");
                return IOWatcher.makeWatchedInputStream(currentPackageURI + "/" + URL);
            }
        }
    }

    /**
     * Overload of makeHTMLComponent (String, HTMLCallback)
     * 
     * 
     * @param htmlStr
     * @return 
     */
    public HTMLComponent makeHTMLComponent(String htmlStr) {
        return makeHTMLComponent(htmlStr, null);
    }

    /**
     * This makes an HTMLComponent with a given HTML String.  It will use a 
     * RequestHandler so that images etc. will be loaded from the current
     * package path.
     * 
     * If the package is running Right to Left then a dir tag will be set by
     * the RequestHandler to make the HTML right to left.
     * 
     * @param htmlStr HTML String to show (do not include &ltbody&gt; &lt;html&gt; etc.
     * @param callback HTMLCallback object if desired.  Otherwise leave null
     * 
     * @return HTMLComponent that will display the given HTML code.
     */
    public HTMLComponent makeHTMLComponent(String htmlStr, HTMLCallback callback) {
        HTMLComponent htmlComp = new HTMLComponent(new EXERequestHandler(this, htmlStr));
        htmlComp.setIgnoreCSS(false);
        htmlComp.setShowImages(true);
        Font bodyFont = Font.getBitmapFont("bodyFont");
        Font titleFont = Font.getBitmapFont("titleFont");
        htmlComp.setDefaultFont("arial.12", bodyFont);
        //HTMLComponent.addFont("arial.60", bodyFont);
        if (callback != null) {
            htmlComp.setHTMLCallback(callback);
        }

        htmlComp.setPage("idevice://current/");

        return htmlComp;
    }

    /**
     * Shows the main options menu (continue, repeat, about, settings, etc)
     * @param menuItemsToShow array representing which items to show
     */
    public void showMenu(boolean[] menuItemsToShow) {
        menuFrm.updateFieldsFromPrefs();
        menuFrm.show(menuItemsToShow);
    }

    /**
     * Returns Menu
     * @return MLearnMenu in use
     */
    public MLearnMenu getMenu() {
        return menuFrm;
    }

    /*
     * Show the main options menu
     */
    public void showMenu() {
        menuFrm.updateFieldsFromPrefs();
        menuFrm.show();
    }

    /**
     * See overloaded function
     */
    public void loadTOC() {
        loadTOC(false);
    }

    /**
     * Load the table of contents
     * 
     * @param hide - Do not show the table of contents form itself immediately.  Do not set curFrm
     * 
     */
    public void loadTOC(boolean hide) {
        myTOC.readList(currentPackageURI + "/exetoc.xml");

        //now check for feedback sounds
        try {
            FileConnection con = (FileConnection) Connector.open(currentPackageURI);
            String prefixes[] = new String[] { "exesfx_good", "exesfx_wrong" };
            String extsn = ".mp3";

            for (int i = 0; i < prefixes.length; i++) {
                int c = 0;
                Vector foundFiles = new Vector();
                try {
                    boolean moreFiles = true;
                    do {
                        try {
                            String filename = prefixes[i] + c + extsn;
                            Enumeration e = con.list(filename, true);
                            if (e.hasMoreElements()) {
                                //means this file exists
                                foundFiles.addElement(filename);
                                c++;
                            } else {
                                moreFiles = false;
                            }
                        } catch (Exception e) {
                            moreFiles = false;
                        }
                    } while (moreFiles);
                } catch (Exception e) {
                    System.out.println("Exception occured looking for audio files");
                } finally {
                    if (foundFiles.size() > 0) {
                        if (prefixes[i].equals("exesfx_good")) {
                            posFbURIs = new String[foundFiles.size()];
                            foundFiles.copyInto(posFbURIs);
                        } else {
                            negFbURIs = new String[foundFiles.size()];
                            foundFiles.copyInto(negFbURIs);
                        }
                    }
                }
            }
            con.close();
        } catch (Exception e) {
            e.printStackTrace();
        }

        TOCForm = myTOC.getForm();
        myTOC.getList().addActionListener(this);
        if (!hide) {
            curFrm = TOCForm;
        }
    }

    /**
     * Used if we are using a loading dialog (now loads pretty quickly, so
     * not really used).  Will remove the loading dialog and show the toc 
     * form
     */
    public void tocReady() {
        if (loadingDialog != null) {
            loadingDialog.setVisible(false);
            loadingDialog.dispose();
            loadingDialog = null;
        }
        TOCForm.show();
    }

    /**
     * Generates a loading dialog
     * @return Dialog with animated loading image
     */
    public Dialog makeLoadingDialog() {
        Dialog dlg = new Dialog();
        Button loadingButton = new Button(loadingImg);
        loadingButton.getStyle().setBorder(Border.createEmpty());
        loadingButton.getSelectedStyle().setBorder(Border.createEmpty());

        dlg.addComponent(loadingButton);

        return dlg;
    }

    /**
     * Show table of contents form
     */
    public void showTOC() {
        stopCurrentIdevice();

        //show the loading screen

        loadingDialog = makeLoadingDialog();
        loadingDialog.showPacked(BorderLayout.CENTER, false);

        Thread loadThread = new Thread(new Runnable() {
            public void run() {
                loadTOC();
                Display.getInstance().callSeriallyAndWait(new Runnable() {
                    public void run() {
                        tocReady();
                    }
                });
            }
        });

        loadThread.start();

    }

    /**
     * Load the specified page - show the first idevice in the list
     * 
     * @param pageHref href to load (e.g. pagename.xml)
     */
    public void loadPage(String pageHref) {
        loadPage(pageHref, false);
    }

    /**
     * Record the TinCan 
     * @param nextPageHref - The next page href that is to be shown.
     */
    public void recordPageTinCan(String nextPageHref) {
        //if going to a new page and we have a time record on the last page
        if (currentHref != null && !currentHref.equals(nextPageHref)) {
            long duration = System.currentTimeMillis() - currentPageStartTime;
            TOCCachePage cPg = myTOC.cache.getPageByHref(currentHref);
            JSONObject actorObj = EXEStrMgr.getInstance().getTinCanActor();
            if (actorObj != null && cPg != null) {
                String pageName = cPg.href.substring(0, cPg.href.lastIndexOf('.'));
                int slashPos = currentPackageURI.lastIndexOf('/', currentPackageURI.length() - 1);
                String folderName = currentPackageURI.substring(slashPos + 1);
                String prefix = MLearnUtils.TINCAN_PREFIX + "/" + folderName;
                JSONObject tinCanStmt = UMTinCan.makePageViewStmt(prefix, pageName, cPg.title, "en-US", duration,
                        actorObj);
                EXEStrMgr.getInstance().queueTinCanStmt(tinCanStmt);
            }
        }

        //page change is taking place or first page is being viewed
        if (currentPageStartTime == 0 || !currentHref.equals(nextPageHref)) {
            currentPageStartTime = System.currentTimeMillis();
        }
    }

    /**
     * For use with Idevices - get the currently playing pagename
     * 
     * @return folderName/Pagename
     */
    public String getTinCanPage() {
        String pageName = this.currentHref.substring(0, this.currentHref.lastIndexOf('.'));
        int slashPos = currentPackageURI.lastIndexOf('/', currentPackageURI.length() - 1);
        String folderName = currentPackageURI.substring(slashPos + 1);
        return folderName + "/" + pageName;
    }

    /**
     * Loads a given page.  If goLast is true, show the last idevice (e.g. reversing)
     * Otherwise - show the first idevice
     * 
     * @param pageHref href to laod - eg. pagename.xml
     * @param goLast if true show the last idevice on the page, otherwise show the first
     */
    public void loadPage(String pageHref, boolean goLast) {
        String myHref = (pageHref != null ? pageHref : this.currentHref);
        recordPageTinCan(myHref);

        Object[] pageIdeviceInfo = myTOC.getPageIdeviceList(currentPackageURI + "/exetoc.xml", myHref);
        ideviceIdList = (String[]) pageIdeviceInfo[EXETOC.PAGELIST_IDEVICEIDS];
        ideviceTitleList = (String[]) pageIdeviceInfo[EXETOC.PAGELIST_IDEVICETITLES];
        TOCCachePage cPg = myTOC.cache.getPageByHref(myHref);
        this.nextHref = cPg.nextHref;
        this.prevHref = cPg.prevHref;

        int index = (goLast == false) ? 0 : ideviceIdList.length - 1;
        EXEStrMgr.lg(14, "Showing idevice for page " + pageHref + " : " + index);

        if (ideviceIdList.length == 0) {
            //this needs to show an idevice (blank html or something should do)
            Idevice blankHTMLDevice = new HTMLIdevice(this, " ");
            showIdevice(blankHTMLDevice, null);
        } else {
            showIdevice(myHref, ideviceIdList[index]);
        }

    }

    /**
     * Utility method that makes sure a stream gets opened as UTF-8
     * 
     * @param URL to open
     * @return InputStreamReader that will read UTF8
     * 
     * @throws IOException 
     */
    public InputStreamReader getUTF8StreamForURL(String URL) throws IOException {
        InputStreamReader reader = new InputStreamReader(this.getInputStreamReaderByURL(URL), "UTF-8");
        return reader;
    }

    /**
     * Get info about what is the next device to load
     * @param increment - how many idevices to go back/forth
     * 
     * @return 
     */
    public String[] getNextIDeviceIdAndHref(int increment) {
        String[] retValue = new String[2];
        int nextIdeviceIndex = currentIdeviceIndex + increment;

        //TODO: Change all "href" in tags to have .xml in it - confusing as heck otherwise
        if (nextIdeviceIndex >= 0 && nextIdeviceIndex < ideviceIdList.length) {
            retValue[NEXT_HREF] = this.currentHref;
            retValue[NEXT_DEVICEID] = ideviceIdList[this.currentIdeviceIndex + increment];
        } else if (nextIdeviceIndex >= ideviceIdList.length && this.nextHref != null) {
            retValue[NEXT_DEVICEID] = firstDevice;
            retValue[NEXT_HREF] = this.nextHref;
        } else if (nextIdeviceIndex < 0 && this.prevHref != null) {
            retValue[NEXT_DEVICEID] = lastDevice;
            retValue[NEXT_HREF] = this.prevHref;
        }

        return retValue;
    }

    /**
     * Checks to see if we should accept navigation commands (avoid issues with people pushing
     * buttons too fast)
     * 
     * @return true if ok to go, false otherwise
     */
    private final boolean canNavigateNow() {
        return (System.currentTimeMillis() - lastNavTime) > transitionTime;
    }

    /**
     * 
     * Goes increment devices along to show the next (or previous) idevice
     * @param increment how many idevices to move along (can be +ve/-ve)
     * 
     */
    public void showNextDevice(int increment) {
        if (!canNavigateNow()) {
            System.out.println("Blocking navigate because something already happening");
            return;
        }

        stopCurrentIdevice();
        int nextIdeviceIndex = currentIdeviceIndex + increment;
        if (nextIdeviceIndex >= 0 && nextIdeviceIndex < ideviceIdList.length) {
            String nextIdeviceId = ideviceIdList[this.currentIdeviceIndex + increment];
            showIdevice(this.currentHref, nextIdeviceId);
        } else if (nextIdeviceIndex >= ideviceIdList.length && this.nextHref != null) {
            // go to the next page
            loadPage(this.nextHref);
        } else if (nextIdeviceIndex < 0 && this.prevHref != null) {
            loadPage(this.prevHref, true);
        } else {
            if (myTOC.collection != null) {
                EXEStrMgr.lg(15, "Package to package nav starting ");
                int currentColIndex = myTOC.getCollectionIndex(currentPackageURI);
                int nextColIndex = currentColIndex;

                if (nextIdeviceIndex >= ideviceIdList.length && this.nextHref == null) {
                    if (currentColIndex < (myTOC.collection.length - 1)) {
                        nextColIndex = currentColIndex + 1;
                    }
                } else if (nextIdeviceIndex <= 0 && this.prevHref == null) {
                    if (currentColIndex >= 1) {
                        nextColIndex = currentColIndex - 1;
                    }
                }

                if (nextColIndex != currentColIndex) {
                    currentPackageURI = myTOC.getColBaseHref(nextColIndex);
                    currentPkgId = myTOC.getColPkgId(nextColIndex);
                    if (currentPackageURI.endsWith("/")) {
                        currentPackageURI = currentPackageURI.substring(0, currentPackageURI.length() - 1);
                        EXEStrMgr.lg(16, "Set current package URI to " + currentPackageURI);
                    }
                    myTOC.tocList = null;

                    if (autoPageOpen) {
                        loadTOC(true);
                        boolean goLast = false;
                        String pageURL = null;
                        if (increment > 0) {
                            pageURL = myTOC.getPageHref(0);
                        } else {
                            pageURL = myTOC.getPageHref(myTOC.getNumPages() - 1);
                            goLast = true;
                        }

                        loadPage(pageURL, goLast);
                    } else {
                        showTOC();
                    }
                }
            } else {
                //if we have no collection to show and we're at the end go to 
                //table of contents.
                showTOC();
            }
        }

        lastNavTime = System.currentTimeMillis();

    }

    /**
     * Stop the currently running idevice
     */
    void stopCurrentIdevice() {
        if (currentIdeviceObj != null) {
            currentIdeviceObj.stop();

            currentIdeviceObj.dispose();
            currentIdeviceObj = null;

            stopMedia(false);
            System.gc();
        }
    }

    /**
     * Show the idevice given by the device object
     * 
     * @param device - The idevice to show
     * @param ideviceId - the deviceid
     */
    public void showIdevice(Idevice device, String ideviceId) {
        try {

            if (device.getMode() == Idevice.MODE_LWUIT_FORM) {
                if (!Display.isInitialized()) {
                    Display.init(this);
                }

                MLearnUtils.checkFreeMem();

                deviceForm = device.getForm();
                curFrm = deviceForm;

                MLearnUtils.printFreeMem(this, "Got form from " + device.getClass().getName());

                //this is hear when we aren't really strictly showing a new idevice (e.g.
                // the slideshow device is just changing slides
                if (ideviceId != null) {
                    this.currentIdevice = ideviceId;
                    for (int i = 0; i < ideviceIdList.length; i++) {
                        if (ideviceIdList[i].equals(ideviceId)) {
                            currentIdeviceIndex = i;
                            break;
                        }
                    }
                }

                //adds the command to the form for menu items
                navListener.addMenuCommandsToForm(deviceForm);

                //Add listeners for navigation purposes
                deviceForm.addKeyListener(KEY_NEXT, this);
                deviceForm.addKeyListener(KEY_PREV, this);

                deviceForm.setTransitionOutAnimator(
                        CommonTransitions.createSlide(CommonTransitions.SLIDE_HORIZONTAL, true, transitionTime));
                EXEStrMgr.lg(17, "Showing idevice form for " + ideviceId);

                //save where we are if we are using a collection
                deviceForm.show();
                if (focusMeAfterFormShows != null) {
                    Component currentFocus = deviceForm.getFocused();
                    deviceForm.setFocused(null);
                    deviceForm.setFocused(focusMeAfterFormShows);
                    focusMeAfterFormShows = null;
                }

                MLearnUtils.printFreeMem(this, "show " + device.getClass().getName());

                //tell the device to start anything that it wants to run - media -etc
                device.start();
                MLearnUtils.printFreeMem(this, "startedr " + device.getClass().getName());
                currentIdeviceObj = device;
            }
        } catch (Exception e) {
            e.printStackTrace();
            EXEStrMgr.lg(312, e.toString());
        }
    }

    /**
     * Show the idevice given by the id which is expected to be contained in
     * the file given by pageHREF.  Will search through the idevices given
     * and then instantiate an idevice by using the IdeviceFactory
     * 
     * @param pageHREF URL of page to load 
     * @param ideviceId deviceId to open
     */
    public void showIdevice(String pageHREF, String ideviceId) {
        stopCurrentIdevice();

        currentHref = pageHREF;

        try {
            EXEStrMgr.lg(18, "Loading idevice now for " + pageHREF + ":" + ideviceId);
            InputStream inStream = getInputStreamReaderByURL(currentPackageURI + "/" + pageHREF);
            XmlNode cachedData = null;

            Idevice device = IdeviceFactory.makeIdevice(inStream, ideviceId, this, cachedData);

            //Save the current location of the student
            if (currentColId != null && pageHREF != null && this.currentHref != null) {
                String lastURLRecordVal = this.currentPkgId + "/" + pageHREF;
                EXEStrMgr.getInstance().setPref("lastpage." + currentColId, lastURLRecordVal);
                EXEStrMgr.lg(19, "Recorindg lastpage." + currentColId + " as " + lastURLRecordVal);
            }

            showIdevice(device, ideviceId);
        } catch (Exception e) {
            EXEStrMgr.lg(313, "exception whilst loading idevice " + pageHREF + ":" + ideviceId + e.toString());
        }
    }

    /**
     * Main event handler - looks out for selections of a package from the current
     * table of contents list and when the user pushes the next or previous
     * buttons (default * and #)
     * @param evt 
     */
    public void actionPerformed(ActionEvent evt) {
        Object src = evt.getSource();
        String pageHref = null;
        if (src instanceof List) {
            List tocList = (List) src;
            int currentIndex = tocList.getSelectedIndex();
            pageHref = myTOC.getPageHref(currentIndex);
            myTOC.getList().removeActionListener(this);

            //System.gc();
            currentHref = pageHref;
            loadPage(pageHref);
        } else if (src instanceof ServerLoginForm) {
            //dispose of the src
            contentBrowser = new ContentBrowseForm(this);
            contentBrowser.makeForm();
            contentBrowser.show();
        } else if (src instanceof Form) {
            int key = evt.getKeyEvent();
            if (key == KEY_NEXT) {
                showNextDevice(1);
            } else if (key == KEY_PREV) {
                showNextDevice(-1);
            }
        }

        //find out what

        int x = 0;
    }

    /**
     * Pause app - does nothing
     */
    public void pauseApp() {
    }

    /**
     * Ends the application - saves all preferences, logs, and stops any
     * server threads running
     * 
     * @param unconditional 
     */
    public void destroyApp(boolean unconditional) {
        MLObjectPusher.enabled = false;//make this thread quite
        EXEStrMgr.getInstance().saveAll();
    }

    /**
     * Stops any media currently playing - and runs garbage collect after
     */
    public void stopMedia() {
        stopMedia(true);
    }

    /**
     * Stops the media currently playing.  Closes and deallocates player.
     * 
     * @param doAutoGc If set true will call System.gc() after stopping media
     */
    public void stopMedia(boolean doAutoGc) {
        if (player != null && mediaEnabled == true) {
            try {
                MLearnUtils.checkFreeMem();
                player.stop();
                player.close();
                player.deallocate();
                player = null;
                logMsg("Played deallocated by stopmedia method");
                MLearnUtils.printFreeMem(this, "Stopped Media before gc");
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                if (doAutoGc) {
                    System.gc();
                    MLearnUtils.printFreeMem(this, "Stopped Media after gc");
                }
            }
        }
    }

    /**
     * Utility method used with player - returns mime type for files ending
     * .mp3, .au, .wav, .3gp, .mpg
     * 
     * @param fileName
     * @return 
     */
    public static String getContentType(String fileName) {
        String fileNameLower = fileName.toLowerCase();
        if (fileNameLower.endsWith(".mp3")) {
            return "audio/mp3";
        } else if (fileNameLower.endsWith(".au")) {
            return "audio/basic";
        } else if (fileNameLower.endsWith(".wav")) {
            return "audio/X-wav";
        } else if (fileNameLower.endsWith(".mpg")) {
            return "video/mpeg";
        } else if (fileNameLower.endsWith(".3gp")) {
            return "video/3gpp";
        } else if (fileNameLower.endsWith(".mp4")) {
            return "video/mp4";
        }

        //not known
        return null;
    }

    /**
     * Provides the current package uri base directory
     * @return Current package URI base directory
     */
    public String getCurrentPackageURI() {
        return currentPackageURI;
    }

    /**
     * Play the default sound that indicates a positive (e.g. correct answer)
     * event
     * 
     * @return the time of the sound in microseconds
     */
    public long playPositiveSound() {
        System.out.println("Play +ve");
        if (posFbURIs != null) {
            Random r = new Random();
            return playMedia(posFbURIs[MLearnUtils.nextRandom(r, posFbURIs.length)]);
        }

        return -1;
    }

    /**
     * Play the default negative sound
     * 
     * @return length of the sound in microseconds
     */
    public long playNegativeSound() {
        System.out.println("Play -ve");
        if (negFbURIs != null) {
            Random r = new Random();
            return playMedia(negFbURIs[MLearnUtils.nextRandom(r, negFbURIs.length)]);
        }

        return -1;
    }

    /**
     * Plays a sound file.  Will only run if mediaEnabled is set to true.
     * 
     * @param mediaURI URI of file to play e.g. file://localhost/file/foo.mp3
     * 
     * @return the length of the clip in microseconds
     */
    public long playMedia(String mediaURI) {
        stopMedia();
        MLearnUtils.checkFreeMem();
        System.out.println("asked to play: " + mediaURI);
        logMsg("asked to play: " + mediaURI);

        //now determine which one to use...
        String newMediaURI = MLearnUtils.reworkMediaURI(myTOC.audioFormatToUse, MLearnUtils.AUDIOFORMAT_NAMES,
                mediaURI);
        if (newMediaURI != null) {
            mediaURI = newMediaURI;
        }
        System.out.println("Actually going to play " + mediaURI);

        String mediaType = getContentType(mediaURI);

        mediaURI = currentPackageURI + "/" + mediaURI;

        String mediaLoc = MLearnUtils.connectorToFile(mediaURI);

        if (mediaEnabled) {
            try {
                //playerInputStream = getInputStreamReaderByURL(mediaURI);

                //player = Manager.createPlayer(playerInputStream, mediaType);

                player = Manager.createPlayer(mediaLoc);
                player.realize();
                VolumeControl vc = (VolumeControl) player.getControl("VolumeControl");
                vc.setLevel(volume);
                player.start();
                long playerTime = player.getDuration();
                this.currentMediaLength = playerTime;
                player.addPlayerListener(this);
                MLearnUtils.printFreeMem(this, "Created player for " + mediaLoc);
                return playerTime;
            } catch (Exception e) {
                EXEStrMgr.lg(314,
                        "Error creating player for " + mediaLoc + " type " + mediaType + " " + e.toString());
                if (player != null) {
                    try {
                        player.deallocate();
                    } catch (Exception e1) {
                        EXEStrMgr.lg(314, "exception deallocating faulty player " + mediaLoc + " type " + mediaType
                                + " " + e1.toString());
                    }

                    try {
                        player.close();
                    } catch (Exception e2) {
                        EXEStrMgr.lg(314, "exception closing faulty player " + mediaLoc + " type " + mediaType + " "
                                + e2.toString());
                    }
                }
            }
        } else {
            System.out.println("Media disabled");
        }

        this.currentMediaLength = (1 * 1000 * 1000);
        //wait a second
        return (1 * 1000 * 1000);
    }

    /**
     * Automatically deallocate resources once the media file itself is finished
     * 
     * @param player The media player
     * @param event event string given by PlayerListener
     * @param eventData info about event
     */
    public void playerUpdate(Player player, String event, Object eventData) {
        if (event.equals(PlayerListener.END_OF_MEDIA)) {
            if (this.player != null) {
                if (this.player.getState() != Player.CLOSED && this.player.getState() != Player.UNREALIZED) {
                    try {
                        this.player.stop();
                    } catch (Exception e) {
                        EXEStrMgr.lg(315, "exception attempting to stop player: " + e.toString());
                    }
                    try {
                        Thread.sleep(300);
                    } catch (InterruptedException e) {
                    }
                }

                this.player.close();

                try {
                    this.player.deallocate();
                } catch (Exception e) {
                    e.printStackTrace();
                    EXEStrMgr.lg(315, "Exception deallocating player: " + e.toString());
                }
                this.player = null;

                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                }

                logMsg("Player closed / deallocated by playerUpdated method");
            } else {
                logMsg("looked and player is null");
            }

            if (playerInputStream != null) {
                try {
                    logMsg("Attempting to close playerInputStream");
                    playerInputStream.close();
                    logMsg("player input stream closed by playerUpdated method");
                } catch (IOException e) {
                    e.printStackTrace();
                }
                playerInputStream = null;

            } else {
                logMsg("PlayerInputStream is already null");
            }
        }
    }

}