//++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// J2ME-Lib source file
// Copyright (c) 2007 Elmar Sonnenschein / esoco GmbH
// Last Change: 27.06.2007 by eso
//
// J2ME-Lib is free software; you can redistribute it and/or modify it under
// the terms of the GNU Lesser General Public License as published by the Free
// Software Foundation; either version 2.1 of the License, or (at your option)
// any later version.
//
// J2ME-Lib 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 Lesser General Public License for more
// details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with J2ME-Lib; if not, write to the Free Software Foundation, Inc.,
// 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA or use the contact
// information from the GNU website http://www.gnu.org
//++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
package de.esoco.j2me.midlet;
import de.esoco.j2me.J2meLib;
import de.esoco.j2me.resource.ResourceBundle;
import de.esoco.j2me.ui.ExecutableCommand;
import de.esoco.j2me.ui.MessageScreen;
import de.esoco.j2me.ui.ScreenManager;
import de.esoco.j2me.util.Executable;
import de.esoco.j2me.util.Log;
import de.esoco.j2me.util.ProcessingQueue;
import de.esoco.j2me.util.TextUtil;
import de.esoco.j2me.util.UserNotificationException;
import javax.microedition.lcdui.Command;
import javax.microedition.lcdui.Display;
import javax.microedition.lcdui.Displayable;
import javax.microedition.lcdui.Form;
import javax.microedition.midlet.MIDlet;
/********************************************************************
* A MIDlet subclass that contains standard functionality for applications and
* some infrastructure methods for J2ME-Lib. It is recommended that applications
* that are based on J2ME-Lib derive their MIDlet class from this superclass to
* make sure that all J2ME-Lib resources are initialized correctly.
*
* <p>A subclass must implement at least the abstract methods {@link #init()}
* and {@link #runApp()}. It may also choose to override other methods like
* {@link #queryQuitApp()}, {@link #cleanup()}, or {@link #about()} if it wants
* to replace or modify the default functionality. The application startup code
* will be run from a seperate thread to prevent the main MIDlet thread from
* being blocked by long-running initialization code.</p>
*
* <p>This class is intended to be used only for single-instance MIDlets because
* some J2ME-Lib functions need to statically access MIDlet-related resources
* like the LCDUI Display. If a suite of multiple MIDlets needs to be built the
* additional MIDlets should either not be derived from BasicMidlet or it must
* be made sure that only one of the MIDlets runs at a time. Else an exeception
* will be thrown from the constructor of the second BasicMidlet that is
* created.</p>
*
* <p>The method {@link J2meLib#init()} will be invoked from the static
* initializer to initialize the library resources as early as possible.</p>
*
* @author eso
*/
public abstract class BasicMidlet extends MIDlet implements Runnable
{
//~ Static fields/initializers ---------------------------------------------
/** The default wait time for the cleanup method */
protected static final int CLEANUP_WAIT_TIME = 5000;
/** The currently active BasicMidlet instance */
private static BasicMidlet theInstance;
static
{
// initialize the library as early as possible
J2meLib.init();
}
//~ Constructors -----------------------------------------------------------
/***************************************
* Creates a new instance and initializes the internal reference to the
* current MIDlet instance. If {@link J2meLib#DEBUG} is TRUE and the an
* MIDlet instance already exists an exception will be thrown because only
* one BasicMidlet instance should exist at a time.
*/
public BasicMidlet()
{
if (theInstance != null)
{
throw new IllegalStateException("Only one MIDlet instance allowed!");
}
theInstance = this;
}
//~ Methods ----------------------------------------------------------------
/***************************************
* Logs a fatal error that has been signalled by an exception and displays
* an error message box. If the exception is an instance of {@link
* UserNotificationException} the strings for the message box will be taken
* from the exception.
*
* <p>After the message box has been dismissed the midlet will be destroyed,
* whereupon the {@link #cleanup()} method will be invoked.</p>
*
* @param eCause The Exception that signalled the error
*/
public static void fatalError(Exception eCause)
{
Log.fatal(eCause);
MessageScreen.showError(getErrorTitle(eCause),
getErrorMessage(eCause),
new Executable()
{
public void execute(Object rArg)
{
theInstance.destroyApp(true);
}
});
}
/***************************************
* Returns the Display object for the currently running midlet instance.
*
* @return The instance of the Display class
*/
public static Display getDisplay()
{
return Display.getDisplay(theInstance);
}
/***************************************
* Because this class is intended to be used as a singleton instance this
* method returns the current BasicMidlet instance if it has been
* initialized already.
*
* @return The current BasicMidlet instance
*/
public static BasicMidlet getInstance()
{
return theInstance;
}
/***************************************
* Returns the property string for a certain key. This method will first
* check the MIDlet properties by invoking {@link #getAppProperty(String)}.
* If that is not set it will also try to read a system property through the
* method {@link System#getProperty(String)}.
*
* @param sKey The property key
*
* @return The property value (may be NULL)
*/
public static String getProperty(String sKey)
{
String sProperty = theInstance.getAppProperty(sKey);
if (sProperty == null)
{
try
{
sProperty = System.getProperty(sKey);
}
catch (Exception e)
{
Log.error("Could not access property " + sKey, e);
}
}
return sProperty;
}
/***************************************
* A shortcut method to get a string from the current resource bundle.
*
* @see ResourceBundle#getString(String)
*/
public static String getString(String sKey)
{
return ResourceBundle.getCurrent().getString(sKey);
}
/***************************************
* Logs a recoverable error that has been signalled by an exception and
* displays an error message box. If the exception is an instance of the
* {@link UserNotificationException} class the strings for the message box
* will be taken from the exception. After the message box has been
* dismissed the midlet will continue to run.
*
* @param eCause The Exception that signalled the error
*/
public static void recoverableError(final Exception eCause)
{
Log.error(eCause);
MessageScreen.showError(getErrorTitle(eCause),
getErrorMessage(eCause),
null);
}
/***************************************
* Displays a confirmation dialog to ask the user if he really wants to
* quit. If so, the quit() method will be invoked. Subclasses can invoke
* this method to prevent the user from unintentional exiting the
* application. Or they may override it to modify the behavior.
*
* <p>This implementation uses the resource strings "JL_MtQuit" and
* "JL_MsQuit" from the J2ME-Lib resource.</p>
*/
public void queryQuitApp()
{
// the executable will only be invoked if the user selects OK
MessageScreen.showConfirmation(getString("JL_MtQuit"),
getString("JL_MsQuit"),
true,
new Executable()
{
public void execute(Object rArg)
{
quit();
}
});
}
/***************************************
* This method quits the midlet after lettting a subclass clean up all
* allocated resources by invoking the abstract method {@link #cleanup()}.
* Subclasses can either invoke this method directly or call the method
* {@link #queryQuitApp()} which will first display a confirmation dialog
* and only call this method if the user confirms the request.
*/
public final void quit()
{
try
{
cleanup();
}
catch (Exception e)
{
Log.fatal(e);
}
theInstance = null;
notifyDestroyed();
}
/***************************************
* The {@link Runnable} interface method which will be invoked from the
* midlet thread created in the {@link #startApp()} method. It first
* initializes the midlet with the abstract {@link #init()} method, then
* runs it by invoking {@link #runApp()}.
*/
public final void run()
{
try
{
init();
runApp();
}
catch (Exception e)
{
fatalError(e);
}
}
/***************************************
* Must be implemented by subclasses to initialize resources that are needed
* by the application to run. If a critical error occurs that would prevent
* the midlet from running correctly a UserNotificationException may be
* thrown. The application startup will then be cancelled and the error
* message from the exception will be displayed to the user.
*
* @throws UserNotificationException If initialization fails
*/
protected abstract void init() throws UserNotificationException;
/***************************************
* Must be implemented by subclasses to execute the application's main code.
* It will be invoked directly after the {@link #init()} method has
* finished. In most cases the app will simply display it's main screen from
* this method which will then start to dispatch events to the application.
*/
protected abstract void runApp();
/***************************************
* Internal method to return the error message for a particular exception.
* Checks whether the exception is an instance of UserNotificationException
* and return it's message string or a default string that contains the
* execption's message otherwise.
*
* @param eCause The exception to check
*
* @return String The corresponding message string
*/
protected static String getErrorMessage(Exception eCause)
{
if (eCause instanceof UserNotificationException)
{
return ((UserNotificationException) eCause).getMessage();
}
else
{
return TextUtil.formatMessage(getString("JL_MsCmdEx"),
new Object[] { eCause.getMessage() });
}
}
/***************************************
* Internal method to return the error title for a particular exception.
* Checks whether the exception is an instance of UserNotificationException
* and return it's title or a default string otherwise.
*
* @param eCause The error causing exception to check
*
* @return String The corresponding title string
*/
protected static String getErrorTitle(Exception eCause)
{
if (eCause instanceof UserNotificationException)
{
return ((UserNotificationException) eCause).getTitle();
}
else
{
return getString("JL_MtError");
}
}
/***************************************
* This method shows a simple information screen for this application. It is
* invoked automatically by the default "About" command. It uses the
* resource strings "MtAbout" and "MsAbout" (title and text) as well as the
* image "ImAbout" that must therefore be defined in the application
* resource. Subclasses may override this method to provide a different
* about screen implementation.
*/
protected void about()
{
MessageScreen.showMessageBox(getString("MtAbout"),
"ImAbout",
getString("MsAbout"),
null,
true,
null);
}
/***************************************
* Performs a cleanup of application resources that will be invoked from the
* {@link #quit()} method. Can be overridden by by subclasses to free any
* resources allocated by them before the midlet will be terminated. The
* implementation may throw a {@link UserNotificationException} to indicate
* that the user needs to be notified of a critial error condition before
* terminating the midlet completely.
*
* <p>This default implementation lets the thread from which it has been
* invoked wait a certain amount of milliseconds as defined in the {@link
* #CLEANUP_WAIT_TIME} constant before returning and therefore terminating
* the MIDlet. The waiting is performed by invoking the method {@link
* ProcessingQueue#finish(int)}. Unless they don't use the task scheduling
* of the {@link ProcessingQueue} class subclasses that override this method
* should always either invoke super or implement the waiting for the queue
* finishing by themselves.</p>
*
* @throws UserNotificationException If a cleanup operation fails
*/
protected void cleanup() throws UserNotificationException
{
ProcessingQueue.finish(CLEANUP_WAIT_TIME);
}
/***************************************
* This method will be invoked by the J2ME runtime to terminate the midlet.
* This implementation invokes the method {@link #quit()} which in turn will
* invoke the abstract method {@link #cleanup()} and then terminate the
* MiDlet. Subclasses should preferably implement the {@link #cleanup()}
* method only instead of overriding this method.
*
* @param bUnconditional If TRUE, the MIDlet must terminate in any case
*
* @see MIDlet#destroyApp(boolean)
*/
protected void destroyApp(boolean bUnconditional)
{
quit();
}
/***************************************
* Returns a new array that contains standard application commands that
* should be displayed on all screens. The commands are instances of the
* class {@link ExecutableCommand} and will automatically execute the
* corresponding BasicMidlet methods. This default implementation returns
* the commands "Quit" and "About" (from the current resource bundle) that
* will invoke the methods {@link #queryQuitApp()} and {@link #about()},
* respectively.
*
* <p>The commands are not used by BasicMidlet directly. A subclass must
* explicitly add them to it's screens.</p>
*
* @return An new Command array
*/
protected Command[] getStandardCommands()
{
Command aQuitCmd = new ExecutableCommand(getString("Quit") + "...",
Command.EXIT,
999)
{
public void execute(Object rArg)
{
queryQuitApp();
}
};
Command aAboutCmd = new ExecutableCommand(getString("About") + "...",
Command.HELP,
99)
{
public void execute(Object rArg)
{
about();
}
};
return new Command[] { aQuitCmd, aAboutCmd };
}
/***************************************
* Returns the first visible screen of the MIDlet that will be displayed
* before the separate application thread is run from {@link #startApp()}.
* This is necessary to prevent the MIDlet from terminating before the
* application code (which is run in the thread) becomes a chance to display
* a screen.
*
* <p>The default implementation returns an empty form with the class name
* as the title. At least for a MIDlet that performs a long-running
* initialization it is recommended that it overrides this method to return
* a screen that displays an information message and some kind of progress
* indicator.</p>
*
* @return A displayable instance that is used as the MIDlet's first screen
*/
protected Displayable getStartScreen()
{
return new Form(TextUtil.getTypeName(getClass()));
}
/***************************************
* This method must be overridden by subclasses to free as many resources as
* possible before entering the paused state. This default implementation
* does nothing.
*
* @see MIDlet#pauseApp()
*/
protected void pauseApp()
{
}
/***************************************
* This method will be invoked by the J2ME runtime to start the midlet. It
* first displays the applications first LCDUI screen that is queried from
* the abstract method {@link #getStartScreen()}. Then it starts a new
* thread that invokes the {@link #run()} method, which in turn calls the
* abstract methods {@link #init()} and {@link #runApp()}.
*
* <p>A separate thread is necessary to prevent that a long-running
* initialization will block the main event handling thread of J2ME and
* therefore prevent screen updates. The start screen must be displayed
* before running the thread to prevent this MIDlet from terminating before
* one of the application methods has a chance to display a screen.</p>
*
* <p>Because it runs in a separate thread the initialization or main code
* of a subclass may run arbitrary long. But long-running processes should
* give feedback to the user, e.g. by displaying a progress bar on the first
* screen.</p>
*
* <p>The start screen will be displayed on the screen by means of the
* {@link ScreenManager#show(Displayable)} method so that it will become the
* first element on the screen stack. When the application code wants to
* display a different screen it should either replace the start screen by
* invoking {@link ScreenManager#setCurrent(Displayable)} or display the new
* screen on top of it by calling {@link ScreenManager#show(Displayable)}.
* What to choose depends on the purpose of the start screen: is it just to
* be displayed during the MIDlet start or does it need to be redisplayed
* later?</p>
*
* @see MIDlet#startApp()
*/
protected void startApp()
{
ScreenManager.show(getStartScreen());
new Thread(this).start();
}
}