
 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 May 29, 2001. This issue covers:

         * The Connected Device Configuration and the Foundation 
           Profile
         * Handling Multiple Simultaneous MIDP Alerts
         
The J2ME Tech Tips are written by Eric Giguere
(http://www.ericgiguere.com), an engineer at iAnywhere 
Solutions, inc., and author of the book "Java 2 Micro
Edition: Professional Developer's Guide."

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

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
THE CONNECTED DEVICE CONFIGURATION AND THE FOUNDATION PROFILE 

Much of what's been written about Java 2 Platform, Micro Edition 
(J2ME) talks about the Connected Limited Device Configuration, or
CLDC for short. The CLDC defines an extremely small subset of 
the Java Platform for very limited devices -- devices, like 
cellular telephones, that do not have enough memory or enough 
processing power to handle a full-blown Java implementation. 
There is, however, another configuration available that is midway 
between the CLDC and a full Java 2 Platform, Standard Edition 
(J2SE(tm)) implementation: the Connected Device Profile, or CDC 
for short.

The CDC defines a much less radical subset of J2SE. Unlike
the CLDC, which is aimed at devices with less (often much
less) than 512K bytes of total available memory, the CDC
requires devices to have at least 2 megabytes of total memory
available for Java. This can be a combination of read-only
and read-write memory. The Java virtual machine* (VM) and core
class libraries are likely to be stored in read-only or
flash memory. 

The CDC really targets the next generation of wireless, handheld 
consumer devices. These devices pack large amounts of memory and 
faster processors into the same or smaller form factors than 
today's most prevalent devices. The CDC can also be used for 
larger consumer devices such as set-top boxes that deliver 
content through televisions, that is, devices that don't need 
a general purpose Java implementation such as J2SE.

The CDC is also a superset of the CLDC. All the classes of the 
CLDC, including the new javax.microedition.io classes defined by 
the Generic Connection Framework, are part of the CDC. This means 
that CLDC-based applications can run unchanged in a CDC-based 
environment. That's true provided that any required CLDC-based 
profiles the applications use (such as the Mobile Information 
Device Profile) are available on the device.

Perhaps the biggest difference between the CDC and the CLDC is 
that the CDC includes the CVM virtual machine, a full 
J2SE-compliant virtual machine. The CLDC made changes to the
virtual machine specification. It disallowed all floating-point 
operations, changed the way classes are verified, and did not 
support low-level standards such as the Java Native Interface 
(JNI) or the Java Virtual Machine Debug Interface (JVMDI). By 
comparison, all of the features of a full J2SE virtual machine 
must be supported by the CVM virtual machine. The J2SE VM itself 
is not a requirement of the CDC --- all the CDC requires is a VM 
that implements the full set of VM features. Vendors can license 
it and adapt it for their devices quickly. The CVM virtual 
machine can even be used with real-time operating systems.

The CDC includes classes from the java.lang, java.util, java.net, 
java.io, java.text and java.security packages. Not every class 
from each package is supported. However those classes that are
supported behave identically to and have the same public 
interfaces as those in J2SE; the one exception is that most 
deprecated methods have been removed. No changes are usually 
required to port existing J2SE code to the CDC.

Just as there are profiles for the CLDC, there are also profiles
being developed for the CDC. The first profile, the Foundation 
Profile, was released at the same time as the CDC. Generally, the 
two are meant to go hand-in-hand. The Foundation Profile extends 
the CDC by adding most of the missing J2SE core libraries, except 
for those related to creating user interfaces. These additions 
include socket classes, the complete internationalization and 
localization classes, and the full set of classes in the 
java.lang and java.io packages. There are still gaps, for 
example, the java.beans, java.rmi and java.sql packages are not 
part of the CDC or the Foundation Profile. 

As you might guess, the Foundation Profile is meant to serve as 
a foundation for other profiles. The PersonalJava(tm) API is 
being redefined as a Personal Profile that extends the Foundation 
Profile. An RMI Profile, in the final stages of development, adds 
RMI support to the Foundation Profile. Other profiles are sure to 
be developed.

Reference implementations of the CDC and the Foundation Profile 
are available for Linux and VxWorks platforms under the terms and 
conditions of the Sun Community Source License (SCSL) agreement.  
You can download them by following the links from the main CDC 
page at http://java.sun.com/products/cdc.

Here's a CDC-Foundation Profile application that illustrates some 
of the things described in this tip. In particular, the application 
uses CDC java.net classes to create device connections (Socket and 
ServerSocket). The application is comprised of two programs. 
One program, PeerFinder, acts as a client; the other program, 
PeerListener, acts as a server.

Here's the source code for PeerFinder:

import java.io.*;
import java.net.Socket;
import java.net.InetAddress;
import java.util.Vector;

public class PeerFinder {

    static final int    PORT_NUM = 4321;
    static final int    DATA_BUFFER_MAX = 256;

    static final String PING_MSG = new String("ping");
    static final String PONG_MSG = new String("pong");

    static String       command = System.getProperty("command");
    static String       portprop = System.getProperty("port");
    static String       hostname = System.getProperty("hostname");

    static Socket       socket;
    static int          port = PORT_NUM;

    static BufferedOutputStream outputStream;
    static BufferedInputStream  inputStream;


    public static void main(String args[]) {

        byte[]  dataBuffer = new byte[DATA_BUFFER_MAX];
        String  dataString = null;

        // Set default port if not set by user
        if ((portprop != null) && !portprop.equals("")) {
            port = Integer.parseInt(portprop);
        }

        // Set default command if not set by user
        if ((command == null) || (command.equals(""))) {
            command = PING_MSG;
        }

        // Set default security manager
        try {
            if (System.getSecurityManager()==null) {
                System.setSecurityManager(new SecurityManager());
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

        // Open a socket to the given peer listener host port
       try {

            if ((hostname != null) && hostname.equals("")) {
                System.out.println("Opening to local 
                                           host port # "+port);
                socket = new Socket(
                             InetAddress.getLocalHost(), port);
            } else {
                System.out.println("Opening peer listener host "+
                                     hostname+
                                   " port # "+port);
                socket = new Socket(hostname, port);
            }

        } catch (Exception e) {
            e.printStackTrace();
        }

        try {

            // Get the input and output streams of socket to peer
            outputStream = new BufferedOutputStream(
                                        socket.getOutputStream());
            inputStream = new BufferedInputStream(
                                        socket.getInputStream() );

            // Check to send a ping
            if (command.equals(PING_MSG)) {
               sendPing();
            }

            // Listen for peer listener reply
            System.out.println("Listening for return message...");

            // Reading from peer listener
            while (inputStream.read(dataBuffer, 0, DATA_BUFFER_MAX)
                   != -1) {
                dataString = new String(dataBuffer);

            // Print received message
                System.out.println("Received peer listener reply: 
                                   "+  dataString);

            // Check if pong was received
                if (dataString.indexOf(PONG_MSG) != -1) {
                    break;
                }
            }

            // Cleanup and close
            inputStream.close();
            outputStream.close();
            socket.close();

        } catch (IOException ioe) {
            ioe.printStackTrace();
        }

    }

    static void sendPing() {
        byte[] dataBuffer = PING_MSG.getBytes();

       try {
            outputStream.write(dataBuffer, 0, dataBuffer.length);
            outputStream.flush();
            System.out.println("Sent: "+PING_MSG);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

Here's the source code for PeerListener:

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;

public class PeerListener {

    static final int    PORT_NUM = 4321;
    static final int    DATA_BUFFER_MAX = 256;

    static final String PING_MSG = new String("ping");
    static final String PONG_MSG = new String("pong");

    static String       command = System.getProperty("command");
    static String       portprop = System.getProperty("port");

    static ServerSocket peerSocket;
    static Socket       socket;
    static int          port = PORT_NUM;

    static BufferedOutputStream outputStream;
    static BufferedInputStream  inputStream;

    public static void main(String args[]) {

        byte[]  dataBuffer = new byte[DATA_BUFFER_MAX];
        String  dataString = null;

        // Set default property if not set by user
        if ((portprop != null) && !portprop.equals("")) {
            port = Integer.parseInt(portprop);
        }

        // Set default security manager
        try {
            if (System.getSecurityManager()==null) {
                System.setSecurityManager(new SecurityManager());
            }
        } catch (Exception e) {
           e.printStackTrace();
        }

        // Open a peer socket to the given port
        try {
            peerSocket = new ServerSocket(port);
        } catch (Exception e) {
           e.printStackTrace();
        }

        // Start listening for peers
        System.out.println("Listening for peer finder 
                                                connection...");

        while (true) {

        try {

                // Block here for peer finder requests to connect
                socket = peerSocket.accept();

                // Get the input and output streams from peer socket
                outputStream = new 
                    BufferedOutputStream(socket.getOutputStream());
                inputStream = new 
                    BufferedInputStream(socket.getInputStream() );

                // Read loop
                while (inputStream.read(dataBuffer, 0, 
                                                  DATA_BUFFER_MAX-1)
                       != -1) {
                    dataString = new String(dataBuffer);
                    System.out.println("Received: "+ dataString+"");

                    // Check if this is a ping request message
                    if ((dataString.indexOf(PING_MSG)) != -1) {
                        // Send pong message to the peer
                           sendPong();
                    } else {
                        // Don't know what type of message this was
                        System.out.println("     Unknown message");
                    }
                }

                inputStream.close();
                outputStream.close();
                socket.close();

            } catch (IOException ioe) {
                ioe.printStackTrace();
                break;
            }
        }

    }

    static void sendPong() {
        byte[]  dataBuffer = PONG_MSG.getBytes();

        try {
            outputStream.write(dataBuffer, 0, dataBuffer.length);
            outputStream.flush();
            System.out.println("Sent: "+ PONG_MSG);
        } catch (Exception e) {
          e.printStackTrace();
        }
    }

}

You will also need a policy file and a security properties file 
to run the application. Use the following policy file 
(java.policy):

grant {
        permission java.net.SocketPermission "*:1024-65535", 
                "connect,accept,listen,resolve";
};

// Standard extensions get all permissions by default

grant codeBase "file:${java.home}/lib/ext/*" {
        permission java.security.AllPermission;
};

// default permissions granted to all domains

grant { 
        // Allows any thread to stop itself using the 
        // java.lang.Thread.stop() method that takes no argument.
        // Note that this permission is granted by default only 
        // to remain backwards compatible.
        // It is strongly recommended that you either remove this 
        // permission from this policy file or further restrict 
        // it to code sources that you specify, because 
        // Thread.stop() is potentially unsafe.
        // See "http://java.sun.com/notes" for more information.
        permission java.lang.RuntimePermission "stopThread";

        // allows anyone to listen on un-privileged ports
        permission java.net.SocketPermission "localhost:1024-", 
                                                        "listen";

        // "standard" properies that can be read by anyone

        permission java.util.PropertyPermission "java.version", 
                                                          "read";
        permission java.util.PropertyPermission "java.vendor", 
                                                          "read";
        permission java.util.PropertyPermission 
                                       "java.vendor.url", "read";
        permission java.util.PropertyPermission 
                                    "java.class.version", "read";
        permission java.util.PropertyPermission 
                                               "os.name", "read";
        permission java.util.PropertyPermission 
                                            "os.version", "read";
        permission java.util.PropertyPermission "os.arch", 
                                                          "read";
        permission java.util.PropertyPermission "file.separator", 
                                                          "read";
        permission java.util.PropertyPermission "path.separator", 
                                                          "read";
        permission java.util.PropertyPermission "line.separator", 
                                                          "read";

        permission java.util.PropertyPermission 
                            "java.specification.version", "read";
        permission java.util.PropertyPermission 
                             "java.specification.vendor", "read";
        permission java.util.PropertyPermission 
                               "java.specification.name", "read";

        permission java.util.PropertyPermission 
                         "java.vm.specification.version", "read";
        permission java.util.PropertyPermission 
                          "java.vm.specification.vendor", "read";
        permission java.util.PropertyPermission 
                            "java.vm.specification.name", "read";
        permission java.util.PropertyPermission 
                                       "java.vm.version", "read";
        permission java.util.PropertyPermission "
                                         java.vm.vendor", "read";
        permission java.util.PropertyPermission "java.vm.name", 
                                                          "read";
};


Use the following master security properties file 
(java.security):


#
# This is the "master security properties file".
#
# In this file, various security properties are set for use by
# java.security classes. This is where users can statically 
# register Cryptography Package Providers ("providers" for short). 
# The term "provider" refers to a package or set of packages that 
# supply a concrete implementation of a subset of the cryptography 
# aspects of the Java Security API. A provider may, for example, 
# implement one or more digital signature algorithms or message 
# digest algorithms.
#
# Each provider must implement a subclass of the Provider class.
# To register a provider in this master security properties file, 
# specify the Provider subclass name and priority in the format
#
#    security.provider.<n>=<className> 
#
# This declares a provider, and specifies its preference 
# order n. The preference order is the order in which providers are 
# searched for requested algorithms (when no specific provider is 
# requested). The order is 1-based; 1 is the most preferred, 
# followed by 2, and so on.
#
# <className> must specify the subclass of the Provider class whose 
# constructor sets the values of various properties that are 
# required for the Java Security API to look up the algorithms or 
# other facilities implemented by the provider.
# 
# There must be at least one provider specification in 
# java.security. There is a default provider that comes standard 
# with the JDK. It is called the "SUN" provider, and its Provider 
# subclass named Sun appears in the sun.security.provider package. 
# Thus, the "SUN" provider is registered via the following:
#
#    security.provider.1=sun.security.provider.Sun 
#
# (The number 1 is used for the default provider.) 
#
# Note: Statically registered Provider subclasses are 
# instantiated when the system is initialized. Providers can be 
# dynamically registered instead by calls to either the addProvider 
# or insertProviderAt method in the Security class.

#
# List of providers and their preference orders (see above):
#
security.provider.1=sun.security.provider.Sun
security.provider.2=com.sun.rsajca.Provider

#
# Class to instantiate as the system Policy. This is the name of 
# the class that will be used as the Policy object.
#
policy.provider=sun.security.provider.PolicyFile

# The default is to have a single system-wide policy file, 
# and a policy file in the user's home directory.
policy.url.1=file:${java.home}/lib/security/java.policy
policy.url.2=file:${user.home}/.java.policy

# whether or not we expand properties in the policy file
# if this is set to false, properties (${...}) will not be expanded 
# in policy files.
policy.expandProperties=true

# whether or not we allow an extra policy to be passed on the 
# command line with -Djava.security.policy=somefile. Comment out 
# this line to disable this feature.
policy.allowSystemProperty=true

# whether or not we look into the IdentityScope for trusted 
# Identities when encountering a 1.1 signed JAR file. If the 
# identity is found and is trusted, we grant it AllPermission.
policy.ignoreIdentityScope=false

#
# Default keystore type.
#
keystore.type=jks

#
# Class to instantiate as the system scope:
#
system.scope=sun.security.provider.IdentityDatabase

#
# List of comma-separated packages that start with or equal this 
# string will cause a security exception to be thrown when
# passed to checkPackageAccess unless the corresponding 
# RuntimePermission ("accessClassInPackage."+package) has
# been granted.
package.access=sun.

#
# List of comma-separated packages that start with or equal this 
# string will cause a security exception to be thrown when
# passed to checkPackageDefinition unless the corresponding
# RuntimePermission ("defineClassInPackage."+package) has
# been granted.
# 
# by default, no packages are restricted for definition, and none 
# of the class loaders supplied with the JDK call 
# checkPackageDefinition.
#
#package.definition=

 
To build the application:

1. Download Foundation Profile version 1.0 from: 
http://www.sun.com/software/communitysource/j2me/cdc/download.html
   (The CDC is bundled with the Foundation Profile in the 
   download.)

2. Compile the Foundation Profile. Follow the instructions in the
   download. (This builds the CDC as well as the Foundation 
   Profile.)
   
3. Make a security subdirectory in the build/<platform>/lib 
   directory. For example:
   
   mkdir build/linux/lib/security
    
4. Copy java.policy and java.security to the security 
   subdirectory. For example:

   cp java.policy build/linux/lib/security
   cp java.security build/linux/lib/security   
   
5. Compile the application source code using Java 2 SDK version 
   1.3. Run javac as usual, but use the -bootclasspath option 
   instead of the -classpath option to include the Foundation 
   Profile classes. The -bootclasspath option excludes the 
   standard J2SE classes from the compilation, which makes it 
   possible to detect your application's use of classes and 
   methods not defined in the CDC or the Foundation Profile.
   
   cd <your_test_dir>
   javac -bootclasspath \ 
   <cdcfoundation_dir>/build/linux/lib/foundation.jar:\
        <cdcfoundation_dir>/build/linux/btclasses.zip \
        PeerListener.java \
        PeerFinder.java

6. Run PeerListener:

   cd <cdcfoundation_dir>/build/linux/bin
   cvm -Djava.class.path=<your_test_dir> PeerListener
  
   You'll see the following line displayed in the window: 
   
     Listening for peer finder connection... 
   
In another window, run PeerFinder:

   cd <cdcfoundation_dir>/build/linux/bin
   cvm -Djava.class.path=<your_test_dir> PeerFinder
 
   You'll see the following lines displayed in the window: 
   
     Sent: ping
     Listening for return message...
     Received peer listener reply: pong   

Some things to note about this example:

o It uses J2SE-style APIs (java.net.Socket and 
  java.net.ServerSocket). Unlike the case for CLDC, there is no 
  need to learn the Generic Connection Framework API.  
o It uses buffered input and output streams. This makes for 
  faster I/O.
o It complies with J2SE-style security (including use of a policy 
  file and the default Security Manager).
o It can be compiled and run on both J2SE and CDC-Foundation 
  Profile. 

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
HANDLING MULTIPLE SIMULTANEOUS MIDP ALERTS

The April 16, 2001 issue of the J2ME Tech Tips introduced you to 
timers and alerts. If you write a multithreaded MIDP application 
-- whether you spawn the threads yourself or use timers to 
schedule background tasks -- it's easy to trigger two or more 
alerts simultaneously. In other words, while one alert is being 
displayed, a second alert is triggered. Dealing with this 
situation is not as simple as you might think. Alerts are 
displayed using the two-argument form of the Display object's 
setCurrent method, as in the following:

Display     display = ...;
Alert       alert = ...;
Displayable next = ...;

display.setCurrent( alert, next );

The first argument is the alert to display and the second is the 
screen object to display after the alert is dismissed or times 
out. The second argument is referred to as the "alert 
destination."

The problem with calling setCurrent as soon as the second alert 
triggers, is that the new alert immediately replaces the previous 
alert. Sometimes this is desirable, but in most cases, you want 
the user to read each alert in the order that they occurred. An 
alternative approach is to try chaining the alerts, for example:

Display     display = ...;
Alert       alert = ...;
Displayable next = ...;
Displayable current = display.getCurrent();

if( current instanceof Alert ){
    display.setCurrent( (Alert)current, alert );
} else {
    display.setCurrent( alert, next );
}

This doesn't work, however, because only one alert can be active 
at any time. The final destination, that is, the destination of 
the last alert, is also lost with this scheme.

What you really need is a way to keep a list of pending alerts 
and display each alert in turn. This would be simple to do if the 
alert had a dismissal event you could trap. Unfortunately, 
there's no direct way to know when an alert is dismissed.

There is an indirect route you can take, however. A Canvas object 
calls its showNotify and hideNotify methods whenever it is shown 
or hidden. These calls are normally used to start and stop 
screen-related operations that only make sense when the canvas
is visible. For example:

public class MyCanvas extends Canvas {
    protected void paint( Graphics g ){
        // do the painting here
    }

    protected void showNotify(){
        // canvas is being displayed, start
        // any timers, etc.
    }

    protected void hideNotify(){
        // canvas is being hidden, stop any
        // timers, etc.
    }
}

You can take advantage of this capability to solve the alert 
dilemma. Whenever you display an alert, make a Canvas object the 
alert destination. When the alert times out or is dismissed, the 
canvas is displayed. You then place logic in the canvas's 
showNotify method to display either a pending alert or another 
screen. The canvas doesn't even have to paint itself because it 
will only be briefly displayed.

Armed with the overall design, you can now build the Canvas 
subclass; let's call it AlertRouter. Here is the source code for 
AlertRouter:

package com.j2medeveloper.util;

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

public class AlertRouter extends Canvas {

    private Display     display;
    private Vector      pending = new Vector();
    private Displayable destination;
    private Alert       current;

    public AlertRouter( Display display ){
        this.display = display;
    }

    protected void paint( Graphics g ){
       // no painting
    }

    protected synchronized void showNotify() {
        if( pending.size() > 0 ){
            current = (Alert) pending.elementAt( 0 );
            pending.removeElementAt( 0 );
            display.setCurrent( current, this );
        } else {
            current = null;
            display.setCurrent( destination );
        }
    }

    public void showAlert( Alert alert ) {
        showAlert( alert, null );
    }

    public synchronized void showAlert( Alert alert, 
                                              Displayable next ){
        if( next != null ){
            destination = next;
        } else if( destination == null ){
            destination = display.getCurrent();
            if( destination == null ){
                destination = this;
            }
        }

        pending.addElement( alert );

        if( current == null ){
            display.setCurrent( this );
        }
    }
}

To display alerts, create an instance of AlertRouter when the 
MIDlet starts. You'll need to pass the MIDlet's Display 
object to the AlertRouter instance. Then whenever you need to 
display an alert, call the alert router's showAlert method. 
Here's a simple example:

import com.j2medeveloper.util.*;
import javax.microedition.midlet.*;
import javax.microedition.lcdui.*;

public class MyMIDlet extends MIDlet {

    private Display     display;
    private AlertRouter router;

    public MyMIDlet(){
        display = Display.getDisplay( this );
        router = new AlertRouter( display );
    }

    protected void startApp(){
        Alert alert = new Alert( "Started", "App started", null, 
                                                          null );
        router.showAlert( alert );
    }

    // other methods omitted....
}

Whenever showAlert is called, the given alert is added to a list 
of pending alerts. The first alert in the list is displayed if no 
alert is showing. Otherwise, each alert waits its turn. The final 
alert destination is the screen that was active just before the 
first alert in the alert chain was shown. A two-argument version
of showAlert lets you override that value with your own 
destination. The most recent call to showAlert determines the 
ultimate destination of the alert chain, which is normally what 
you want.

Here's a more complicated MIDlet that tests the alert router by 
using several timers to create alerts at different time 
intervals:

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

public class MultiAlert extends MIDlet {

    Display      display;
    Command      exitCommand = new Command( "Exit", Command.EXIT,
                                                             1 );
    Timer        timer1 = new Timer();
    Timer        timer2 = new Timer();
    Timer        timer3 = new Timer();
    MainForm     form = new MainForm();
    AlertRouter  router;

    public MultiAlert() {
        display = Display.getDisplay( this );
        router = new AlertRouter( display );

        timer1.schedule( new AlertTrigger( "Alert 1", 
                             "This is alert #1" ), 5000, 10000 );
        timer2.schedule( new AlertTrigger( "Alert 2", 
                              "This is alert #2" ), 5000, 7000 );
        timer3.schedule( new AlertTrigger( "Alert 3", 
                              "This is alert #3" ), 5000, 9000 );
    }

    protected void destroyApp( boolean unconditional ) {
        timer1.cancel();
        timer2.cancel();
        timer3.cancel();
    }

    protected void startApp() {
        display.setCurrent( form );
    }

    protected void pauseApp() {
    }

    public void exit(){
        destroyApp( true );
        notifyDestroyed();
    }
 
    class AlertTrigger extends TimerTask {

        public AlertTrigger( String title, String message )
        {
            this.title = title;
            this.message = message;
        }

          public void run(){
            Alert alert = new Alert( title, message, null, null );
            alert.setTimeout( Alert.FOREVER );
            router.showAlert( alert );
       }

        private String title;
        private String message;
    }

    class MainForm extends Form implements CommandListener {
        public MainForm(){
            super( "MultiAlert Demo" );
            addCommand( exitCommand );
            setCommandListener( this );
        }

        public void commandAction( Command c, Displayable d ){
            exit();
        }
    }
}

Run this MIDlet and wait a few seconds for the first alert to 
appear, then wait a few more seconds before dismissing it.  
You'll see another alert appear immediately after the first.  
Dismiss this second alert and any other alerts. Eventually, 
you will be brought back to the main screen, where the whole 
process starts again.

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

- NOTE

Sun respects your online time and privacy. The Java Developer 
Connection mailing lists are used for internal Sun 
Microsystems(tm) purposes only. You have received this email 
because you elected to subscribe. To unsubscribe, go to the 
Subscriptions page (http://developer.java.sun.com/subscription/), 
uncheck the appropriate checkbox, and click the Update button.

As of May  22, 2001, Sun Microsystems updated its Privacy Policy 
(http://sun.com/privacy) to give you a better understanding of 
Sun's Privacy Policy and Practice. If you have any questions, 
contact privacy@sun.com.

- SUBSCRIBE

To subscribe to a JDC newsletter mailing list, go to the 
Subscriptions page (http://developer.java.sun.com/subscription/), 
choose the newsletters you want to subscribe to, and click Update.


- FEEDBACK
Comments? Send your feedback on the J2ME Tech Tips to:

jdc-webmaster@sun.com


- 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


- LINKS TO NON-SUN SITES
The J2ME Tech Tips may provide, or third parties may provide, 
links to other Internet sites or resources. Because Sun has no 
control over such sites and resources, You acknowledge and 
agree that Sun is not responsible for the availability of such 
external sites or resources, and does not endorse and is not 
responsible or liable for any Content, advertising, products, 
or other materials on or available from such sites or resources. 
Sun will not be responsible or liable, directly or indirectly, 
for any damage or loss caused or alleged to be caused by or in 
connection with use of or reliance on any such Content, goods or 
services available on or through any such site or resource.

J2ME Tech Tips 
May 29, 2001

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

* As used in this document, the terms "Java virtual machine" 
  or "JVM" mean a virtual machine for the Java platform.




