Sun Java Solaris Communities My SDN Account
 
Article

Networking, User Experience, and Threads

 

The user's experience of an application is important to its success. A powerful application with a poor user interface will not be popular, whether it runs on desktop computers or small devices, but the demands are more stringent in the small device world. Desktop users, hardened by years of experience, may tolerate interfaces that are somewhat clumsy. Small devices, however, fall into the category of consumer electronics, where user expectations are much higher.

Many MIDlets make connections to the network. Because today's wireless networks are slow, a user interface question arises: what kind of feedback does the user see? As you'll see, answering this question will lead into a discussion of multithreading.

This article describes several approaches to providing user feedback during network activity, and to managing threads appropriately. It starts with a simple MIDlet that provides no feedback, then gradually enhance it.

A Stone Age Networked MIDlet

When I was in junior high school, my shop class was treated to a movie about a cartoon antihero who used tools inappropriately. He was forever banging on nails with a ratchet wrench or trying to cut sheet metal with a screwdriver. If this character were a programmer, he might write a networked MIDlet like this:

import java.io.*;

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

public class PrimitiveMIDlet
    extends MIDlet
    implements CommandListener {
  private Display mDisplay;
  private Form mMainForm;
  private Command mExitCommand, mConnectCommand;
  
  public void startApp() {
    mDisplay = Display.getDisplay(this);
    
    if (mMainForm == null) {
      mMainForm = new Form("PrimitiveMIDlet");
      mMainForm.append(new StringItem("",
      "Select Connect to make a network connection"));
      mExitCommand = new Command("Exit", Command.EXIT, 0);
      mConnectCommand = new Command("Connect", Command.SCREEN, 0);
      
      mMainForm.addCommand(mExitCommand);
      mMainForm.addCommand(mConnectCommand);
      mMainForm.setCommandListener(this);
    }
    
    mDisplay.setCurrent(mMainForm);
  }
  
  public void commandAction(Command c, Displayable s) {
    if (c == mExitCommand)
      notifyDestroyed();
    else if (c == mConnectCommand)
      connect();
  }
  
  private void connect() {
    String url = getAppProperty("NetworkThreading.URL");
    
    try {
      // Query the server and retrieve the response.
      HttpConnection hc = (HttpConnection)Connector.open(url);
      InputStream in = hc.openInputStream();
      
      // Pull back the server's response. If a content length is not
      // specified, we'll just read 255 bytes.
      int contentLength = (int)hc.getLength();
      if (contentLength == -1) contentLength = 255;
      byte[] raw = new byte[contentLength];
      int length = in.read(raw);
      
      // Clean up.
      in.close();
      hc.close();
      
      // Show the response to the user.
      String s = new String(raw, 0, length);
      Alert a = new Alert("Response", s, null, null);
      a.setTimeout(Alert.FOREVER);
      mDisplay.setCurrent(a, mMainForm);
    }
    catch (IOException ioe) {
      Alert a = new Alert("Exception", ioe.toString(), null, null);
      a.setTimeout(Alert.FOREVER);
      mDisplay.setCurrent(a, mMainForm);
    }
  }
  
  public void pauseApp() {}
  
  public void destroyApp(boolean unconditional) {}
}

PrimitiveMIDlet's screen flow is simple. It presents a main screen. From there, you can either exit the application or make a network connection. If you choose the network connection, the results from the server appear as shown.

Main form   Server response
Screen flow for PrimitiveMIDlet

What's so primitive about this code?

  • PrimitiveMIDlet doesn't provide any visual feedback while it's connecting to the network. This is not a problem in a development environment, where a device emulator is connecting to some server running on the programmer's local machine. The connection takes a fraction of a second, hardly perceptible. In the real world, the delay will be much more noticable. The user will be staring at an apparently frozen application for five or ten seconds while the MIDlet connects to the server.
  • This Neanderthal programmer has hijacked a system thread for his own lengthy processing. The system calls his commandAction() method when the user selects a command. The thread that calls this method belongs to the system, not to the developer. This would not be a crime if the method executed swiftly, but in this case, the network connection may hog the system's thread for a long time.

In J2SE application programming and even in servlet programming, the system creates a thread for you and there are few restrictions on how long your processing can take. The rule of MIDlet threading is simple and strict: the only threads that belong to you are the ones you create yourself.

In a MIDlet, you are writing code that the system will call from one of its own threads. When your MIDlet's startApp(), pauseApp(), destroyApp(), and event handler methods are called, for example, they run inside a system thread. Your methods must return quickly so that the system thread can continue its other work. Any task that can't be completed quickly must be moved out of the system's thread.

This style of programming may take some getting used to, as you are really only writing code that is called from the system's threads. If you've done any other GUI programming, however, this technique will be familiar. AWT and Swing have an event dispatch thread that handles operating system events and calls event handlers in your code. The rule is the same: event handlers should execute quickly and return control to the event dispatch thread so that the rest of the interface doesn't lock up.

The diagram below shows how the system thread calls methods in a MIDlet. The left side, in yellow, represents code in the underlying implementation of MIDP, while the right side, in blue, represents the code you have written. Time increases going down. The red line shows typical activity for the system's event thread. It spends most of its time in system code, harvesting events from the underlying device, repainting windows, and performing other important work. It makes brief excursions into your application code to call MIDlet life cycle methods or event handler methods.

Normal operation of the system's event thread
Normal operation of the system event thread

The next diagram reveals the flaw in PrimitiveMIDlet. The system thread gets hung up in the MIDlet's connect() method. While the system thread is waiting for a network connection, it is unable to fulfill its other obligations. The user interface freezes, the device appears to be dead, and the user becomes frustrated.

Hijacking the system event thread
Hijacking the system event thread

Using a New Thread for Network Activity

The way out of this trouble is to create a new thread that will perform the network connection. Instead of calling connect() in the system thread, the commandAction() method will create a separate thread that will call connect(), then terminate. The only code that needs to change is in commandAction().

public void commandAction(Command c, Displayable s) {
  if (c == mExitCommand)
    notifyDestroyed();
  else if (c == mConnectCommand) {
    Thread t = new Thread() {
      public void run() {
        connect();
      }
    };
    t.start();
  }
}

The MIDlet is now properly threaded. The following diagram shows how the new thread (in green) handles the network connection, leaving the system event thread (in red) to go about its business. If you download the source code for this article, you'll find this example is ThreadedMIDlet.java.

Using a new thread for network activity
Using a new thread for network activity

Unfortunately, the new thread introduces a new wrinkle. Because network activity takes place in the new thread, the user interface remains responsive--a good thing generally, but a problem here: an impatient user could invoke the Connect command repeatedly, creating a new thread each time:

Too many threads
Too many threads

In addition, we still haven't addressed a fundamental question: what does the user see when the application is connecting to the network? Adding a simple wait screen will answer that question, and solve the multiple thread problem, and observer a key precept of good user interface programming:

Let the User Know What's Happening

Clearly, you need to make the user wait while the MIDlet performs its network connection. Displaying a wait screen does the job simply and intuitively. It informs the user that the MIDlet is busy and also prevents the user from banging on the Connect command and spewing out more threads.

The simplest tactic is to create a new screen. We'll use a form with only a title. We can initialize it in startApp() at the same time we initialize our main screen.

mWaitForm = new Form("Waiting...");

Then it's a simple matter for commandAction() to display the wait screen just before it starts the network connection thread. In the downloaded code, this example is SimpleWaitMIDlet.java.

public void commandAction(Command c, Displayable s) {
  if (c == mExitCommand)
    notifyDestroyed();
  else if (c == mConnectCommand) {
    mDisplay.setCurrent(mWaitForm);
    Thread t = new Thread() {
      public void run() {
        connect();
      }
    };
    t.start();
  }
}

The screen flow for SimpleWaitMIDlet now includes a third screen:

Main form   Simple wait screen   Server response
Screen flow for SimpleWaitMIDlet

As long as the wait screen is displayed, the user cannot invoke the Connect command, so the MIDlet will never generate more than one network thread at a time. At the same time, the wait screen reassures the user that the application is still working. SimpleWaitMIDlet is about midway between rudimentary and deluxe. Read on to learn how to improve on this simple design.

Allow the User to Cancel

To improve on SimpleWaitMIDlet, let's begin by cleaning up the code. The MIDlet class serves several purposes and the threading is confusing. We'll begin by moving the connect() method into a separate class, CleanWorker. We'll have this worker class call back methods in the MIDlet class. A more elaborate implementation might define a listener interface for the worker class, but for now we'll keep things simple. The next code sample shows the new MIDlet class, CleanMIDlet.

At the same time we're tidying up the code, let's add a crucial element to the user interface. Because wireless networks tend to be slow and unreliable, we should give impatient users the option to cancel a connection in progress. To augment the user interface, we'll just add a Command to the wait form. To abort the connection, we'll close() the current HttpConnection.

CleanMIDlet handles the creation of the command and stores a reference to it in a member variable named mCancelCommand. The startApp() method also registers the MIDlet as the listener for command events from the wait form. Don't get confused here; the MIDlet is the command listener for both the main form and the wait form. In practice, the result is that the event handling code for the Cancel command is a simple addition to the MIDlet's commandAction() method. If the user does invoke the Cancel command, we just set the current screen back to the main form and call the worker class's cancel() method.

import java.io.*;

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

public class CleanMIDlet
    extends MIDlet
    implements CommandListener {
  private Display mDisplay;
  private Form mMainForm, mWaitForm;
  private Command mExitCommand, mConnectCommand, mCancelCommand;
  private CleanWorker mWorker;
  
  public void startApp() {
    mDisplay = Display.getDisplay(this);
    
    if (mMainForm == null) {
      mMainForm = new Form("CleanMIDlet");
      mMainForm.append(new StringItem("",
          "Select Connect to make a network connection"));
      mExitCommand = new Command("Exit", Command.EXIT, 0);
      mConnectCommand = new Command("Connect", Command.SCREEN, 0);
  
      mMainForm.addCommand(mExitCommand);
      mMainForm.addCommand(mConnectCommand);
      mMainForm.setCommandListener(this);
      
      mWaitForm = new Form("Waiting...");
      mCancelCommand = new Command("Cancel", Command.CANCEL, 0);
      
      mWaitForm.addCommand(mCancelCommand);
      mWaitForm.setCommandListener(this);
    }

    mDisplay.setCurrent(mMainForm);
  }
  
  public void commandAction(Command c, Displayable s) {
    if (c == mExitCommand)
      notifyDestroyed();
    else if (c == mConnectCommand) {
      mDisplay.setCurrent(mWaitForm);
      String url = getAppProperty("NetworkThreading.URL");
      mWorker = new CleanWorker(this, url);
      mWorker.start();
    }
    else if (c == mCancelCommand) {
      mDisplay.setCurrent(mMainForm);
      mWorker.cancel();
      mWorker = null;
    }
  }

  public void pauseApp() {}

  public void destroyApp(boolean unconditional) {}
  
  public void networkResponse(String s) {
    Alert a = new Alert("Response", s, null, null);
    a.setTimeout(Alert.FOREVER);
    mDisplay.setCurrent(a, mMainForm);
  }
  
  public void networkException(IOException ioe) {
    Alert a = new Alert("Exception", ioe.toString(), null, null);
    a.setTimeout(Alert.FOREVER);
    mDisplay.setCurrent(a, mMainForm);
  }
}

The connect() method doesn't change much when it moves into the network worker class. The worker is a subclass of Thread, which makes it easy to arrange that it runs in its own thread.

When the cancel() method is called, it sets a boolean member variable, mCancel, and closes the currently open connection, mHttpConnection. Closing the connection forces an IOException in the connect() method, whose exception handler checks mCancel to see whether the exception resulted from the user canceling the connection.

Note in connect() the call to setRequestProperty(), which turns off HTTP persistent connections. By default, the client may reuse outgoing connections to the same server. This feature does not work correctly with the J2MEWTK 1.0.3 emulator when connections are interrupted. Passing a Connection: close header in the HTTP request turns the feature off.

import java.io.*;

import javax.microedition.io.*;

public class CleanWorker extends Thread {
  private CleanMIDlet mMIDlet;
  private String mURL;
  private HttpConnection mHttpConnection;
  private boolean mCancel;
  
  public CleanWorker(CleanMIDlet midlet, String url) {
    mMIDlet = midlet;
    mURL = url;
    mCancel = false;
  }
  
  public void run() {
    connect();
  }
  
  public void cancel() {
    try {
      mCancel = true;
      if (mHttpConnection != null)
        mHttpConnection.close();
    }
    catch (IOException ignored) {}
  }
  
  private void connect() {
    InputStream in = null;
    
    try {
      // Query the server and retrieve the response.
      mHttpConnection = (HttpConnection)Connector.open(mURL);
      mHttpConnection.setRequestProperty("Connection", "close");
      in = mHttpConnection.openInputStream();
      
      // Pull back the server's response. If a content length is not
      // specified, we'll just read 255 bytes.
      int contentLength = (int)mHttpConnection.getLength();
      if (contentLength == -1) contentLength = 255;
      byte[] raw = new byte[contentLength];
      int length = in.read(raw);

      // Clean up.
      in.close();
      mHttpConnection.close();

      // Show the response to the user.
      String s = new String(raw, 0, length);
      mMIDlet.networkResponse(s);
    }
    catch (IOException ioe) {
      if (mCancel == false) {
        try {
          if (in != null) in.close();
          if (mHttpConnection != null) mHttpConnection.close();
        }
        catch (IOException ignored) {}
        mMIDlet.networkException(ioe);
      }
      mCancel = false;
    }
  }
}

Using One Thread for Network Activity

We've resolved our threading troubles and have some relatively clean code, but we're not handling threading as efficiently as we could. Currently, we create a new worker thread is created every time the user invokes the Connect command:

Clean threading
Clean threading

On the small platforms that run MIDP, creating a new thread consumes a lot of processing power and memory. It would be nice if we could reuse a single thread to do all the network stuff instead of creating a new thread every time we wanted to hit the network. The following diagram illustrates this technique. The worker thread is always present, but it becomes active (bold) only when network activity is requested.

Using a single worker thread
Using a single worker thread

Now that network activity is split into a separate worker class, this change is not too difficult. We'll create a new SingleWorker class based on our CleanWorker class. The run() method, instead of calling connect() once, now loops. It sits quietly in wait(). When another thread uses notify() to tell it to wake up, it calls the connect() method. The excerpt below shows the updated run() method as well as the go() method that wakes up the thread to do its network stuff. The mTrucking member variable is a boolean that indicates whether the SingleWorker instance is currently running, or should terminate.

public synchronized void run() {
  while (mTrucking) {
    try { wait(); }
    catch (InterruptedException ie) {}
    if (mTrucking) connect();
  }
}

public synchronized void go() {
  notify();
}

The corresponding MIDlet, SingleMIDlet, is little changed from CleanMIDlet. Its startApp() method creates a SingleWorker instance and kicks it off by calling start(). Whenever the application needs a network connection, commandAction() just calls SingleWorker's go() method.

import java.io.*;

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

public class SingleMIDlet
    extends MIDlet
    implements CommandListener {
  private Display mDisplay;
  private Form mMainForm, mWaitForm;
  private SingleWorker mWorker;
  private Command mExitCommand, mConnectCommand, mCancelCommand;
  
  public void startApp() {
    mDisplay = Display.getDisplay(this);
    
    if (mMainForm == null) {
      mMainForm = new Form("SingleMIDlet");
      mMainForm.append(new StringItem("",
          "Select Connect to make a network connection"));
      mExitCommand = new Command("Exit", Command.EXIT, 0);
      mConnectCommand = new Command("Connect", Command.SCREEN, 0);
  
      mMainForm.addCommand(mExitCommand);
      mMainForm.addCommand(mConnectCommand);
      mMainForm.setCommandListener(this);
      
      mWaitForm = new Form("Waiting...");
      mCancelCommand = new Command("Cancel", Command.CANCEL, 0);
      
      mWaitForm.addCommand(mCancelCommand);
      mWaitForm.setCommandListener(this);
            
      String url = getAppProperty("NetworkThreading.URL");
      mWorker = new SingleWorker(this, url);
      mWorker.start();
    }

    mDisplay.setCurrent(mMainForm);
  }
  
  public void commandAction(Command c, Displayable s) {
    if (c == mExitCommand)
      notifyDestroyed();
    else if (c == mConnectCommand) {
      mDisplay.setCurrent(mWaitForm);
      mWorker.go();
    }
    else if (c == mCancelCommand) {
      mDisplay.setCurrent(mMainForm);
      mWorker.cancel();
    }
  }

  public void pauseApp() {}

  public void destroyApp(boolean unconditional) {
    mWorker.stop();
  }
  
  public void networkResponse(String s) {
    Alert a = new Alert("Response", s, null, null);
    a.setTimeout(Alert.FOREVER);
    mDisplay.setCurrent(a, mMainForm);
  }
  
  public void networkException(IOException ioe) {
    Alert a = new Alert("Exception", ioe.toString(), null, null);
    a.setTimeout(Alert.FOREVER);
    mDisplay.setCurrent(a, mMainForm);
  }
}

Make It Pretty

Now that the code looks better, let's make a prettier wait screen. Our wait screen will be implemented as a Canvas subclass called WaitCanvas. It displays a rotating wheel and features a message that can be updated dynamically. To animate the wheel we use yet another thread (a Timer, really) that is initialized in WaitCanvas's constructor. Our thread diagram now includes a pink thread for animation of the wait screen.

Worker and animation threads
Worker and animation threads

WaitCanvas is listed below. Most of its code concerns drawing, but note the creation of a Timer and TimerTask in the constructor.

import java.util.*;

import javax.microedition.lcdui.*;

public class WaitCanvas
    extends Canvas {
  private int mCount, mMaximum;
  private int mInterval;
  
  private int mWidth, mHeight, mX, mY, mRadius;
  private String mMessage;
  
  public WaitCanvas() {
    mCount = 0;
    mMaximum = 36;
    mInterval = 100;
    
    mWidth = getWidth();
    mHeight = getHeight();
    
    // Calculate the radius.
    int halfWidth = (mWidth - mRadius) / 2;
    int halfHeight = (mHeight - mRadius) / 2;
    mRadius = Math.min(halfWidth, halfHeight);
    
    // Calculate the location.
    mX = halfWidth - mRadius / 2;
    mY = halfHeight - mRadius / 2;

    // Create a Timer to update the display.
    TimerTask task = new TimerTask() {
      public void run() {
        mCount = (mCount + 1) % mMaximum;
        repaint();
      }
    };
    Timer timer = new Timer();
    timer.schedule(task, 0, mInterval);
  }
  
  public void setMessage(String s) {
    mMessage = s;
    repaint();
  }
  
  public void paint(Graphics g) {
    int theta = -(mCount * 360 / mMaximum);
    
    // Clear the whole screen.
    g.setColor(255, 255, 255);
    g.fillRect(0, 0, mWidth, mHeight);
    
    // Now draw the pinwheel.
    g.setColor(0, 0, 0);
    g.drawArc(mX, mY, mRadius, mRadius, 0, 360);
    g.fillArc(mX, mY, mRadius, mRadius, theta +  90, 90);
    g.fillArc(mX, mY, mRadius, mRadius, theta + 270, 90);
    
    // Draw the message, if there is a message.
    if (mMessage != null)
      g.drawString(mMessage, mWidth / 2, mHeight,
          Graphics.BOTTOM | Graphics.HCENTER);
  }
}

The new MIDlet and worker classes, DeluxeMIDlet and DeluxeWorker, are similar to their predecessors. Note, though, that DeluxeWorker needs explicit knowledge of the wait screen in order to update the message that's displayed as the network connection is made. A reference to the wait screen is passed to DeluxeWorker's constructor and saved in a member variable.

The screen flow is the same as for SimpleWaitMIDlet, but the waiting screen is much more attractive, as shown below.

Main form   Pretty wait screen   Server response
Screen flow for DeluxeMIDlet

Summary

This article presented six different approaches to network threading and user experience. To help you find your way through the sample code, here's a summary of all six examples.

  1. PrimitiveMIDlet demonstrated a bad way to do network access. It used the system's event dispatch thread to perform a lengthy operation.
  2. ThreadedMIDlet fixed one threading problem and created another. It performed network activity in a new thread, but it allowed the user to hammer on the Connect command, spawning multiple threads, and it provided no feedback about the network activity.
  3. SimpleWaitMIDlet used a form to display a wait screen, which both informed the user of the network activity and limited the number of network threads to one.
  4. The same source code was broken into two pieces, CleanMIDlet and CleanWorker, for clarity and maintainability. At the same time, we added the capability to cancel a connection.
  5. SingleMIDlet and SingleWorker reused a single thread for all network activity instead of creating a new thread for each network connection.
  6. Finally, DeluxeMIDlet, DeluxeWorker, and WaitCanvas demonstrated an aesthetically pleasing implementation of a wait screen.

You can download the source code as a ZIP file. Alternately, you can download the JAD file and JAR file for a MIDlet suite that contains all of the examples.




Reader Feedback
Excellent   Good   Fair   Poor  

If you have other comments or ideas for future technical tips, please type them here:

Comments:
If you would like a reply to your comment, please submit your email address:
Note: We may not respond to all submitted comments.

Have a question about Java programming? Use Java Online Support.



Back To Top

Oracle is reviewing the Sun product roadmap and will provide guidance to customers in accordance with Oracle's standard product communication policies. Any resulting features and timing of release of such features as determined by Oracle's review of roadmaps, are at the sole discretion of Oracle. All product roadmap information, whether communicated by Sun Microsystems or by Oracle, does not represent a commitment to deliver any material, code, or functionality, and should not be relied upon in making purchasing decisions. It is intended for information purposes only, and may not be incorporated into any contract.