Sun Java Solaris Communities My SDN Account Join SDN
 
Wireless Tech Tips

J2ME Tech Tips: August 20, 2001

 

Tech Tips archive

August 20, 2001

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

The J2ME Tech Tips are written by Eric Giguere, 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.

Pixel

BUILDING SPLASH SCREENS FOR MIDLETS

A splash screen is an informational screen that presents the name and version of an application, along with any necessary legal information such as the copyright and trademark notices. It's something you might want your MIDlets to display on startup. Adding a splash screen to a MIDlet is easy to do, but there are a few things to watch out for.

The simplest way to build a splash screen is to use an Alert. Alerts, which were discussed in detail in the April 16, 2001 J2ME Tech Tip titled "An Introduction to the High-Level User Interface API: Alerts and Tickers," are part of the Mobile Information Device Profile (MIDP) high-level user interface API. An alert displays a message and an optional image for a set period of time or until the user explicitly dismisses the alert. So building a splash screen is simply a matter of building and displaying an alert, as done by the following method:

public void showSplashScreen( 
                     Display d, Displayable next ){
    Image logo = null;
    
    try {
      logo = Image.createImage(
                              "/images/logo.png" );
    }
    catch( IOException e ){
    }
    
    Alert a = new Alert( "Time Tracker", 
           "Copyright 2001 by Nobody, Inc.",
            logo, null ); 
    a.setTimeout( Alert.FOREVER );
    display.setCurrent( a, next );
}

An alert might not be flexible enough for your needs, however. To dismiss the splash screen with any key or to display a simple animation, you'll need to use a Canvas for your splash screen. For example, you could define a simple splash screen like this:

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

public class SplashScreen extends Canvas {
    private Display     display;
    private Displayable next;
    private Timer       timer = new Timer();

    public SplashScreen( 
               Display display, Displayable next ){
        this.display = display;
        this.next    = next;

        display.setCurrent( this );
    }

    protected void keyPressed( int keyCode ){
        dismiss();
    }

    protected void paint( Graphics g ){
        // do your drawing here
    }

    protected void pointerPressed( int x, int y ){
        dismiss();
    }

    protected void showNotify(){
        timer.schedule( new CountDown(), 5000 );
    }

    private void dismiss(){
        timer.cancel();
        display.setCurrent( next );
    }

    private class CountDown extends TimerTask {
        public void run(){
            dismiss();
        }
    }
}

To display this splash screen, simply create an instance of it. When you create the instance, you pass to it the MIDlet's Display object; you also pass to it the screen to activate after the splash screen is dismissed:

public void showSplashScreen( 
               Display display, Displayable next ){
    new SplashScreen( display, next );
}

The splash screen is dismissed when the user presses any key or presses on the screen (if the device supports a pointing device). If neither event occurs, the screen is automatically dismissed five seconds after it is first displayed.

When should you display a splash screen? Your first thought might be to make its activation the last line of the MIDlet's startApp method. Remember, though, that the startApp method can be called more than once for the same MIDlet instance. The constructor is not a good place to activate a splash screen because the MIDP specification does not guarantee that the MIDlet's Display object (required to display any kind of screen or alert) is initialized yet. So the startApp method is indeed the place to call a splash screen, but only once. The startApp method is also where you can safely obtain the MIDlet's Display object, so combine the two tasks as in the following MIDlet:

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

public class MyMIDlet extends MIDlet
                      implements CommandListener {

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

    public MyMIDlet(){
    }

    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 );
        new SplashScreen(
                       display, new TrivialForm() );
    }

    public void exitMIDlet(){
        notifyDestroyed();
    }

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

    // A trivial UI screen

    class TrivialForm extends Form {
        TrivialForm(){
            super( "MyMIDlet" );
            addCommand( exitCommand );
            setCommandListener( MyMIDlet.this );
        }
    }
}

It isn't usually necessary to display a splash screen each time a MIDlet is invoked. Ideally, you should display a splash screen the first time the user runs the MIDlet, but not again. To do this, you need to use the Record Management System (RMS) classes to store an indication of whether the application has been run. (The basics of the RMS were covered in the February 20, 2001 J2ME Tech Tip "Record Management System Basics." This approach could be as simple as testing for the existence of a particular record store (which could even be shared among all the MIDlets in the same MIDlet suite). You display the splash screen only if the record store doesn't exist. After you display the splash screen, you create an empty record store to indicate that the splash screen has been shown.

Pixel

CLIENT-SERVER COMMUNICATION OVER HTTP USING MIDP AND SERVLETS

Many, if not most, Mobile Information Device Profile (MIDP) applications have a server-side piece to them. In other words, a client-server paradigm is used to offload complicated work from the limited capabilities of the MIDP device to the more capable server environment. Because the MIDP 1.0 specification only mandates support for HTTP, all client-server communication must be done through an HTTP gateway, of which a web server is one example (however vendors are free to add support for different communication protocols). This Tech Tip looks at how MIDlets can communicate with servlets or JavaServer Pages (JSP) running on a web server. The same techniques can also be used to communicate with server applications written in other languages such as Perl, but using the Java programming language on both ends of the communication makes coding simpler.

The first step is to understand how MIDlets can use HTTP to talk to a web server using the HttpConnection class. This was covered in the December 18, 2000 J2ME Tech Tip, "Making HTTP Connections with MIDP". If you read the Tech Tip, pay close attention to the HttpConnectionHelper class developed in the tip.

The second step is to configure your web server to support servlets, JSPs, or both. This is beyond the scope of this Tech Tip, but there are several web servers or web server extensions, both free and commercial, that you can use. For example, you can use the Tomcat servlet engine from the Apache Software Foundation. Tomcat is a free, open-source web server that is downloadable from the Jakarta Project binary download site. A good place to start looking for web servers is the Java Servlet Technology page and the JavaServer Pages Technology page. Note that if you run your MIDlets in an emulator, a local web server with servlet or JSP support is all you need. If you want to run MIDlets on real devices, however, the web server must be accessible from the Internet, which might require configuration help from your IT department.

After you complete these basic steps, you're ready to try your first client-server communication. Use the HTTP POST method in place of the GET method to transfer arbitrary data from the client to the server. With GET, the only way to pass data is using encoded text as part of the request URL. POST is more flexible because it allows you to specify a specific format for the data (including binary) and it doesn't have the length restrictions of the GET method.

To make a POST request using the MIDP's HttpConnection class, you first obtain the HttpConnection object and then call its setRequestMethod method:

import java.io.*;
import javax.microedition.io.*;

try {
    String url = ....; // a URL
    HttpConnection conn = (HttpConnection) 
                           Connector.open( url );
    conn.setRequestMethod( HttpConnection.POST );
    ..... // other code here
}
catch( IOException ioe ){
    // handle I/O errors
} 

Remember to call setRequestMethod before attempting to obtain any input or output streams, otherwise the call will have no effect.

After setting the request method, you should also set any appropriate HTTP headers. The MIDP Specification requires that you set the User-Agent and Content-Language headers:

conn.setRequestProperty( "User-Agent", 
       "Profile/MIDP-1.0 Configuration/CLDC-1.0" );
conn.setRequestProperty(
                     "Content-Language", "en-US" );

You should also consider setting the Accept and Connection headers:

conn.setRequestProperty( "Accept", 
                      "application/octet-stream" );
conn.setRequestProperty( "Connection", 
                             "close" ); // optional

The Accept header tells the web server what kind of data the client will accept. Use the MIME type that best describes the data you expect to receive.

The Connection header controls the "keep alive" feature of an HTTP connection; this feature lets the client and the server reuse the same connection for multiple requests. If you plan to make many requests to the same server over a short period of time, you might get better performance by not setting the Connection header. If you don't set the Connection header, the state defaults to keep alive. Otherwise, you should set the Connection header value to "close" (as above) to ensure that the connection is terminated after the server sends its response.

The Connection header is particularly important because of its close association with the Content-Length header. The Content-Length header lists the size (in bytes) of the request or response body. A valid Content-Length header is necessary for keep alive to work -- it's a requirement of the HTTP 1.1 protocol. If the Content-Length header is missing from the request, the server won't know how many bytes of data to read, and the client will have a similar problem if it's missing from the response. An incorrect value can also cause problems. This is why during development it's often a good idea to turn off keep alive; this avoids having either end of the conversation block, waiting for non-existent data to be received. You can turn keep alive back on once you're sure the data is being sent and received correctly.

Now you're ready to send the data along with the POST request. Assuming that the data is in binary format, this is what you do:

byte[] data = ....; // the data to send

conn.setRequestProperty( 
   "Content-Length", Integer.toString(
                                   data.length ) );

OutputStream os = conn.openOutputStream();
os.write( data );
os.close();

What data format should you use for your communication with the server? It's really up to you. You could send an XML document, but XML is very verbose. If the server is written in the Java programming language, why not use the encoding and decoding capabilities of DataOutputStream and DataInputStream? In other words:

// Example of sending some
// structured data to the server...
DataOutputStream os = new DataOutputStream( 
                         conn.openOutputStream() );
os.writeBoolean( true );
os.writeUTF( "This is a string..." );
os.writeInt( 5 );
os.close();

After you close the output stream, an HTTP POST request is formed and sent to the web server. The MIDlet should next call getResponseCode to obtain the servlet's response code to check for a valid response. Normally, this means checking that the response code is HttpConnection.HTTP_OK:

int rc = conn.getResponseCode();
if( rc == HttpConnection.HTTP_OK ){
    // safe to process the response
} else {
    // deal with errors, warnings, redirections, etc.
}

A response code other than HTTP_OK is not necessarily an error. The web server might redirect you to another URL, for example, in which case you should resend the data to the new URL (the HttpConnectionHelper class handles this for you automatically).

Processing the response data is a matter of opening an input stream to read the response body. If structured data is being sent back, do the following:

DataInputStream in = new DataInputStream( 
                          conn.openInputStream() );
String msg = in.readUTF();
..... // etc. etc., read other data
in.close();

Alternatively, if the data being sent back is something like an image, you should check the length of the response to see how much data is being sent. If the length is not -1, allocate a byte array of that size and read the data into the byte array:

byte[]      data;
int         len = conn.getLength();
InputStream in = conn.openInputStream();

if( len != -1 ){ 
    int total = 0;
    data = new byte[len];
    
    while( total < len ){
       total += in.read( data, total, len - total );
    }
} else {
    bytearrayoutputstream tmp = 
                        new bytearrayoutputstream();
    int ch;
    
    while( ( ch = in.read() ) != -1 ){
        tmp.write( ch );
    }
    
    data = tmp.tobytearray();
}

in.close();

Be sure to respect the response body length. If you try to read more bytes than were sent, your application will block until the server decides to timeout the connection. Also, you must always be prepared to handle the case where the length is not set.

On the server side, things are fairly simple. In a way similar to how the MIDlet processes the response, the servlet gets the length of the request body and opens an input stream to process it:

int len = request.getContentLength();
ServletInputStream in = request.getInputStream();

if( len != -1 ){
    // read in a fixed number of bytes
} else {
    // read until EOF
}

After processing the input, the servlet should set the response code and the content type:

response.setStatus( Response.SC_OK );
response.setContentType(
                       "application/octet-stream" );

Of course, if an error occurs, the servlet calls sendError and does not send any response data. Otherwise, the servlet sets the content length (if known) and sends the data:

byte[] data = ....; // the data to send
response.setContentLength( data.length );

ServletOutputStream out = response.getOutputStream();
out.write( data );

The servlet then returns from the doPost method, and the web server sends the response to the client, which processes it as discussed above.

This Tech Tip ends with a complete client-server example that shows a MIDlet communicating with a servlet. The example uses the HttpConnectionHelper class previously described.

Let's start with the servlet code, since it's simpler. The servlet takes a string sent to it by the client, converts it to uppercase and sends it back as a series of tokens (words):

package j2me.techtips;

import java.io.*;
import java.util.*;
import javax.servlet.*;
import javax.servlet.http.*;

/**
 * A simple example of a servlet that responds to an 
 * input stream sent to it by a MIDP client.
 */

public class SampleServer extends HttpServlet {
    public void doPost( HttpServletRequest request,
                     HttpServletResponse response )
            throws IOException, ServletException {

        // Get the input stream and read the data...

        ServletInputStream in = 
                           request.getInputStream();
        DataInputStream    din = 
                          new DataInputStream( in );

        String text = din.readUTF();
        din.close();

        // Do something with the data. In this case 
        // make the string upper case and split it 
        // into tokens.

        text = text.toUpperCase();

        StringTokenizer tok = new StringTokenizer( 
                                             text );
        Vector v = new Vector();
        while( tok.hasMoreTokens() ){
            v.addElement( tok.nextToken() );
        }

        // Form a response: send back the # of strings
        // followed by each string in turn.

        ByteArrayOutputStream bout = 
                           new ByteArrayOutputStream();
        DataOutputStream dout = 
                          new DataOutputStream( bout );

        int size = v.size();
        dout.writeInt( size );
        for( int i = 0; i < size; ++i ){
            dout.writeutf( (string) v.elementat( i ) );
        }

        byte[] data = bout.tobytearray();

        // set the response headers and data...

        response.setcontenttype( 
                          "application/octet-stream" );
        response.setcontentlength( data.length );
        response.setstatus( response.sc_ok );

        outputstream out = response.getoutputstream();
        out.write( data );
        out.close();
    }
}

The MIDP client consists of an entry form where the user can type in some text and press the "Send" button to have it delivered to the server for processing:

package j2me.techtips;

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

/**
 * Sample client that shows how to communicate with 
 * a servlet running on a web server. Asks for a string 
 * from the user and sends it to the server, which then 
 * makes it upper case, and returns it as a series of 
 * tokens.
 */

public class SampleClient extends MIDlet
                        implements CommandListener {

    // The URL to connect to... 
    // change as appropriate...

    private static String url =
    "http://localhost:8080/servlet/j2me.techtips.SampleServer";

    private Display display;
    private Command exitCommand = 
             new Command( "Exit", Command.EXIT, 1 );
    private Command okCommand   = 
                 new Command( "OK", Command.OK, 1 );
    private Command sendCommand = 
               new Command( "Send", Command.OK, 1 );
    private TextBox entryForm;

    public SampleClient(){
    }

    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 );
        entryForm = new EntryForm();
        display.setCurrent( entryForm );
    }

    public void exitMIDlet(){
        notifyDestroyed();
    }

    public void commandAction( 
                        Command c, Displayable d ){
        if( c == sendCommand ){
            StatusForm f = 
              new StatusForm( entryForm.getString() );
            display.setCurrent( f );
            f.start();
        } else if( c == okCommand ){
            display.setCurrent( entryForm );
        } else {
            exitMIDlet();
        }
    }

    // The text entry form...

    class EntryForm extends TextBox {
        EntryForm(){
            super( "Enter some text", "", 80, 0 );
            addCommand( exitCommand );
            addCommand( sendCommand );
            setCommandListener( SampleClient.this );
        }
    }

    // A status for for displaying messages as the
    // data is sent...

    class StatusForm extends Form 
                   implements Runnable, 
                     HttpConnectionHelper.Callback {
        StatusForm( String text ){
            super( "Status" );

            // Convert the string into a byte array.  
            // Doing it this way ensures that the 
            // characters retain their encoding.

            try {
                ByteArrayOutputStream bout = 
                        new ByteArrayOutputStream();
                DataOutputStream      dout = 
                       new DataOutputStream( bout );

                dout.writeUTF( text );
                data = bout.toByteArray();

                dout.close();
            }
            catch( IOException e ){
                // should handle this....
            }
        }

        // Updates the display.

        void display( String text ){
            if( message == null ){
                message = new
                           StringItem( null, text );
                append( message );
            } else {
                message.setText( text );
            }
        }

        // Done.

        void done( String msg ){
            display( msg != null ? msg : "Done." );
            addCommand( okCommand );
            setCommandListener( SampleClient.this );
        }

        // Callback for making the HTTP connection.

        public void prepareRequest( 
              String originalURL, HttpConnection conn ) 
              throws IOException
        {
            conn.setRequestMethod( 
                                 HttpConnection.POST );
            conn.setRequestProperty( 
                                "User-Agent", 
           "Profile/MIDP-1.0 Configuration/CLDC-1.0" );
            conn.setRequestProperty( 
                         "Content-Language", "en-US" );
            conn.setRequestProperty( 
                "Accept", "application/octet-stream" );
            conn.setRequestProperty( 
                               "Connection", "close" );
            conn.setRequestProperty( 
                         "Content-Length", 
                     Integer.toString( data.length ) );

            OutputStream os = conn.openOutputStream();
            os.write( data );
            os.close();
        }

        // Do the connection on a separate thread to 
        // keep the UI responsive...

        public void run(){
            HttpConnection conn = null;

            display( 
               "Obtaining HttpConnection object..." );

            try {
                conn = HttpConnectionHelper.connect( 
                                          url, this );

                display( 
                      "Connecting to the server..." );
                int rc = conn.getResponseCode();

                if( rc == HttpConnection.HTTP_OK ){
                    StringBuffer text = 
                                   new StringBuffer();

                    // Here's where you read the data.  
                    // This case expects an integer 
                    // followed by zero or more 
                    // strings.

                    try {
                        DataInputStream din = 
                           new DataInputStream(
                             conn.openInputStream() );

                        int n = din.readInt();
                        while( n-- > 0 ){
                            text.append( 
                                      din.readUTF() );
                            text.append( '\n' );
                        }
                    }
                    catch( IOException e ){
                    }

                    done( 
                    "Response is:\n" + 
                                    text.toString() );
                } else {
                    done( 
                    "Unexpected return code: " + rc );
                }
            }
            catch( IOException e ){
                done( "Exception " + e + 
                              " trying to connect." );
            }
        }

        // Starts the upload in the background...

        void start(){
            display( "Starting..." );

            Thread t = new Thread( this );
            try {
                t.start();
            }
            catch( Exception e ){
                done( "Exception " + e + 
                         " trying to start thread." );
            }
        }

        private StringItem message;
        private byte[]     data;
    }
}

Notice how a separate thread is used to do the actual HTTP communication -- you should always do this to keep the user interface active and responsive.

Pixel

- NOTE

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

As of May 22, 2001, Sun Microsystems updated its Privacy Policy 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, choose the newsletters you want to subscribe to, and click Update.

- FEEDBACK

Comments? Send your feedback on the JDC Tech Tips to:

jdc-webmaster@sun.com

- ARCHIVES

You'll find the JDC Tech Tips archives at: http://java.sun.com/jdc/TechTips/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 JDC 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.

JDC Tech Tips August 20, 2001

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