A Java Chat Client for Palm OS
by Jonathan Knudsen03/07/2001
In his last article for oreilly.com, The Big Small Platform, Jonathan Knudsen introduced readers to Java 2, Micro Edition (J2ME) and one implementation on Palm OS. In this article, Jonathan presents source code for a network-chat client that runs on the Palm OS J2ME implementation currently available from Sun Microsystems.
Sun is talking a good talk with J2ME, but is it real yet or just vapor? To find out, I wrote a chat-client application in the Palm OS implementation of the Connected Limited Device Configuration (CLDC) of J2ME.
Prerequisites
To build the example chat client, you'll need several tools:
First, you'll need the Palm OS implementation of the CLDC. You need to register to download the software, but registration is free.
Next, you need a Palm OS device. The Palm OS CLDC implementation should run on a Palm III, Palm V, Palm VII, or Handspring Visor. Alternately, you can use the Palm OS emulator (POSE), a piece of software that runs on your desktop computer and emulates a Palm OS device.
Finally, you should have some kind of Java installed on your desktop computer. This will probably be a flavor of Java 2, Standard Edition (J2SE), either SDK 1.2 or SDK 1.3. I think JDK 1.1 will work too, but I haven't tried it. Your desktop computer will be used both for development and to run the chat server.
More About POSE
For development work, I highly recommend getting the Palm OS Emulator (POSE).
POSE only emulates Palm hardware, so you'll actually need to supply a ROM image that contains some version of Palm OS. If you have an actual Palm OS device, you can download the ROM from the device to the emulator. Otherwise, you can get a Palm OS ROM image from the above site by filling out some forms and jumping through some hoops.
The development cycle is much shorter using the emulator than using a real Palm OS device. You don't have to physically download your application to the device, and you don't have to worry about making a network connection from the Palm OS device when you need to test a networking application.
POSE has a handy switch that redirects the Palm OS network calls to your desktop computer's network connection. To set this up, right-click on POSE, choose Settings, then Properties, and check off "Redirect NetLib calls to host TCP/IP". Any time your POSE application tries to make a network connection, the underlying TCP/IP stack of your desktop computer will be used.
Building the Application
In my article titled The Big Small Platform, I talked about the steps involved in creating an application for the Palm OS CLDC implementation. This month's example application is composed of a handful of classes, which makes things a little more complicated. On Windows, I used the following batch file to automate building the application.
@setlocal @set cldc=\Apps\j2me_cldc @set lib=%cldc%\bin\api\classes @set palm_lib=%cldc%\tools\palm\src javac -bootclasspath %lib% -classpath .;%lib% Client.java \ Listener.java BufferedReader.java Login.java %cldc%\bin\preverify -classpath .;%lib% -d . Client Client$1 \ Listener Listener$1 BufferedReader Login java -classpath .;%palm_lib% palm.database.MakePalmApp \ -bootclasspath %lib% -o %cldc%\bin\Client.prc -networking \ Client Client$1 Listener Listener$1 BufferedReader Login @endlocal
The javac line of the batch file compiles all the source files for this application. The next line preverifies the classes, including the inner classes produced in compilation. The java line creates a Palm OS application file from all of the preverified classes.
The Chat Server, a J2SE Application
A chat client isn't much good unless it can connect to a server. The server consists of two pieces: Server.java (Listing 1) and Listener.java (Listing 2), an abstract class. I won't describe these classes--it's standard stuff, and you should be able to simply compile and run the server. On the command line, you can specify a server name and the port number to be used.
Jonathan Knudsen will speak about J2ME and XML at the O'Reilly Conference on Enterprise Java, March 26-29, 2001, in Santa Clara, California.
The Chat Client, in the J2ME World
The Palm CLDC chat client is composed of four classes:
The Client class (Listing 3) performs the bulk of the work. It provides a user interface, sends lines from the Palm to the chat server, and displays text received from the chat server on the screen.
The Login class (Listing 4) asks for initialization information for Client: the user can use Login to enter a screen name and the address and port number of the chat server.
The Listener abstract class (Listing 2) is used to set up a separate thread to listen for text coming from the server. (Note this is the same source code for the Listener class that the server uses, back in J2SE-land.)
A BufferedReader class (Listing 5) is provided as a bit of glue to read text lines from the server. It's kind of a brain-dead implementation--feel free to optimize.
Most of the code has to do with the user interface, which is the stuff in the com.sun.kjava package. Since this is outside the CLDC, and not likely to be included in any official Profiles, I recommend you don't spend a lot of time on it.
The important stuff is in the login() method of the Login class. It's in this method that the StreamConnection is obtained from the Connector class, like this:
String name = mNameField.getText(); String host = mHostField.getText(); int port = Integer.parseInt(mPortField.getText()); String url = "socket://" + host + ":" + port; StreamConnection s = (StreamConnection) Connector.open(url); Client c = new Client(name, s); c.register(NO_EVENT_OPTIONS);
The host and port number are pulled from user interface controls and assembled to create the url string, using the "socket://" prefix. Then the url string is passed to Connector's open() method, which returns a StreamConnection object. This object is passed to Client's constructor and control is passed to the Client object.
Client's constructor calls a helper method, wireNetwork(), that uses the StreamConnection for two purposes. It creates a PrintStream for writing text to the server. Then it creates an anonymous inner subclass of Listener. This object reads text lines coming from the server and adds them to the Palm's screen. The entire wireNetwork() method follows:
protected void wireNetwork(StreamConnection s)
throws IOException {
mOut = new PrintStream(s.openOutputStream());
new Listener(s.openInputStream()) {
public void processLine(String line) {
for (int i = 0; i < mDisplayLines.length - 1; i++)
mDisplayLines[i] = mDisplayLines[i + 1];
mDisplayLines[mDisplayLines.length - 1] = line;
paint();
}
};
}
The screen display is simply maintained as an array of Strings, mDisplayLines. When a new line is received from the server, the lines are shifted up and the new line is shown at the bottom.
When the user types text on the Palm OS device, then writes a newline, the text is sent to the server, using the PrintStream created in wireNetwork(). This happens in Client's sendLine() method:
protected void sendLine() {
// Send the stuff to the server.
String line = mName + ": " + mInputField.getText();
mOut.println(line);
mInputField.setText("");
}
The text is prepended with the user's screen name before being sent to the server. The input text field is cleared in preparation for the next text.
Go For a Spin
Building this application is a little tricky because it involves several different classes. Use something like the batch file described above and you should have no problems.
Once everything compiles, install the resulting Client.prc into the emulator (or a real Palm device, if you're feeling adventurous).
To test the chat client, you'll need to start the server running on a desktop machine. Then run Client on your Palm OS device, point it at the server address and port number, and tap on the Login button. If everything goes right, you should see a welcome message from the server on your Palm OS screen. Other people should be able to connect to your server at the same time.
You can send text to the server by typing it (if you're using the emulator) or writing it with the stylus. To send a new line to the server, hit Return or write a newline symbol with the stylus. The text will be sent to the server, which sends it to all connected clients so they can display it on their screens.
Conclusion
The Palm OS CLDC implementation is a good platform for exploring J2ME. It's certainly usable for developing networked applications on Palm OS. For production-level applications, you should probably wait until one of the J2ME Profiles gets ported to Palm OS. I've heard rumors of the Mobile Information Device Profile (MIDP) being ported to Palm OS, and there's also a PDA Profile in the works.
J2ME is exciting technology. The promise of Write Once, Run Anywhere is hanging in the air once again--let's hope it comes true, quickly, in the small device world.
Jonathan Knudsen is the author of an upcoming book, Mobile Java, about the J2ME Mobile Information Device Profile. Currently he is the Director of Courseware Development for LearningPatterns.com. Prior to that he was an author and editor at O'Reilly & Associates. His books include The Unofficial Guide to LEGO MINDSTORMS Robots, Java 2D Graphics, and Learning Java. While at O'Reilly he wrote a monthly column called Bite-Size Java. Jonathan works at his home in Princeton, New Jersey, with his wife, one cat, and four children.
Listing 1 - Server.java
import java.io.*;
import java.net.*;
import java.util.*;
public class Server {
public static void main(String[] args) throws IOException {
String name = args[0];
int port = Integer.parseInt(args[1]);
new Server(name, port);
}
private List mClients;
private String mName;
public Server(String name, int port) throws IOException {
mClients = new Vector();
mName = name;
ServerSocket serverSocket = new ServerSocket(port);
while (true) addClient(serverSocket.accept());
}
public void addClient(Socket clientSocket) throws IOException {
final PrintWriter out =
new PrintWriter(clientSocket.getOutputStream(), true);
mClients.add(out);
out.println("[Welcome to " + mName + ".]");
new Listener(clientSocket.getInputStream()) {
public void processLine(String line) {
if (line.length() == 0) mClients.remove(out);
else sendToClients(line);
}
};
}
public void sendToClients(String line) {
Iterator iterator = mClients.iterator();
while(iterator.hasNext()) {
PrintWriter out = (PrintWriter)iterator.next();
out.println(line);
}
}
}
Listing 2 - Listener.java
import java.io.*;
public abstract class Listener {
public abstract void processLine(String line);
private BufferedReader mIn;
public Listener(InputStream in) {
mIn = new BufferedReader(new InputStreamReader(in));
Thread t = new Thread() {
public void run() {
try {
String line;
while ((line = mIn.readLine()) != null) processLine(line);
}
catch (IOException ioe) {}
}
};
t.start();
}
}
Listing 3 - Client.java
import java.io.*;
import javax.microedition.io.*;
import com.sun.kjava.*;
public class Client
extends Spotlet {
private static Graphics sGraphics;
public static void main(String[] args) throws IOException {
// Clear off the KVM splash screen.
sGraphics = Graphics.getGraphics();
sGraphics.clearScreen();
// Put up the login screen.
Login l = new Login();
l.register(NO_EVENT_OPTIONS);
}
private String mName;
private String[] mDisplayLines;
private TextField mInputField;
private PrintStream mOut;
public Client(final String name, StreamConnection s)
throws IOException {
mName = name;
mDisplayLines = new String[12];
for (int i = 0; i < mDisplayLines.length; i++)
mDisplayLines[i] = "";
createUI();
wireNetwork(s);
}
public void shutDown() {
mOut.println("");
mOut.close();
}
protected void createUI() {
// Create the Input field and give it focus.
mInputField = new TextField("Send", 0, 0, 144, 12);
mInputField.setText("");
mInputField.setFocus();
}
protected void wireNetwork(StreamConnection s)
throws IOException {
mOut = new PrintStream(s.openOutputStream());
new Listener(s.openInputStream()) {
public void processLine(String line) {
for (int i = 0; i < mDisplayLines.length - 1; i++)
mDisplayLines[i] = mDisplayLines[i + 1];
mDisplayLines[mDisplayLines.length - 1] = line;
paint();
}
};
}
public void paint() {
sGraphics.clearScreen();
// Paint UI controls.
mInputField.paint();
// Draw the chat lines.
for (int i = 0; i < mDisplayLines.length; i++)
sGraphics.drawString(mDisplayLines[i], 0, 16 + 12 * i);
}
public void keyDown(int key) {
if (mInputField.hasFocus()) {
if (key == '\n') sendLine();
else mInputField.handleKeyDown(key);
}
}
protected void sendLine() {
// Send the stuff to the server.
String line = mName + ": " + mInputField.getText();
mOut.println(line);
mInputField.setText("");
}
}
Listing 4 - Login.java
import java.io.*;
import javax.microedition.io.*;
import com.sun.kjava.*;
public class Login
extends Spotlet {
private static Graphics sGraphics = Graphics.getGraphics();
private TextField mNameField, mHostField, mPortField;
private Button mLoginButton, mCancelButton;
public Login() {
mNameField = new TextField("Name", 30, 30, 100, 12);
mNameField.setText("Palmer");
mNameField.setFocus();
mHostField = new TextField("Host", 30, 60, 100, 12);
mHostField.setText("172.16.0.3");
mPortField = new TextField("Port", 30, 90, 100, 12);
mPortField.setText("7099");
mLoginButton = new Button("Login", 30, 120);
mCancelButton = new Button("Cancel", 90, 120);
paint();
}
public void paint() {
// Paint UI controls.
mNameField.paint();
mHostField.paint();
mPortField.paint();
mLoginButton.paint();
mCancelButton.paint();
}
public void keyDown(int key) {
if (mNameField.hasFocus()) mNameField.handleKeyDown(key);
else if (mHostField.hasFocus()) mHostField.handleKeyDown(key);
else if (mPortField.hasFocus()) mPortField.handleKeyDown(key);
}
public void penDown(int x, int y) {
if (mLoginButton.pressed(x, y)) login();
else if (mCancelButton.pressed(x, y)) System.exit(0);
else if (mNameField.pressed(x, y)) setFocus(mNameField);
else if (mHostField.pressed(x, y)) setFocus(mHostField);
else if (mPortField.pressed(x, y)) setFocus(mPortField);
}
private void setFocus(TextField t) {
killFocus();
t.setFocus();
}
private void killFocus() {
if (mNameField.hasFocus()) mNameField.loseFocus();
if (mHostField.hasFocus()) mHostField.loseFocus();
if (mPortField.hasFocus()) mPortField.loseFocus();
}
public void login() {
killFocus();
unregister();
try {
String name = mNameField.getText();
String host = mHostField.getText();
int port = Integer.parseInt(mPortField.getText());
String url = "socket://" + host + ":" + port;
StreamConnection s = (StreamConnection)
Connector.open(url);
Client c = new Client(name, s);
c.register(NO_EVENT_OPTIONS);
}
catch (IOException ioe) {
System.out.println(ioe);
System.exit(0);
}
}
}
Listing 5 - BufferedReader.java
import java.io.*;
public class BufferedReader {
private Reader mReader;
private char[] mBuffer;
public BufferedReader(Reader r) {
mReader = r;
mBuffer = new char[512];
}
public String readLine() throws IOException {
boolean trucking = true;
int index = 0;
while (trucking) {
int c = mReader.read();
if (c == '\n' || c == -1) trucking = false;
else if (c != '\r') mBuffer[index++] = (char)c;
}
if (index == 0) return null;
String line = new String(mBuffer, 0, index);
return line;
}
}


