
 J 2 M E    T  E  C  H    T  I  P  S

                      TIPS, TECHNIQUES, AND SAMPLE CODE


WELCOME to the Java Developer Connection(sm) (JDC)
Java(tm) 2 Platform, Micro Edition (J2ME(tm)) 
Tech Tips, for November 14, 2001. This issue covers:

     * Dealing With Dates and Times in MIDP Applications
     * Developing Custom Components for the Mobile Information 
       Device Profile
         
The J2ME Tech Tips are written by Eric Giguere
(http://www.ericgiguere.com), an engineer at iAnywhere 
Solutions, inc. Eric is the author of the book "Java 2 Micro
Edition: Professional Developer's Guide" and co-author of the 
upcoming "Mobile Information Device Profile for Java 2 Micro 
Edition," both books in John Wiley & Sons' Professional 
Developer's Guide series.

You can view this issue of the J2ME Tech Tips on the Web at
http://java.sun.com/jdc/J2METechTips/2001/tt1114.html

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
DEALING WITH DATES AND TIMES IN MIDP APPLICATIONS

Parsing and displaying dates and times is often complicated 
because of formatting and locale issues. Java 2 Platform, 
Standard Edition (J2SE(tm)) provides several classes to simplify
date and time handling -- classes such as java.util.Calendar, 
java.util.Date, java.util.TimeZone, and java.text.DateFormat. By 
comparison, the Mobile Information Device Profile (MIDP) defines
only subsets of the Calendar, Date and TimeZone classes, and does 
not include any form of DateFormat.  How, then, can your MIDP
applications properly handle dates and times?

The answer lies in the javax.microedition.lcdui.DateField class, 
part of the MIDP high-level user interface API. DateField is an 
interactive user interface component that displays a date, time,
or both. It also allows you to edit the date and time. DateField 
extends the Item class. This means that DateField components can 
be placed on Form objects. So the first step in using a DateField 
is to create a Form and place the DateField on the form:

    Form f = new Form( "A Form" );
    f.append( df );
    
As with any Item, the DateField is displayed only when the form 
is made active by calling Display.setCurrent.

The DateField class defines two constructors:

    public DateField( String label, int mode );
    public DateField( String label, int mode,
                      java.util.TimeZone zone );
                    
To properly display dates and times, a DateField instance needs 
to know which time zone to use. The two-argument constructor uses 
the device's default time zone. The three-argument constructor 
lets you specify an explicit time zone if the default is 
inappropriate. Note that you can't change the displayed time zone
without creating a new instance of DateField.

The first two arguments are identical in both constructors. The 
first argument is the label to display alongside the field -- use 
null if there is no label. The second argument is the input mode 
of the field. There are three possible modes, these are declared 
as constants in the DateField class:

    public static final int DATE = 1;
    public static final int TIME = 2;
    public static final int DATE_TIME = 3;
    
The input mode controls what the field displays: a date only, 
a time only, or a combined date and a time. You can change the 
input mode at any time by calling the setInputMode method.

When you create a new DateField instance, you do not have to set
a date or time. The following code, for example, displays an 
uninitialized date:

    Display display = ....; // initialized elsewhere
    Form f = new Form( "An Empty Date" );
    DateField df = new DateField( "Date:", 
                                  DateField.DATE );
    f.append( df );
    display.setCurrent( f );
    
To initialize the field to a particular date or time, call 
setDate and pass in a java.util.Date object initialized to the 
correct value:

    Calendar c = Calendar.getInstance();
    c.set( Calendar.MONTH, Calendar.OCTOBER );
    c.set( Calendar.DAY_OF_MONTH, 18 );
    c.set( Calendar.YEAR, 1996 );
    c.set( Calendar.HOUR_OF_DAY, 16 );
    c.set( Calendar.MINUTE, 39 );
    c.set( Calendar.SECOND, 45 );
    c.set( Calendar.MILLISECOND, 0 );
    
    Date moment = c.getTime();
    DateField df = new DateField( null, 
                                  DateField.DATE_TIME );
    df.setTime( moment );
  
A Date object represents a moment in time (in coordinated
universal time, or UTC, to be exact) as the number of 
milliseconds since midnight, January 1, 1970. Use a Calendar 
instance to create a Date instance, as shown above.  
  
Note that a DateField in TIME input mode requires the date 
portion to be set to January 1, 1970. Two useful routines for 
clearing out the date portion of a Date and for combining two 
Date objects into a single object are as follows:

    // Return a Date with the time intact but the date
    // set to January 1, 1970

    public static Date clearDate( Date d ){
        Calendar c = Calendar.getInstance();
        c.setTime( d );
        c.set( Calendar.MONTH, Calendar.JANUARY );
        c.set( Calendar.DAY_OF_MONTH, 1 );
        c.set( Calendar.YEAR, 1970 );
        return c.getTime();
    }

    // Combine a date and time into a single 
    // Date instance

    public static Date combineDateTime( 
                                Date date, Date time ){
        Calendar cd = Calendar.getInstance();
        Calendar ct = Calendar.getInstance();

        cd.setTime( date );
        ct.setTime( time );

        ct.set( Calendar.MONTH, 
                            cd.get( Calendar.MONTH ) );
        ct.set( Calendar.DAY_OF_MONTH, 
                     cd.get( Calendar.DAY_OF_MONTH ) );
        ct.set( Calendar.YEAR, 
                             cd.get( Calendar.YEAR ) );

        return ct.getTime();
    }

Always do your date manipulation using the Calendar class, not 
using the raw milliseconds value stored in a Date object.

After a DateField is displayed, the system will allow the user to 
select the object and edit the date, time or both, depending on 
the input mode. Whenever you need to obtain the new date/time, 
call the getDate method:

    DateField df = ....;
    Date editedDate = df.getDate();
    
Here is a simple MIDlet that lets you view and edit dates and 
times using all three input modes.

import java.util.*;
import javax.microedition.lcdui.*;
import javax.microedition.midlet.*;

/**
 * Demonstration of time/date editing using the MIDP
 * DateField class.
 */

public class DateFieldTest extends MIDlet {

    private Display display;
    
    // Define our Command objects
  
    private Command exitCommand =
             new Command( "Exit", Command.EXIT, 1 ); 
    private Command okCommand =
             new Command( "OK", Command.OK, 1 );
    private Command cancelCommand =
             new Command( 
                         "Cancel", Command.CANCEL, 1 );

    public DateFieldTest(){
    }

    protected void destroyApp( boolean unconditional )
                    throws MIDletStateChangeException {
        exitMIDlet();
    }

    protected void pauseApp(){
    }
 
    protected void startApp()
                    throws MIDletStateChangeException {
        if( display == null ){ // first time called...
            initMIDlet();
        }
    }

    private void initMIDlet(){
        display = Display.getDisplay( this );
        testList = new TestList();
        display.setCurrent( testList );
    }

    public void exitMIDlet(){
        notifyDestroyed();
    }
 
    // Return a Date with the time intact but the date
    // set to January 1, 1970

    public static Date clearDate( Date d ){
        Calendar c = Calendar.getInstance();
        c.setTime( d );
        c.set( Calendar.MONTH, Calendar.JANUARY );
        c.set( Calendar.DAY_OF_MONTH, 1 );
        c.set( Calendar.YEAR, 1970 );
        return c.getTime();
    }

    // Combine a date and time into a single 
    // Date instance

    public static Date combineDateTime( Date date, 
                                        Date time ){
        Calendar cd = Calendar.getInstance();
        Calendar ct = Calendar.getInstance();

        cd.setTime( date );
        ct.setTime( time );

        ct.set( Calendar.MONTH, 
              cd.get( Calendar.MONTH ) );
        ct.set( Calendar.DAY_OF_MONTH,  
                   cd.get( Calendar.DAY_OF_MONTH ) );
        ct.set( Calendar.YEAR, 
                   cd.get( Calendar.YEAR ) );

        return ct.getTime();
    }
    
    // The list of tests we can perform, arranged 
    // in threes so that ( index % 3 ) == one 
    // of DATE, TIME or DATE_TIME

    static final String[] testLabels = {
        "Current date",
        "Current time",
        "Current date/time",
        "Edit date",
        "Edit time",
        "Edit date/time",
    };

    private TestList testList;
    private Date     editDate;

    //
    // Displays the list of actions
    //

    class TestList extends List
                   implements CommandListener {
        public TestList(){
            super( "DateField Tests", IMPLICIT,
                   testLabels, null );
            addCommand( exitCommand );
            setCommandListener( this );
        }

        public void commandAction( Command c,
                                   Displayable d ){
            if( c == exitCommand ){
                exitMIDlet();
            } else if( c == List.SELECT_COMMAND ){
             
                // Figure out which date to display
                // and what the input mode is

                int     which = getSelectedIndex();
                String  label = getString( which );
                int     mode = ( which % 3 ) + 1;
                boolean save = ( which > 2 );

                display.setCurrent(
                   new Edit( save, label, mode ) );
            }
        }
    }

    //
    // Edit a date, time or date/time, optionally
    // saving the value.
    //

    class Edit extends Form
               implements CommandListener {

        public Edit( boolean save, String label,
                     int mode ){

            super( label );
            this.save = save;

            Date d = editDate;

            if( !save ){
                d = new Date();
            }

            dateField = new DateField( null, mode );
            append( dateField );

            if( d != null ){
                if( mode == DateField.TIME ){
                    d = clearDate( d );
                }

                dateField.setDate( d );
            }

            addCommand( okCommand );

            if( save ){
                addCommand( cancelCommand );
            }

            setCommandListener( this );
        }

        public void commandAction( Command c,
                                   Displayable d ){
            Alert alert = null;
            Date  date = dateField.getDate();

            if( 
              save && date != null && c == okCommand ){
                if( editDate != null ){
                   int mode = dateField.getInputMode();
                    if( mode == DateField.DATE ){
                        editDate = combineDateTime( 
                                      date, editDate );
                    } else if( 
                              mode == DateField.TIME ){
                        editDate = combineDateTime( 
                                      editDate, date );
                    } else {
                        editDate = date;
                    }
                } else {
                    editDate = date;
                }

                Calendar cal = Calendar.getInstance();
                cal.setTime( editDate );

                alert = new Alert( "New date/time" );
                alert.setString( 
                 "The saved date/time is now " + cal );
                alert.setTimeout( Alert.FOREVER );
            }

            if( alert != null ){
                display.setCurrent( alert, testList );
            } else {
                display.setCurrent( testList );
            }
        }

        private DateField dateField;
        private boolean   save;
    }
}

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
DEVELOPING CUSTOM COMPONENTS FOR THE MOBILE INFORMATION DEVICE 
PROFILE

A question that novice Mobile Information Device Profile (MIDP)
programmers often ask is "How can I extend the MIDP user 
interface classes to define my own custom components?" You might 
want to extend the TextField class, for example, to support more 
kinds of constraints or alternate forms of input. Or you might 
want to define a completely new kind of user interface (UI) 
component.

Unlike the Abstract Windowing Toolkit (AWT) in J2SE, however, the
MIDP user interface classes are not written with extensibility
in mind. In particular, the classes that make up the high-level 
user interface API are not extensible. For example, the Item 
class, which serves as the base class for all components that can 
be placed on a Form, does not expose any public or protected 
constructors. More importantly, none of the high-level UI classes 
expose the low-level painting or input events required to write 
custom components. The only class suitable for extension is the 
Canvas class. This class is part of the low-level user interface 
API which does expose the necessary information. Unfortunately, 
you can't use components from both the low-level and high-level 
APIs at the same time. In other words, you can't place a canvas 
on a form, or place a form item on a canvas. While a canvas is 
extensible, you pay for this flexibility with increased 
responsibility. The canvas is responsible for painting the entire 
screen and for responding to all input events, except for the 
selection and triggering of Command objects. After you decide to 
write your own component class, there's no going back -- it's 
really an all-or-none proposition.  

Be sure to explore the alternatives before you decide to write 
your own component class. Do you truly need a new component, or
can you get by with the predefined MIDP classes? The high-level 
API defines a set of abstract user interface components, 
components that are adapted by a device manufacturer to suit the
display and input characteristics of the device. Any components 
you write do not share this adaptability, so careful design
and testing are required for them to work well across all 
devices. The components also increase the size of your application.

There are many possible implementations for custom components, 
but, in general, you need at least two classes: one class to 
define a "container" or "manager" for the components, and another 
to define the base class for those components. Here is an 
implementation of a possible base class:

import javax.microedition.lcdui.*;

// A root class for Canvas-based components.
// Because Area extends Canvas, you can actually
// use a component directly as a Canvas, although
// it's recommended you place it on Manager.

public abstract class Area extends Canvas {
    protected int      x;
    protected int      y;
    protected int      w;
    protected int      h; 
    protected Font     font; 
    protected Manager  parent; 
    protected int      backcolor = -1;
    protected int      forecolor = -1;

    protected Area( int x, int y, int w, int h ){
        this( x, y, w, h, null );
    }

    protected Area( int x, int y,
                    int w, int h, Font f ){
        this.x = x;
        this.y = y;
        this.w = w;
        this.h = h;
        this.font = font;
    }

    // Erase the background using backcolor

    protected void eraseBackground( Graphics g ){
        g.setColor( getBackColor() );

        if( parent == null ){
            g.fillRect( 0, 0, getCanvasWidth(),
                        getCanvasHeight() );
        } else {
            g.fillRect( 0, 0, w, h );
        }
    }

    public final int getBackColor(){
        if( backcolor == -1 ){
            if( parent != null ){
                return parent.getBackColor();
            }

            backcolor = 0xFFFFFF;
        }

        return backcolor;
    }

    protected final int getCanvasHeight(){
        return super.getHeight();
    }

    protected final int getCanvasWidth(){
        return super.getWidth();
    }

    public final int getHeight(){ return h; }
    public final int getWidth(){ return w; }
    public final int getX(){ return x; }
    public final int getY(){ return y; }

    public final Font getFont(){
        if( font == null ){
            if( parent != null ){
                return parent.getFont();
            }

            font = Font.getDefaultFont();
        }

        return font;
    }

    public final int getForeColor(){
        if( forecolor == -1 ){
            if( parent != null ){
                return parent.getForeColor();
            }
            
            forecolor = 0;
        }

        return forecolor;
    }

    public Manager getParent(){ return parent; }

    public void keyPressed( int keyCode ){
    }

    public void keyReleased( int keyCode ){
    }

    public void keyRepeated( int keyCode ){
    }

    protected void moveFocus( boolean forward ){
        if( parent != null ){
            parent.moveFocus( forward );
        }
    }

    // If the area is acting like a Canvas, call
    // the real paint routine

    protected void paint( Graphics g ){
        eraseBackground( g );
        g.setColor( getForeColor() );
        g.setFont( getFont() );
        paintArea( g, true );
    }

    // The manager calls this paint routine on each of 
    // its children

    public final void paint( Graphics g,
                             boolean hasFocus ){
        int  cx = g.getClipX();
        int  cy = g.getClipY();
        int  ch = g.getClipHeight();
        int  cw = g.getClipWidth();
        Font f  = g.getFont();
        int  col = g.getColor();

        eraseBackground( g );

        g.setClip( x, y, w, h );
        g.setFont( getFont() );
        g.setColor( getForeColor() );

        paintArea( g, hasFocus );

        g.setClip( cx, cy, cw, ch );
        g.setFont( f );
        g.setColor( col );
    }

    // Subclass implements to do actual painting

    protected abstract void paintArea( 
                        Graphics g, boolean hasFocus );

    // Repaint the area of the given child

    public void repaintArea( Area child, boolean now ){
        if( parent != null ){
            parent.repaintArea( child, now );
        } else {
            repaint( child.getX(),
                     child.getY(),
                     child.getWidth(),
                     child.getHeight() );
    
            if( now ){
                serviceRepaints();
            }
        }
    }

    public void setBackColor( int col ){
         backcolor = col;
    }

    public void setForeColor( int col ){
        forecolor = col;
    }

    protected void setParent( Manager parent ){
        this.parent = parent;
    }
}

Although the root class is fairly simple, it does handle the 
basics. It stores the position, dimensions, colors, and fonts of 
the component. The Area class is meant to be subclassed. Here's 
an example of a simple push button component that extends Area:

import javax.microedition.lcdui.*;

public class PushButton extends Area {

    // Define the listener interface

    public interface Listener {
        void buttonPressed( PushButton which );
    }

    private String   label;
    private boolean  selected;
    private Listener listener;

    public PushButton( String label,
                       int x, int y ){
        this( label, x, y, 0, 0, null );
    }

    public PushButton( String label,
                       int x, int y,
                       Font f ){
        this( label, x, y, 0, 0, f );
    }  
    public PushButton( String label,
                       int x, int y,
                       int w, int h ){
       this( label, x, y, w, h, null );
    }

    public PushButton( String label,
                       int x, int y,
                       int w, int h,
                       Font f ){
        super( x, y, w, h, f );
        
        if( label == null ) label = "";

        int tw = calcMinWidth( label, getFont() );
        int th = calcMinHeight( label, getFont() );
        
        if( tw > w ) this.w = tw;
        if( th > h ) this.h = th;
        
        this.label = label;
    }

    public static int calcMinWidth( 
                                 String text, Font f ){
        return f.stringWidth( text ) + 8;
    }

    public static int calcMinHeight( 
                                 String text, Font f ){
        return f.getHeight() + 8;
        }

    public String getLabel(){ return label; }

    protected void paintArea( 
                        Graphics g, boolean hasFocus ){
        g.setStrokeStyle( g.SOLID );
        g.drawRect( x, y, w-1, h-1 );
        
        if( selected ){
            g.setColor( getForeColor() );
            g.fillRect( x, y, w-1, h-1 );
            g.setColor( getBackColor() );
        } else if( hasFocus ){
            g.setStrokeStyle( g.DOTTED );
            g.drawRect( x+2, y+2, w-5, h-5 );
            g.setStrokeStyle( g.SOLID );
        }
        
        g.drawString( 
                     label, x+4, y+4, g.TOP | g.LEFT );
        }

    public void keyPressed( int keyCode ){
        int action = getGameAction( keyCode );
        switch( action ){
            case UP:
            case LEFT:
                moveFocus( false );
                break;
            case DOWN:
            case RIGHT:
                moveFocus( true );
                break;
            case FIRE:
                selected = true;
                repaintArea( this, true );
                break;
        }
    }

    public void keyReleased( int keyCode ){
        int action = getGameAction( keyCode );
        switch( action ){
            case FIRE:
                selected = false;
                repaintArea( this, true );
                
                if( listener != null ){
                    listener.buttonPressed( this );
                }
                break;
        }
    }
    
    public void setListener( Listener listener ){
        this.listener = listener;
    }
}

Most of the logic in PushButton has to do with the painting of 
the component. The push button can draw itself in three states: 
unselected with focus, unselected without focus, and selected.  
It responds to key events in order to change its selected state
or move the focus away from it to another component. (The term 
focus, as used here, refers to the concept of input focus, that 
is, the particular component on screen to which keyboard/keypad 
input is directed.)

The only thing missing now is the manager class. Its purpose is 
to track the other components and to pass along paint and input 
events as appropriate.

import java.util.*;
import javax.microedition.lcdui.*;

// A subclass of Area that can act as
// the parent for other components.

public class Manager extends Area {
    protected Vector children = new Vector();
    protected Area   focus = null;

    public Manager(){
        super( 0, 0, 0, 0, null );
        w = getCanvasWidth();
        h = getCanvasHeight();
    }
    
    public void add( Area child ){
        if( !children.contains( child ) ){
            children.addElement( child );
            child.setParent( this );
            repaintArea( child, false );
        }
    }

    protected Area getFocus(){
        if( focus == null && children.size() > 0 ){
            focus = (Area) children.elementAt( 0 );
        }

        return focus;
    }
    
    public void keyPressed( int keyCode ){
        Area focus = getFocus();
        if( focus != null && focus != this ){
            focus.keyPressed( keyCode );
        }
    }
    
    public void keyReleased( int keyCode ){
        Area focus = getFocus();
        if( focus != null && focus != this ){
            focus.keyReleased( keyCode );
        }
    }
    
    public void keyRepeated( int keyCode ){
        Area focus = getFocus();
        if( focus != null && focus != this ){
            focus.keyRepeated( keyCode );
        }
    }
    
    // Called to move the focus to the next
    // or previous component

    protected void moveFocus( boolean forward ){
        Area oldFocus = getFocus();
        if( oldFocus != null ){
            int i = children.indexOf( oldFocus );
            int last = children.size() - 1;
            if( forward ){
                if( ++i > last ) i = 0;
            } else {
                if( --i < 0 ) i = last;
            }
            
            focus = (Area) children.elementAt( i );
            repaintArea( oldFocus, false );
            repaintArea( focus, true );
        }
    }

    public void remove( Area child ){
        if( children.removeElement( child ) ){
            child.setParent( null );
            repaintArea( child, false );
        }
    }

    protected void paint( Graphics g ){
        if( focus == null ) getFocus();

        eraseBackground( g );
        g.setColor( getForeColor() );

        int n = children.size();
        for( int i = n-1; i >= 0; --i ){
             try {
                Area area = 
                    (Area) children.elementAt( i );
                area.paint( g, ( focus == area ) );
            }
            catch( Exception e ){
            }
        }
    }

    protected void paintArea( 
                        Graphics g, boolean hasFocus ){
    }
}

Finally, here's a simple MIDlet that uses the Manager and 
PushButton classes to display three push buttons:

import javax.microedition.lcdui.*;
import javax.microedition.midlet.*;

// A simple example that shows how to use
// custom-drawn components.  Creates a
// Manager that has three PushButtons on it.

public class Tester extends MIDlet
                    implements CommandListener,
                               PushButton.Listener {

    private Display display;
    private Command exitCommand =
                         new Command( "Exit",
                                     Command.EXIT, 1 ); 

    public Tester(){
    }

    protected void destroyApp( boolean unconditional )
                    throws MIDletStateChangeException {
        exitMIDlet();
    }

    protected void pauseApp(){
    }

    protected void startApp()
                    throws MIDletStateChangeException {
        if( display == null ){ // first time called...
            initMIDlet();
        }
    }

    private void initMIDlet(){
        display = Display.getDisplay( this );

        Manager m = new Manager();

        PushButton pb = new PushButton( 
                                     "First", 0, 0 );
        pb.setListener( this );
        m.add( pb );

        pb = new PushButton( "Second", 20, 20 );
        pb.setListener( this );
        m.add( pb );

        pb = new PushButton( "Third", 0, 60, 50, 0 );
        pb.setListener( this );
        m.add( pb );

        m.addCommand( exitCommand );
        m.setCommandListener( this );

        display.setCurrent( m );
    }

    public void exitMIDlet(){
        notifyDestroyed();
    }

    public void commandAction( Command c,
                               Displayable d ){
        exitMIDlet();
    }
    
    public void buttonPressed( PushButton which ){
        String label = which.getLabel();

        Alert a = new Alert( "Pressed!",
            "You pressed the " + label + " button.",
            null, null );
            
        display.setCurrent( a, which.getParent() );
    }
}

If you're familiar with AWT or Swing programming in J2SE, the 
code in the initMIDlet method should look very familiar. Run this 
sample and you'll see three push buttons on screen, two of which 
are overlapping. Use the UP, DOWN, LEFT and RIGHT keys to move 
the input focus from one button to another, and press and release
the FIRE key to display an alert.

Use the code above as the basis for your own custom component 
coding. There are many improvements you can make. For example, 
you could add support for pointer events. Or you can optimize the 
painting and support the hiding of components. Start with this 
small bit of code and only add the functionality that you need.

.  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .

IMPORTANT: Please read our Terms of Use and Privacy policies:
http://www.sun.com/share/text/termsofuse.html
http://www.sun.com/privacy/ 

* FEEDBACK
  Comments? Send your feedback on the J2ME Tech Tips to: 
  
  jdc-webmaster@sun.com

* SUBSCRIBE/UNSUBSCRIBE
  - To subscribe, go to the subscriptions page,
    (http://developer.java.sun.com/subscription/), choose
    the newsletters you want to subscribe to and click "Update".
  - To unsubscribe, go to the subscriptions page,
    (http://developer.java.sun.com/subscription/), uncheck the
    appropriate checkbox, and click "Update".
  - To use our one-click unsubscribe facility, see the link at 
    the end of this email:
    
- ARCHIVES
You'll find the J2ME Tech Tips archives at:

http://java.sun.com/jdc/J2METechTips/index.html


- COPYRIGHT
Copyright 2001 Sun Microsystems, Inc. All rights reserved.
901 San Antonio Road, Palo Alto, California 94303 USA.

This document is protected by copyright. For more information, see:

http://java.sun.com/jdc/copyright.html

J2ME Tech Tips 
November 14, 2001

Sun, Sun Microsystems, Java, Java Developer Connection, J2ME, and
J2SE are trademarks or registered trademarks of Sun Microsystems, 
Inc. in the United States and other countries.




