Bluetooth connections - Example: btsppEcho MIDlet

This section provides the source code for the btsppEcho. For the complete Eclipse project ZIP file, see Forum Nokia.

The example includes the following classes:

MIDletApplication

package example.btsppecho;


import java.io.IOException;
import java.util.Vector;
import javax.microedition.midlet.MIDlet;
import javax.microedition.lcdui.Display;
import javax.microedition.lcdui.Displayable;
import javax.bluetooth.BluetoothStateException;
import javax.bluetooth.LocalDevice;
import javax.bluetooth.ServiceRecord;

import example.btsppecho.client.ConnectionService;


public class MIDletApplication
    extends MIDlet
{
    private final static String UUID =
                                "A55665EE9F9146109085C2055C888B39";
    private final static String SERVICE_URL =
                                "btspp://localhost:" + UUID;
    private final SettingsList settingsList;
    private boolean restoreDiscoverableModeOnExit;
    private int initialDiscoverableMode;

    // Client server
    private ConnectionService ConnectionService = null;
    private ClientForm clientForm = null;

    // Server client
    private ServiceDiscoveryList serviceDiscoveryList = null;
    private ServerForm serverForm = null;
    boolean serverUseAuthentication = false;
    boolean serverUseEncryption = false;


    public MIDletApplication()
    {
        // Try to read the initial discoverable mode of
        // device, so it can be restored on exit.
        try
        {
            restoreDiscoverableModeOnExit = true;
            initialDiscoverableMode =
                LocalDevice.getLocalDevice()
                           .getDiscoverable();
        }
        catch (BluetoothStateException e)
        {
            restoreDiscoverableModeOnExit = false;
        }

        settingsList = new SettingsList(this);
    }


    private void exit()
    {
        destroyApp(false);
        notifyDestroyed();
    }


    public void startApp()
    {
        Display display = Display.getDisplay(this);
        display.setCurrent(settingsList);
        ErrorScreen.init(null, display);
    }


    public void pauseApp()
    {
        // we can ignore this
    }


    public void destroyApp(boolean unconditional)
    {
        // stop any networking, service discovery, etc.
        // threads that the MIDlet may have started

        if (serviceDiscoveryList != null)
        {
            serviceDiscoveryList.cancelPendingSearches();
            serviceDiscoveryList.abort();
        }

        if (ConnectionService != null)
        {
            ConnectionService.close();
        }

        if (clientForm != null)
        {
            clientForm.closeAll();
        }

        if (serverForm != null)
        {
            serverForm.closeAll();
        }

        // I restore the discoverable mode to the initial
        // value on exit, so the behaviour of this MIDlet
        // might be more similar in this respect on all
        // vendors' devices. (You might want to rethink
        // this in your MIDlet, for example if a device
        // allows you to run multiple simultaneous Bluetooth
        // applications, each having varying start &
        // exit times.)
        if (restoreDiscoverableModeOnExit)
        {
            try
            {
                LocalDevice ld = LocalDevice.getLocalDevice();
                ld.setDiscoverable(initialDiscoverableMode);
            }
            catch (BluetoothStateException e)
            {
                // there is nothing we can do
                // to handle this case: ignore it
            }
        }
    }


    // screen callbacks

    // ClientForm

    public void clientFormExitRequest()
    {
        exit();
    }


    public void clientFormViewLog(Displayable next)
    {
       LogScreen logScreen =
           new LogScreen(this,
                         next,
                         "Log",
                         "Back");
       Display.getDisplay(this).setCurrent(logScreen);
   }


    // SettingsList callbacks

    public void settingsListStart(boolean isServer,
                                  int inquiryAccessCode,
                                  boolean useAuthentication,
                                  boolean useAuthorization,
                                  boolean useEncryption)
    {
        // set inquiry access mode for ConnectionService
        if (!isServer)
        {
            try
            {
                LocalDevice.getLocalDevice()
                           .setDiscoverable(inquiryAccessCode);
            }
            catch(BluetoothStateException e)
            {
                String msg = "Error changing inquiry access type: " +
                          e.getMessage();
                ErrorScreen.showError(msg, settingsList);
            }
        }

        if (isServer)
        {
            // start application in server role

            // we only run one server at a time,
            // so the following is safe
            serverUseAuthentication = useAuthentication;
            serverUseEncryption = useEncryption;
            serviceDiscoveryList =
                new ServiceDiscoveryList(
                        this,
                        UUID,
                        inquiryAccessCode);
            Display.getDisplay(this)
                   .setCurrent(serviceDiscoveryList);
        }
        else
        {
            // start application in client role

            clientForm = new ClientForm(this);
            String url = SERVICE_URL;
            if (useAuthentication)
            {
                url += ";authenticate=true";
            }
            else
            {
                url += ";authenticate=false";
            }
            if (useAuthorization)
            {
                url += ";authorize=true";
            }
            else
            {
                url += ";authorize=false";
            }
            if (useEncryption)
            {
                url += ";encrypt=true";
            }
            else
            {
                url += ";encrypt=false";
            }

            url += ";name=btsppEcho";

            ConnectionService = new ConnectionService(url, clientForm);

            Display.getDisplay(this).setCurrent(clientForm);
        }
    }


    public void settingsListPropertiesRequest()
    {
        String[] keys =
        {
            "bluetooth.api.version",
            "bluetooth.connected.devices.max",
            "bluetooth.connected.inquiry",
            "bluetooth.connected.inquiry.scan",
            "bluetooth.connected.page",
            "bluetooth.connected.page.scan",
            "bluetooth.l2cap.receiveMTU.max",
            "bluetooth.master.switch",
            "bluetooth.sd.attr.retrievable.max",
            "bluetooth.sd.trans.max",
        };

        String str = "";
        try
        {
          str = "my bluetooth address: " +
                LocalDevice.getLocalDevice()
                           .getBluetoothAddress() + "\n";
        }
        catch (BluetoothStateException e)
        {
            // there is nothing we can do: ignore it
        }
        for (int i=0; i < keys.length; i++)
        {
           str += keys[i] + ": " +
                  LocalDevice.getProperty(keys[i]) + "\n";
        }

        TextScreen textScreen =
            new TextScreen(this,
                           settingsList,
                           "Device properties",
                           str,
                           "Back");
        Display.getDisplay(this).setCurrent(textScreen);
    }


    public void settingsListExitRequest()
    {
        exit();
    }


    // ServiceDiscoveryList callbacks

    public void serviceDiscoveryListFatalError(String errorMessage)
    {
        ErrorScreen.showError(errorMessage, serviceDiscoveryList);
        Display.getDisplay(this).setCurrent(settingsList);
    }


    public void serviceDiscoveryListError(String errorMessage)
    {
        ErrorScreen.showError(errorMessage, serviceDiscoveryList);
    }


    public void serviceDiscoveryListOpen(Vector selectedServiceRecords)
    {
        int security;
        if (serverUseAuthentication)
        {
           if (serverUseEncryption)
           {
               security = ServiceRecord.AUTHENTICATE_ENCRYPT;
           }
           else
           {
               security = ServiceRecord.AUTHENTICATE_NOENCRYPT;
           }
        }
        else
        {
           security = ServiceRecord.NOAUTHENTICATE_NOENCRYPT;
        }

        if (serverForm == null)
        {
            serverForm = new ServerForm(this);
        }
        serverForm.makeConnections(selectedServiceRecords, security);
        Display.getDisplay(this).setCurrent(serverForm);
    }


    public void serviceDiscoveryListExitRequest()
    {
        exit();
    }


    public void serviceDiscoveryListBackRequest(Displayable next)
    {
        Display.getDisplay(this).setCurrent(next);
    }


    public void serviceDiscoveryListViewLog(Displayable next)
    {
        LogScreen logScreen =
            new LogScreen(this,
                          next,
                          "Log",
                          "Back");
        Display.getDisplay(this).setCurrent(logScreen);
    }


    // TextScreen

    public void textScreenClosed(Displayable next)
    {
        Display.getDisplay(this).setCurrent(next);
    }


    // LogScreen

    public void logScreenClosed(Displayable next)
    {
        Display.getDisplay(this).setCurrent(next);
    }


    // ServerForm

    public void serverFormSearchRequest(int numConnectionsOpen)
    {
         // cleanup for new search
        serviceDiscoveryList.init(numConnectionsOpen);

        if (numConnectionsOpen > 0)
        {
            serviceDiscoveryList.addBackCommand(serverForm);
        }
        Display.getDisplay(this).setCurrent(serviceDiscoveryList);
    }


    public void serverFormExitRequest()
    {
        exit();
    }


    public void serverFormAddConnection(Vector alreadyOpen)
    {
        // I took a simple approach of simply changing the
        // screen to the ServiceDiscovery screen when the
        // user wants to try and add a new connection, or
        // perform both a new device inquiry + service search
        // and then add more connections.
        //
        // However, reality can be a bit more complicated:
        //   - How many previously discovered items (e.g. device
        //     running the desired service) have we already
        //     connected to, or not connected to?
        //   - How many additional new connections can this device
        //     open below its maximum limit? The maximum number
        //     of simultaneous connections can vary in different
        //     devices (i.e. see "bluetooth.connected.devices.max").
        //   - Can new inquiries/searches be started while
        //     already connected?
        // Depending on your MIDlet's needs + use cases and
        // the devices it is likely to be deployed in, it
        // might employ a bit more user friendly approach than
        // the simplistic/generic one used here.


        serviceDiscoveryList.remove(alreadyOpen);
        serviceDiscoveryList.addBackCommand(serverForm);
        Display.getDisplay(this).setCurrent(serviceDiscoveryList);
    }


    public void serverFormViewLog()
    {
        LogScreen logScreen =
            new LogScreen(this,
                          serverForm,
                          "Log",
                          "Back");
        Display.getDisplay(this).setCurrent(logScreen);
    }
}

ClientForm

package example.btsppecho;


import java.io.IOException;
import java.util.Vector;
import javax.bluetooth.BluetoothStateException;
import javax.bluetooth.LocalDevice;
import javax.microedition.lcdui.Command;
import javax.microedition.lcdui.CommandListener;
import javax.microedition.lcdui.Displayable;
import javax.microedition.lcdui.Form;
import javax.microedition.lcdui.Item;
import javax.microedition.lcdui.StringItem;
import javax.microedition.lcdui.TextField;

import example.btsppecho.client.ClientConnectionHandler;
import example.btsppecho.client.ClientConnectionHandlerListener;
import example.btsppecho.client.ConnectionService;


public class ClientForm
    extends Form
    implements CommandListener, ClientConnectionHandlerListener
{
    private final MIDletApplication midlet;
    private final StringItem numConnectionsField;
    private final TextField sendDataField;
    private final StringItem receivedDataField;
    private final StringItem statusField;
    private final Command sendCommand;
    private final Command quitCommand;
    private final Command logCommand;
    private final Vector handlers = new Vector();

    private StringItem btAddressField = null;
    private volatile int numReceivedMessages = 0;
    private volatile int numSentMessages = 0;
    private int sendMessageId = 0;


    public ClientForm(MIDletApplication midlet)
    {
        super("Client");
        this.midlet = midlet;

        try
        {
            String address = LocalDevice.getLocalDevice()
                                        .getBluetoothAddress();
            btAddressField = new StringItem("My address", address);
            append(btAddressField);
        }
        catch (BluetoothStateException e)
        {
            // nothing we can do, don't add field
        }

        numConnectionsField = new StringItem("Connections", "0");
        append(numConnectionsField);
        statusField = new StringItem("Status", "listening");
        append(statusField);
        sendDataField = new TextField("Send data",
                                      "Client says: 'Hello, world.'",
                                      64,
                                      TextField.ANY);
        append(sendDataField);
        receivedDataField = new StringItem("Last received data", null);
        append(receivedDataField);

        sendCommand = new Command("Send", Command.SCREEN, 1);
        quitCommand = new Command("Exit", Command.EXIT, 1);
        logCommand = new Command("View log", Command.SCREEN, 2);
        addCommand(quitCommand);
        addCommand(logCommand);
        setCommandListener(this);
    }


    void closeAll()
    {
        for (int i=0; i < handlers.size(); i++)
        {
            ClientConnectionHandler handler =
                (ClientConnectionHandler) handlers.elementAt(i);
            handler.close();
        }
    }


    public void commandAction(Command cmd, Displayable disp)
    {
        if (cmd == logCommand)
        {
            midlet.clientFormViewLog(this);
        }
        if (cmd == sendCommand)
        {
            String sendData = sendDataField.getString();
            try
            {
                sendMessageToAllClients(++sendMessageId, sendData);
            }
            catch (IllegalArgumentException e)
            {
                // Message length longer than
                // ServerConnectionHandler.MAX_MESSAGE_LENGTH

                String errorMessage =
                           "IllegalArgumentException while trying " +
                           "to send a message: " + e.getMessage();

                handleError(null, errorMessage);
            }
        }
        else if (cmd == quitCommand)
        {
            // the MIDlet aborts the ConnectionService, etc.
            midlet.clientFormExitRequest();
        }
    }


    public void removeHandler(ClientConnectionHandler handler)
    {
        // Note: we assume the caller has aborted/closed/etc.
        // the handler if that needed to be done. This method
        // simply removes it from the list of handlers maintained
        // by the ConnectionService.
        handlers.removeElement(handler);
    }


    public void sendMessageToAllClients(int sendMessageId, String sendData)
        throws IllegalArgumentException
    {
        Integer id = new Integer(sendMessageId);

        for (int i=0; i < handlers.size(); i++)
        {
            ClientConnectionHandler handler =
                (ClientConnectionHandler) handlers.elementAt(i);

            // throws IllegalArgumentException if message length
            // > ServerConnectionHandler.MAX_MESSAGE_LENGTH
            handler.queueMessageForSending(
                        id,
                        sendData.getBytes());
        }
    }


    // interface L2CAPConnectionListener

    public void handleAcceptAndOpen(ClientConnectionHandler handler)
    {
        handlers.addElement(handler);
        // start the reader and writer, it also causes underlying
        // InputStream and OutputStream to be opened.
        handler.start();

        statusField.setText("'Accept and open' for new connection");
    }


    public void handleStreamsOpen(ClientConnectionHandler handler)
    {

         // first connection
         if (handlers.size() == 1)
         {
             addCommand(sendCommand);
         }

         String str = Integer.toString(handlers.size());
         numConnectionsField.setText(str);
         statusField.setText("I/O streams opened on connection");
    }


    public void handleStreamsOpenError(ClientConnectionHandler handler,
                                       String errorMessage)
    {
        handlers.removeElement(handler);

        String str = Integer.toString(handlers.size());
        numConnectionsField.setText(str);
        statusField.setText("Error opening I/O streams: " +
                            errorMessage);
    }



    public void handleReceivedMessage(ClientConnectionHandler handler,
                                      byte[] messageBytes)
    {
        numReceivedMessages++;

        String msg = new String(messageBytes);
        receivedDataField.setText(msg);

        statusField.setText(
            "# messages read: " + numReceivedMessages + " " +
            "sent: " + numSentMessages);
    }


    public void handleQueuedMessageWasSent(
                    ClientConnectionHandler handler,
                    Integer id)
    {
        numSentMessages++;

        statusField.setText("# messages read: " +
                    numReceivedMessages + " " +
                    "sent: " + numSentMessages);
    }


    public void handleClose(ClientConnectionHandler handler)
    {
        removeHandler(handler);
        if (handlers.size() == 0)
        {
            removeCommand(sendCommand);
        }

        String str = Integer.toString(handlers.size());
        numConnectionsField.setText(str);
        statusField.setText("Connection closed");
    }


    public void handleErrorClose(ClientConnectionHandler handler,
                                 String errorMessage)
    {
        removeHandler(handler);
        if (handlers.size() == 0)
        {
            removeCommand(sendCommand);
        }

        statusField.setText("Error: (close)\"" + errorMessage + "\"");
    }


    public void handleError(ClientConnectionHandler hander,
                            String errorMessage)
    {
        statusField.setText("Error: \"" + errorMessage + "\"");
    }
}

ServerForm

package example.btsppecho;


import java.io.IOException;
import java.util.Vector;

import javax.bluetooth.BluetoothStateException;
import javax.bluetooth.LocalDevice;
import javax.bluetooth.ServiceRecord;

import javax.microedition.lcdui.Command;
import javax.microedition.lcdui.CommandListener;
import javax.microedition.lcdui.Displayable;
import javax.microedition.lcdui.Form;
import javax.microedition.lcdui.Item;
import javax.microedition.lcdui.StringItem;
import javax.microedition.lcdui.TextField;

import example.btsppecho.server.ServerConnectionHandler;
import example.btsppecho.server.ServerConnectionHandlerListener;


class ServerForm
    extends Form
    implements ServerConnectionHandlerListener, CommandListener
{
    private final MIDletApplication midlet;
    private final StringItem numConnectionsField;
    private final TextField sendDataField;
    private final StringItem receivedDataField;
    private final StringItem statusField;
    private final Command sendCommand;
    private final Command addConnectionCommand;
    private final Command searchCommand;
    private final Command logCommand;
    private final Command quitCommand;
    private final Command clearStatusCommand;
    private final Vector handlers;

    private int maxConnections;
    private StringItem btAddressField = null;
    private volatile int numReceivedMessages = 0;
    private volatile int numSentMessages = 0;
    private int sendMessageId = 0;


    ServerForm(MIDletApplication midlet)
    {
        super("Server");
        this.midlet = midlet;
        handlers = new Vector();

        String value =
            LocalDevice.getProperty(
                            "bluetooth.connected.devices.max");
        try
        {
            maxConnections = Integer.parseInt(value);
        }
        catch (NumberFormatException e)
        {
            maxConnections = 0;
        }

        // 1. add Form items
        try
        {
            String address = LocalDevice.getLocalDevice()
                                        .getBluetoothAddress();
            btAddressField = new StringItem("My address", address);
            append(btAddressField);
        }
        catch (BluetoothStateException e)
        {
            // nothing we can do, don't add field
        }
        numConnectionsField = new StringItem("Connections", "0");
        append(numConnectionsField);
        statusField = new StringItem("Status", "");
        append(statusField);
        sendDataField = new TextField("Send data",
                                      "Server says: 'Hello, world.'",
                                      64,
                                      TextField.ANY);
        append(sendDataField);
        receivedDataField = new StringItem("Last received data",
                                           null);
        append(receivedDataField);


        // 2. create commands
        sendCommand = new Command("Send", Command.SCREEN, 1);
        searchCommand = new Command("Search for more",
                                    Command.SCREEN,
                                    1);
        addConnectionCommand = new Command("Add connection",
                                           Command.SCREEN,
                                           2);
        logCommand = new Command("View log", Command.SCREEN, 3);
        clearStatusCommand = new Command("Clear status", Command.SCREEN, 4);
        quitCommand = new Command("Quit", Command.EXIT, 1);


        // 3. add commands and set command listener
        addCommand(searchCommand);
        addCommand(addConnectionCommand);
        addCommand(logCommand);
        addCommand(clearStatusCommand);
        addCommand(quitCommand);
        // The 'sendCommand' is added later to screen,
        // if at least one connection is open.
        setCommandListener(this);
    }


    public void makeConnections(Vector serviceRecords, int security)
    {
        for (int i=0; i < serviceRecords.size(); i++)
        {
            ServiceRecord serviceRecord =
                (ServiceRecord) serviceRecords.elementAt(i);
            boolean found = false;
            for (int j=0; j < handlers.size(); j++)
            {
                ServerConnectionHandler old =
                    (ServerConnectionHandler) handlers.elementAt(j);
                String oldAddr = old.getServiceRecord().
                                     getHostDevice().
                                     getBluetoothAddress();
                String newAddr =
                       serviceRecord.getHostDevice()
                                    .getBluetoothAddress();
                if (oldAddr.equals(newAddr))
                {
                     found = true;
                     break;
                }
            }
            if (!found)
            {
                ServerConnectionHandler newHandler =
                    new ServerConnectionHandler(
                            this,
                            serviceRecord,
                            security);
                newHandler.start(); // start reader & writer
            }
        }
    }


    private void removeHandler(ServerConnectionHandler handler)
    {
        if (handlers.contains(handler))
        {
            handlers.removeElement(handler);
            String str = Integer.toString(handlers.size());
            numConnectionsField.setText(str);
            if (handlers.size() == 0)
            {
                removeCommand(sendCommand);
                addCommand(searchCommand);
            }
        }
    }


    void closeAll()
    {
        for (int i=0; i < handlers.size(); i++)
        {
            ServerConnectionHandler handler =
                (ServerConnectionHandler) handlers.elementAt(i);
            handler.close();
            removeHandler(handler);
        }
    }


    public void commandAction(Command cmd, Displayable disp)
    {
        if (cmd == addConnectionCommand)
        {
            Vector v = new Vector();
            for (int i=0; i < handlers.size(); i++)
            {
                ServerConnectionHandler handler =
                    (ServerConnectionHandler) handlers.elementAt(i);
                String btAddress = handler.getServiceRecord()
                                       .getHostDevice()
                                       .getBluetoothAddress();
                v.addElement(btAddress);
            }
            midlet.serverFormAddConnection(v);
        }
        else if (cmd == clearStatusCommand)
        {
            statusField.setText("");
        }
        else if (cmd == logCommand)
        {
            midlet.serverFormViewLog();
        }
        else if (cmd == sendCommand)
        {
            String sendData = sendDataField.getString();
            Integer id = new Integer(sendMessageId++);

            for (int i=0; i < handlers.size(); i++)
            {
                ServerConnectionHandler handler =
                    (ServerConnectionHandler) handlers.elementAt(i);
                try
                {
                    handler.queueMessageForSending(
                                id,
                                sendData.getBytes());
                    statusField.setText(
                                    "Queued a send message request");
                }
                catch(IllegalArgumentException e)
                {
                    // Message length longer than
                    // ServerConnectionHandler.MAX_MESSAGE_LENGTH

                    String errorMessage =
                        "IllegalArgumentException while trying " +
                        "to send a message: " + e.getMessage();
                    handleError(handler, errorMessage);
                }
            }
        }
        else if (cmd == searchCommand)
        {
            midlet.serverFormSearchRequest(handlers.size());
        }
        else if (cmd == quitCommand)
        {
            closeAll();
            midlet.serverFormExitRequest();
        }

        // To keep this MIDlet simple, I didn't add any way
        // to drop individual connections. But you might
        // want to do so.
    }


    // ServerConnectionHandlerListener interface methods

    public void handleOpen(ServerConnectionHandler handler)
    {
        handlers.addElement(handler);
        // for the first open connection
        if (handlers.size() == 1)
        {
            removeCommand(searchCommand);

            removeCommand(sendCommand);
            addCommand(sendCommand);
        }

        // Remove the 'Add connection' command
        // when the device already has open the
        // maximum number of connections it can
        // support.
        if (handlers.size() >= maxConnections)
        {
            removeCommand(addConnectionCommand);
        }

        statusField.setText("Connection opened");
        String str = Integer.toString(handlers.size());
        numConnectionsField.setText(str);
    }


    public void handleOpenError(
                    ServerConnectionHandler handler,
                    String errorMessage)
    {
        statusField.setText("Error opening outbound connection: " +
                            errorMessage);
    }


    public void handleReceivedMessage(
                    ServerConnectionHandler handler,
                    byte[] messageBytes)
    {
        numReceivedMessages++;

        String message = new String(messageBytes);
        receivedDataField.setText(message);

        statusField.setText(
            "# messages read: " + numReceivedMessages + " " +
            "sent: " + numSentMessages);

        // Broadcast message to all clients
        for (int i=0; i < handlers.size(); i++)
        {
            ServerConnectionHandler h =
                (ServerConnectionHandler) handlers.elementAt(i);

            Integer id = new Integer(sendMessageId++);
            try
            {
                h.queueMessageForSending(id, messageBytes);
            }
            catch (IllegalArgumentException e)
            {
                String errorMessage =
                    "IllegalArgumentException while trying to " +
                    "send message: " + e.getMessage();
                handleError(handler, errorMessage);
            }
        }
    }


    public void handleQueuedMessageWasSent(
                    ServerConnectionHandler handler,
                    Integer id)
    {
        numSentMessages++;
        statusField.setText("# messages read: " +
                            numReceivedMessages + " " +
                            "sent: " + numSentMessages);
    }


    public void handleClose(ServerConnectionHandler handler)
    {
        removeHandler(handler);
        if (handlers.size() == 0)
        {
            removeCommand(sendCommand);
            addCommand(searchCommand);
        }

        // If the number of currently open connections
        // drops below the maximum number that this
        // device could have open, restore
        // 'Add connection' to the screen commands.
        if (handlers.size() < maxConnections)
        {
            removeCommand(addConnectionCommand);
            addCommand(addConnectionCommand);
        }

        statusField.setText("Connection closed");
    }


    public void handleErrorClose(ServerConnectionHandler handler,
                                 String errorMessage)
    {
        removeHandler(handler);
        if (handlers.size() == 0)
        {
            removeCommand(sendCommand);
            addCommand(searchCommand);
        }

        statusField.setText("Error (close): '" + errorMessage + "'");
    }


    public void handleError(ServerConnectionHandler handler,
                            String errorMessage)
    {
        statusField.setText("Error: '" + errorMessage + "'");
    }
}

LogScreen

package example.btsppecho;


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

import javax.bluetooth.DiscoveryAgent;
import javax.bluetooth.DiscoveryListener;


// This class represents a simple log screen. In the ideal case,
// the actual log would be encapsulated by a different class
// than the presentation (LogScreen). It is a bit less elegant,
// but eliminates one class, to combine the log and log
// screen in the same class. This  MIDlet uses the LogScreen
// mainly as a simple aid during device inquiry + service discovery
// to help a user follow the progress if they wish to. So the
// log isn't persistently saved in the record store. (That would
// be easy to add if it were needed.) For unsophisticated users,
// a MIDlet would use some other visual aid rather than a log to show
// progress through the device inquiry + service discovery phases.
// The target users of this MIDlet are mainly developers learning
// to use Bluetooth, so a LogScreen is probably more helpful for them,
// as it helps show how which Bluetooth devices were found during
// device inquiry, which devices of those are running the desired
// service, and so on.

public class LogScreen
    extends Form
    implements CommandListener
{
    private static final Vector entries = new Vector();
    private static final String FIRST_ENTRY = "-- Log started: --\n\n";

    // We place a limit the maximum number of entries logged.
    // Only the 1 .. MAX_ENTRIES last entries will be kept
    // in the log. If the log exceeds MAX_ENTRIES, the
    // earliest entries will be deleted.
    private static final int MAX_ENTRIES = 300;

    static
    {
        log(FIRST_ENTRY);
    }


    private final MIDletApplication midlet;
    private final Displayable next;
    private final Command refreshCommand;
    private final Command deleteCommand;
    private final Command closeCommand;


    public LogScreen(MIDletApplication midlet,
                     Displayable next,
                     String title,
                     String closeLabel)
    {
        super(title);
        this.midlet = midlet;
        this.next = next;

        refresh(); // add any text already present

        refreshCommand = new Command("Refresh", Command.SCREEN, 1);
        deleteCommand = new Command("Delete", Command.SCREEN, 2);
        closeCommand = new Command(closeLabel, Command.SCREEN, 3);
        addCommand(refreshCommand);
        addCommand(deleteCommand);
        addCommand(closeCommand);
        setCommandListener(this);
    }


    public static void log (String string)
    {
        if (entries.size() > MAX_ENTRIES)
        {
            entries.removeElementAt(0);
        }
        entries.addElement(string);
    }


    private void refresh()
    {
        // clear the display's text
        while(size() > 0)
        {
            delete(0);
        }

        // get the lastest status and display that as text
        String text = "";
        for (int i=0; i < entries.size(); i++)
        {
            String str = (String) entries.elementAt(i);
            if (str != null)
            {
                text += str;
            }
        }
        append(text);
    }


    public void commandAction(Command command, Displayable d)
    {
        if (command == closeCommand)
        {
            midlet.logScreenClosed(next);
        }
        else if (command == refreshCommand)
        {
            refresh();
        }
        else if (command == deleteCommand)
        {
            // The deletion of all log strings affects
            // all LogScreen instances.

            synchronized(this)
            {
                entries.removeAllElements();
                log(FIRST_ENTRY);
            }

            refresh();
        }
    }


    // It was somewhat convenient to place these helper
    // methods inside the LogScreen class.

    public static String inquiryAccessCodeString(int iac)
    {
        String str = null;
        switch(iac)
        {
            case DiscoveryAgent.CACHED:
                str = "CACHED";
                break;

            case DiscoveryAgent.GIAC:
                str = "GIAC";
                break;

            case DiscoveryAgent.LIAC:
                str = "LIAC";
                break;

            case DiscoveryAgent.PREKNOWN:
                str = "PREKNOWN";
                break;
        }
        return str;
    }


    public static String responseCodeString(int responseCode)
    {
        String str = null;
        switch (responseCode)
        {
          case DiscoveryListener.SERVICE_SEARCH_COMPLETED:
              str = "SERVICE_SEARCH_COMPLETED";
              break;
          case DiscoveryListener.SERVICE_SEARCH_DEVICE_NOT_REACHABLE:
              str = "SERVICE_SEARCH_DEVICE_NOT_REACHABLE";
              break;
          case DiscoveryListener.SERVICE_SEARCH_ERROR:
              str = "SERVICE_SEARCH_ERROR";
              break;
          case DiscoveryListener.SERVICE_SEARCH_NO_RECORDS:
              str = "SERVICE_SEARCH_NO_RECORDS";
              break;
          case DiscoveryListener.SERVICE_SEARCH_TERMINATED:
              str = "SERVICE_SEARCH_TERMINATED";
              break;
        }
        return str;
    }
}

ServiceDiscoveryList

package example.btsppecho;


import java.util.Hashtable;
import java.util.Vector;
import java.util.Enumeration;

import javax.bluetooth.BluetoothStateException;
import javax.bluetooth.DataElement;
import javax.bluetooth.DeviceClass;
import javax.bluetooth.DiscoveryAgent;
import javax.bluetooth.DiscoveryListener;
import javax.bluetooth.LocalDevice;
import javax.bluetooth.RemoteDevice;
import javax.bluetooth.ServiceRecord;
import javax.bluetooth.UUID;

import javax.microedition.lcdui.Command;
import javax.microedition.lcdui.CommandListener;
import javax.microedition.lcdui.Displayable;
import javax.microedition.lcdui.Image;
import javax.microedition.lcdui.ImageItem;
import javax.microedition.lcdui.List;
import javax.microedition.lcdui.Form;

import example.btsppecho.MIDletApplication;


class ServiceDiscoveryList
    extends List
    implements CommandListener,
               DiscoveryListener,
               Runnable
{
    private final static String TITLE = "New search";
    private final static int WAIT_MILLIS = 500; // milliseconds
    private final static String[] ACTIVITY = { "",
                                               ".",
                                               "..",
                                               "...",
                                               "...." };

    private final UUID uuid;
    private final MIDletApplication midlet;
    private final Command backCommand;
    private final Command searchCommand;
    private final Command openCommand;
    private final Command stopCommand;
    private final Command logCommand;
    private final Command exitCommand;


    private final int sdTransMax;
    private final int inquiryAccessCode;

    private DiscoveryAgent discoveryAgent;
    private volatile boolean inquiryInProgress = false;
    private volatile int numServiceSearchesInProgress = 0;
    private volatile boolean aborting = false;
    private volatile Thread thread;
    private Displayable backDisplayable;

    private volatile Thread activityIndicatorThread;
    private boolean activityIndicatorRunning = false;

    private Vector unsearchedRemoteDevices = new Vector();
    private Vector transIds = new Vector();
    private Hashtable matchingServiceRecords = new Hashtable();

    private int numConnectionsAlreadyOpen = 0;


    ServiceDiscoveryList(MIDletApplication midlet,
                         String uuidString,
                         int inquiryAccessCode)
    {
        super(TITLE, List.IMPLICIT);
        this.midlet = midlet;
        uuid = new UUID(uuidString, false);
        this.inquiryAccessCode = inquiryAccessCode;

        openCommand = new Command("Open connection",
                                  Command.SCREEN,
                                  1);
        searchCommand = new Command("Search", Command.SCREEN, 2);
        logCommand = new Command("View log", Command.SCREEN, 3);
        stopCommand = new Command("Stop", Command.SCREEN, 4);
        backCommand = new Command("Back", Command.BACK, 5);
        exitCommand = new Command("Exit", Command.EXIT, 0);


        String property =
               LocalDevice.getProperty("bluetooth.sd.trans.max");
        sdTransMax = Integer.parseInt(property);

        // create discovery agent
        try
        {
            discoveryAgent =
                LocalDevice.getLocalDevice().getDiscoveryAgent();

            addCommand(logCommand);
            addCommand(exitCommand);
            addCommand(searchCommand);
            setCommandListener(this);

            start();
        }
        catch(BluetoothStateException e)
        {
            midlet.serviceDiscoveryListFatalError(
                      "Couldn't get a discovery agent: '" +
                      e.getMessage() + "'");
        }
    }

    static Image makeImage(String filename)
    {
        Image image = null;

        try
        {
            image = Image.createImage(filename);
        }
        catch (Exception e)
        {
            // there's nothing we can do, so ignore
        }
        return image;
    }



    public void addBackCommand(Displayable backDisplayable)
    {
        this.backDisplayable = backDisplayable;
        removeCommand(backCommand);
        addCommand(backCommand);
    }


    public synchronized void start()
    {
        thread = new Thread(this);
        thread.start();
    }


    public synchronized void abort()
    {
        thread = null;
    }


    public void init(int numConnectionsAlreadyOpen)
    {
        this.numConnectionsAlreadyOpen =
             numConnectionsAlreadyOpen;

        // stop any pending searches
        if (inquiryInProgress ||
            numServiceSearchesInProgress > 0)
        {
            cancelPendingSearches();
        }

        // remove any old list elements
        while (size() > 0)
        {
            delete(0);
        }
    }


    private synchronized void setInquiryInProgress(boolean bool)
    {
        inquiryInProgress = bool;
    }


    public void commandAction(Command command, Displayable d)
    {
        if (command == logCommand)
        {
            midlet.serviceDiscoveryListViewLog(this);
        }
        else if (command == searchCommand &&
                 !inquiryInProgress &&
                 (numServiceSearchesInProgress == 0))
        {
            // new inquiry started

            removeCommand(openCommand);

            // remove old device lists
            unsearchedRemoteDevices.removeAllElements();
            for (Enumeration keys = matchingServiceRecords.keys();
                 keys.hasMoreElements();)
            {
                matchingServiceRecords.remove(keys.nextElement());
            }

            // delete all old List items
            while (size() > 0)
            {
                delete(0);
            }

            try
            {
                // disable page scan and inquiry scan
                LocalDevice dev = LocalDevice.getLocalDevice();
                dev.setDiscoverable(DiscoveryAgent.NOT_DISCOVERABLE);

                String iacString =
                           LogScreen.inquiryAccessCodeString(inquiryAccessCode);
                LogScreen.log("startInquiry (" +
                              iacString + ")\n");

                //startActivityIndicator();
                // this is non-blocking
                discoveryAgent.startInquiry(inquiryAccessCode, this);

                setInquiryInProgress(true);
                addCommand(stopCommand);
                removeCommand(searchCommand);
            }
            catch (BluetoothStateException e)
            {
                addCommand(searchCommand);
                removeCommand(stopCommand);
                midlet.serviceDiscoveryListError(
                           "Error during startInquiry: '" +
                           e.getMessage() + "'");
            }
        }
        else if (command == stopCommand)
        {
            // stop searching
            if (cancelPendingSearches())
            {
                setInquiryInProgress(false);
                removeCommand(stopCommand);
                addCommand(searchCommand);
            }
        }
        else if (command == exitCommand)
        {
            midlet.serviceDiscoveryListExitRequest();
        }
        else if (command == openCommand)
        {
            int size = this.size();
            boolean[] flags = new boolean[size];
            getSelectedFlags(flags);
            Vector selectedServiceRecords = new Vector();
            for (int i=0; i < size; i++)
            {
                if (flags[i])
                {
                    String key = getString(i);
                    ServiceRecord rec =
                        (ServiceRecord) matchingServiceRecords.get(key);
                    selectedServiceRecords.addElement(rec);
                }
            }

            // try to perform an open on selected items
            if (selectedServiceRecords.size() > 0)
            {
                String value = LocalDevice.getProperty(
                                   "bluetooth.connected.devices.max");
                int maxNum = Integer.parseInt(value);

                int total = numConnectionsAlreadyOpen +
                            selectedServiceRecords.size();
                if (total > maxNum)
                {
                    midlet.serviceDiscoveryListError(
                               "Too many selected. " +
                               "This device can connect to at most " +
                               maxNum + " other devices");
                }
                else
                {
                    midlet.serviceDiscoveryListOpen(
                               selectedServiceRecords);
                }
            }
            else
            {
                midlet.serviceDiscoveryListError(
                          "Select at least one to open");
            }
        }
        else if (command == backCommand)
        {
            midlet.serviceDiscoveryListBackRequest(backDisplayable);
        }
    }


    boolean cancelPendingSearches()
    {
        LogScreen.log("Cancel pending inquiries and searches\n");

        boolean everythingCancelled = true;

        if (inquiryInProgress)
        {
            // Note: The BT API (v1.0) isn't completely clear
            // whether cancelInquiry is blocking or non-blocking.

            if (discoveryAgent.cancelInquiry(this))
            {
                setInquiryInProgress(false);
            }
            else
            {
                everythingCancelled = false;
            }
        }

        for (int i=0; i < transIds.size(); i++)
        {
            // Note: The BT API (v1.0) isn't completely clear
            // whether cancelServiceSearch is blocking or
            // non-blocking?

            Integer pendingId =
                (Integer) transIds.elementAt(i);
            if (discoveryAgent.cancelServiceSearch(
                                   pendingId.intValue()))
            {
                transIds.removeElement(pendingId);
            }
            else
            {
                everythingCancelled = false;
            }
        }
        return everythingCancelled;
    }


    // DiscoveryListener callbacks

    public void deviceDiscovered(RemoteDevice remoteDevice,
                                 DeviceClass deviceClass)
    {
        LogScreen.log("deviceDiscovered: " +
                      remoteDevice.getBluetoothAddress() +
                      " major device class=" +
                      deviceClass.getMajorDeviceClass() +
                      " minor device class=" +
                      deviceClass.getMinorDeviceClass() + "\n");

        // Note: The following check has the effect of only
        // performing later service searches on phones.
        // If you intend to run the MIDlet on some other device (e.g.
        // handheld computer, PDA, etc. you have to add a check
        // for them as well.) You might also refine the check
        // using getMinorDeviceClass() to check only cellular phones.
        boolean isPhone =
            (deviceClass.getMajorDeviceClass() == 0x200);

        // Setting the following line to 'true' is a workaround
        // for some early beta SDK device emulators. Set it
        // to false when compiling the MIDlet for download to
        // real MIDP phones!
        boolean isEmulator = true; //false;

        if (isPhone || isEmulator)
        {
            unsearchedRemoteDevices.addElement(remoteDevice);
        }
    }


    public void inquiryCompleted(int discoveryType)
    {
        LogScreen.log("inquiryCompleted: " +
                      discoveryType + "\n");

        setInquiryInProgress(false);

        if (unsearchedRemoteDevices.size() == 0)
        {
            setTitle(TITLE);

            addCommand(searchCommand);
            removeCommand(stopCommand);

            midlet.serviceDiscoveryListError(
                       "No Bluetooth devices found");
        }
    }


    public void servicesDiscovered(int transId,
                                   ServiceRecord[] serviceRecords)
    {
        LogScreen.log("servicesDiscovered: transId=" +
                      transId +
                      " # serviceRecords=" +
                      serviceRecords.length + "\n");

        // Remove+Add: ensure there is at most one open command
        removeCommand(openCommand);
        addCommand(openCommand);

        // Use the friendly name and/or bluetooth address
        // to identify the matching Device + ServiceRecord
        // (Note: Devices with different Bluetooth addresses
        // might have the same friendly name, for example a
        // device's default friendly name.)

        // there should only be one record
        if (serviceRecords.length == 1)
        {
            RemoteDevice device = serviceRecords[0].getHostDevice();
            String name = device.getBluetoothAddress();

            // This MIDlet only uses the first matching service
            // record found for a particular device.
            if (!matchingServiceRecords.containsKey(name))
            {
                matchingServiceRecords.put(name, serviceRecords[0]);
                append(name, null);

                // The List should have at least one entry,
                // before we add an open command.
                if (size() == 1)
                {
                    addCommand(openCommand);
                }
            }
        }
        else
        {
            midlet.serviceDiscoveryListError(
                       "Error: Unexpected number (" +
                       serviceRecords.length + ") of service records " +
                       "in servicesDiscovered callback, transId=" +
                       transId);
        }
    }


    public void serviceSearchCompleted(int transId, int responseCode)
    {
        setTitle("New search");

        String responseCodeString =
            LogScreen.responseCodeString(responseCode);
        LogScreen.log("serviceSearchCompleted: transId=" +
                    transId +
                    " (" +
                    responseCodeString + ")\n");

        // For any responseCode, decrement the counter
        numServiceSearchesInProgress--;

        // remove the transaction id from the pending list
        for (int i=0; i < transIds.size(); i++)
        {
            Integer pendingId = (Integer) transIds.elementAt(i);
            if (pendingId.intValue() == transId)
            {
                transIds.removeElement(pendingId);
                break;
            }
        }

        // all the searches have completed
        if (!inquiryInProgress && (transIds.size() == 0))
        {
           removeCommand(stopCommand);
           addCommand(searchCommand);

           if (matchingServiceRecords.size() == 0)
           {
                midlet.serviceDiscoveryListError(
                           "No matching services found");
           }
        }
    }


    // Interface Runnable
    public void run()
    {
        Thread currentThread = Thread.currentThread();
        int i = 0;

    running:
        while (thread == currentThread)
        {
            synchronized (this)
            {
                if (thread != currentThread)
                {
                    break running;
                }
                else if (!inquiryInProgress)
                {
                    doServiceSearch();
                }

                if (inquiryInProgress ||
                    numServiceSearchesInProgress > 0)
                {
                    setTitle("Searching " + ACTIVITY[i]);
                    if (++i >= ACTIVITY.length)
                    {
                        i = 0;
                    }
                }

                try
                {
                    wait(WAIT_MILLIS);
                }
                catch (InterruptedException e)
                {
                    // we can safely ignore this
                }
            }
        }
    }


    public void doServiceSearch()
    {
        if ((unsearchedRemoteDevices.size() > 0) &&
            (numServiceSearchesInProgress < sdTransMax))
        {
            synchronized(this)
            {
                RemoteDevice device =
                    (RemoteDevice) unsearchedRemoteDevices
                                       .elementAt(0);

                UUID[] uuids = new UUID[1];
                uuids[0] = uuid;
                try
                {
                    int[] attrSet = null; // default attrSet

                    numServiceSearchesInProgress++;

                    int transId = discoveryAgent.searchServices(
                                                     attrSet,
                                                     uuids,
                                                     device,
                                                     this);

                    LogScreen.log("starting service search on device=" +
                                  device.getBluetoothAddress() +
                                  " transId=" + transId + "\n");

                    transIds.addElement(new Integer(transId));
                    unsearchedRemoteDevices.removeElementAt(0);
                }
                catch (BluetoothStateException e)
                {
                    numServiceSearchesInProgress--;

                    midlet.serviceDiscoveryListError(
                               "Error, could not perform " +
                               "service search: '" +
                               e.getMessage());
                }
            }
        }
    }


    public void remove(Vector alreadyOpen)
    {
        if (size() > 0)
        {
            for (int i=0; i < alreadyOpen.size(); i++)
            {
                // Bluetooth address of slave device that
                // we already have a connection open to
                String btAddress = (String) alreadyOpen.elementAt(i);

                boolean found = false;
                for (int j = 0; j < size(); j++)
                {
                    if (getString(j).equals(btAddress))
                    {
                        found = true;

                        // if the element we are about to
                        // remove is selected, select something else
                        if (getSelectedIndex() == j)
                        {
                            setSelectedIndex(j, false);
                        }
                        delete(j);
                        break;
                    }
                }
                if (found)
                {
                    break;
                }
            }
        }
    }
}

SettingsList

package example.btsppecho;


import javax.microedition.lcdui.Command;
import javax.microedition.lcdui.CommandListener;
import javax.microedition.lcdui.Displayable;
import javax.microedition.lcdui.List;

import javax.bluetooth.DiscoveryAgent;


class SettingsList
     extends List
     implements CommandListener
{
    // UI strings
    private static final String SERVER = "Server";
    private static final String CLIENT = "Client";
    // (I abbreviated the strings "Authentication",
    // "Authorization" and "Encryption" because they are a
    // bit long on some MIDP device's List items. Another
    // MIDlet might hard code its preference anyways.)
    private static final String AUTHENTICATION_TRUE =
                                "Authen.: true";
    private static final String AUTHENTICATION_FALSE =
                                "Authen.: false";
    private static final String AUTHORIZATION_TRUE =
                                "Authoriz.: true";
    private static final String AUTHORIZATION_FALSE =
                                "Authoriz.: false";
    private static final String ENCRYPTION_TRUE =
                                "Encrypt: true";
    private static final String ENCRYPTION_FALSE =
                                "Encrypt: false";
    private static final String RETRIEVE_DEVICES_TRUE =
                                "Use known devices";
    private static final String RETRIEVE_DEVICES_FALSE =
                                "Use inquiry";
    private static final String INQUIRY_TYPE_LIAC = "LIAC";
    private static final String INQUIRY_TYPE_GIAC = "GIAC";
    private static final String INQUIRY_TYPE_NOT_DISCOVERABLE =
                                "not discoverable";
    private static final String INQUIRY_TYPE_CACHED = "cached";
    private static final String INQUIRY_TYPE_PREKNOWN = "preknown";

    // settings
    private int inquiryType;
    private int protocol;
    private boolean isServer;
    private boolean useAuthorization;  // client-only
    private boolean useAuthentication;
    // when useAuthenication is false, useEncryption is also false
    private boolean useEncryption;

    // MIDlet stuff
    private final MIDletApplication midlet;
    private final Command startCommand;
    private final Command propCommand;
    private final Command exitCommand;


    SettingsList(MIDletApplication midlet)
    {
        super("Settings", List.IMPLICIT);
        this.midlet = midlet;

        // default setting values


        // You should think about what is the preferred
        // default inquiry type used to make your service
        // discoverable. This MIDlet uses LIAC as the default,
        // but you might want to use GIAC.
        inquiryType = DiscoveryAgent.LIAC;

        isServer = false; // client by default
        useAuthentication = false;
        useEncryption = false; // false when auth. not used
        useAuthorization = false; // false when auth. not used
        updateListElements();

        // add screen commands
        startCommand = new Command("Start application",
                                   Command.SCREEN,
                                   0);
        propCommand = new Command("BT properties",
                                  Command.SCREEN,
                                  1);
        exitCommand = new Command("Exit", Command.EXIT, 0);
        addCommand(startCommand);
        addCommand(propCommand);
        addCommand(exitCommand);
        setCommandListener(this);
    }


    private void updateListElements()
    {
        // remove all old list items
        while(size() > 0)
        {
            delete(0);
        }

        // Index 0: Server / Client
        String string;
        if (isServer)
        {
            string = SERVER;
        }
        else
        {
            string = CLIENT;
        }
        append(string, null);

        // Index 3: LIAC / GIAC
        if (inquiryType == DiscoveryAgent.LIAC)
        {
            append(makeInquiryLabel(isServer, INQUIRY_TYPE_LIAC),
                   null);
        }
        else if (inquiryType == DiscoveryAgent.GIAC)
        {
            append(makeInquiryLabel(isServer, INQUIRY_TYPE_GIAC),
                   null);
        }
        else if (inquiryType == DiscoveryAgent.PREKNOWN)
        {
            append(makeInquiryLabel(isServer,
                                    INQUIRY_TYPE_PREKNOWN),
                   null);
        }
        else if (inquiryType == DiscoveryAgent.CACHED)
        {
            append(makeInquiryLabel(isServer, INQUIRY_TYPE_CACHED),
                   null);
        }
        else if (inquiryType == DiscoveryAgent.NOT_DISCOVERABLE)
        {
            append(makeInquiryLabel(isServer, INQUIRY_TYPE_CACHED),
                   null);
        }


        // Index 2: use authentication true / false
        //          (encryption and authorization can only be
        //           used if authentication is used)
        if (useAuthentication)
        {
            append(AUTHENTICATION_TRUE, null);

            // Index 3: use encryption true / false
            if (useEncryption)
            {
                append(ENCRYPTION_TRUE, null);
            }
            else
            {
               append(ENCRYPTION_FALSE, null);
            }

            // Index 4: ConnectionService only : use auth. true / false
            if (!isServer)
            {
                if (useAuthorization)
                {
                    append(AUTHORIZATION_TRUE, null);
                }
                else
                {
                    append(AUTHORIZATION_FALSE, null);
                }
            }
        }
        else
        {
            useAuthentication = false;
            useEncryption = false;

            append(AUTHENTICATION_FALSE, null);
        }
    }


    private String makeInquiryLabel(boolean searching, String string)
    {
        if (searching)
        {
            // we are searching
            return "Discover: " + string;
        }
        else
        {
            // we will be searched for
            return "Discoverable: " + string;
        }
    }


    public void commandAction(Command command, Displayable d)
    {
        if (command == startCommand)
        {
            midlet.settingsListStart(isServer,
                                     inquiryType,
                                     useAuthentication,
                                     useAuthorization,
                                     useEncryption);
        }
        else if (command == propCommand)
        {
            midlet.settingsListPropertiesRequest();
        }
        else if (command == exitCommand)
        {
            midlet.settingsListExitRequest();
        }
        else if (command == List.SELECT_COMMAND)
        {
            int index = getSelectedIndex();
            switch(index)
            {
                // Index 0: "Server client" (isServer=true) or
                //          "Client server" (isServer=false)
                case 0:
                  isServer = !isServer;
                  break;


                // Index 1: "Discovery mode: LIAC" or
                //          "Discovery mode: GIAC"
                case 1:
                    // toggle between LIAC and GIAC
                    if (inquiryType == DiscoveryAgent.LIAC)
                    {
                       inquiryType = DiscoveryAgent.GIAC;
                    }
                    else
                    {
                        inquiryType = DiscoveryAgent.LIAC;
                    }
                    break;


                // Index 2: "Authentication: true" or
                //          "Authentication: false"
                case 2:
                    // toggle
                    useAuthentication = !useAuthentication;
                    if (!useAuthentication)
                    {
                        // Authorization and encryption are only
                        // settable if authentication is true, otherwise
                        // they are false and we should remove them.
                        // (The order of removal is important.)

                        // Only a client has this setting option
                        // and not a server, thus the size check.
                        if (size() == 5)
                        {
                            delete(4); // remove authorization from List
                            useAuthorization = false;
                        }

                        this.delete(3); // remove encryption from List
                        useEncryption = false;
                    }
                    break;


                // Index 3: "Encryption: true" or
                //          "Encryption: false"
                case 3:
                    useEncryption = !useEncryption;  // toggle
                    break;


                // Index 4: "Authorization: true" or
                //          "Authorization: false"
                case 4:
                    // toggle
                    useAuthorization = !useAuthorization;
                    break;

            }
            updateListElements();
            setSelectedIndex(index, true);
        }
    }
}

TextScreen

package example.btsppecho;


import javax.microedition.lcdui.*;


// A closeable screen for displaying text.

class TextScreen
    extends Form
    implements CommandListener
{
    private final MIDletApplication midlet;
    private final Displayable next;


    TextScreen(MIDletApplication midlet,
               Displayable next,
               String title,
               String text,
               String closeLabel)
    {
        super(title);
        this.midlet = midlet;
        this.next = next;
        append(text);
        addCommand(new Command(closeLabel, Command.BACK, 1));
        setCommandListener(this);
    }


    public void commandAction(Command c, Displayable d)
    {
        // The application code only adds a 'close' command.
        midlet.textScreenClosed(next);
    }
}

ErrorScreen

package example.btsppecho;


import javax.microedition.lcdui.Alert;
import javax.microedition.lcdui.AlertType;
import javax.microedition.lcdui.Display;
import javax.microedition.lcdui.Displayable;
import javax.microedition.lcdui.Image;

import java.util.Timer;
import java.util.TimerTask;


class ErrorScreen
    extends Alert
{
    private static Image image;
    private static Display display;
    private static ErrorScreen instance = null;


    private ErrorScreen()
    {
        super("Error");
        setType(AlertType.ERROR);
        setTimeout(2000);
        setImage(image);
    }


    static void init(Image img, Display disp)
    {
        image = img;
        display = disp;
    }


    static void showError(String message, Displayable next)
    {
        if (instance == null)
        {
            instance = new ErrorScreen();
        }
        if (message == null)
        {
            message = "";
        }

        instance.setString(message);
        display.setCurrent(instance, next);
    }
}

ClientConnectionHandler

package example.btsppecho.client;


import java.io.InputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Hashtable;
import java.util.Enumeration;
import java.util.Vector;

import javax.microedition.io.StreamConnection;
import javax.microedition.io.StreamConnectionNotifier;


public class ClientConnectionHandler
    implements Runnable
{
    private final static byte ZERO = (byte) '0';
    private final static int LENGTH_MAX_DIGITS = 5;

    // this is an arbitrarily chosen value:
    private final static int MAX_MESSAGE_LENGTH =
                             65536 - LENGTH_MAX_DIGITS;

    private final ConnectionService ConnectionService;
    private final ClientConnectionHandlerListener listener;
    private final Hashtable sendMessages = new Hashtable();

    private StreamConnection connection;
    private InputStream in;
    private OutputStream out;

    private volatile boolean aborting;


    public ClientConnectionHandler(
        ConnectionService ConnectionService,
        StreamConnection connection,
        ClientConnectionHandlerListener listener)
    {
        this.ConnectionService = ConnectionService;
        this.connection = connection;
        this.listener = listener;
        aborting = false;
        in = null;
        out = null;

        // Our caller uses method 'start' to start the reader
        // and writer. (This allows the ConnectionService a
        // chance to add us to its list of handlers before
        // the reader and writer start running.)
    }


    ClientConnectionHandlerListener getListener()
    {
        return listener;
    }


    public synchronized void start()
    {
        Thread thread = new Thread(this);
        thread.start();
    }


    public void close()
    {
        if (!aborting)
        {
            synchronized(this)
            {
                aborting = true;
            }

            synchronized(sendMessages)
            {
                sendMessages.notify();
            }

            if (out != null)
            {
                try
                {
                    out.close();
                    synchronized (this)
                    {
                        out = null;
                    }
                }
                catch(IOException e)
                {
                    // there is nothing we can do: ignore it
                }
            }

            if (in != null)
            {
                try
                {
                    in.close();
                    synchronized (this)
                    {
                        in = null;
                    }
                }
                catch(IOException e)
                {
                    // there is nothing we can do: ignore it
                }
            }

            if (connection != null)
            {
                try
                {
                    connection.close();
                    synchronized (this)
                    {
                        connection = null;
                    }
                }
                catch (IOException e)
                {
                    // there is nothing we can do: ignore it
                }
            }
        }
    }


    public void queueMessageForSending(Integer id, byte[] data)
    {
        if (data.length > MAX_MESSAGE_LENGTH)
        {
            throw new IllegalArgumentException(
                          "Message too long: limit is " +
                          MAX_MESSAGE_LENGTH + " bytes");
        }

        synchronized(sendMessages)
        {
            sendMessages.put(id, data);
            sendMessages.notify();
        }
    }


    private void sendMessage(byte[] data)
        throws IOException
    {
        byte[] buf = new byte[LENGTH_MAX_DIGITS + data.length];
        writeLength(data.length, buf);
        System.arraycopy(data,
                         0,
                         buf,
                         LENGTH_MAX_DIGITS,
                         data.length);
        out.write(buf);
        out.flush();
    }


    public void run()
    {
        // the reader

        // 1. open the streams, start the writer
        try
        {
            in = connection.openInputStream();
            out = connection.openOutputStream();

            // start the writer
            Writer writer = new Writer(this);
            Thread writeThread = new Thread(writer);
            writeThread.start();

            listener.handleStreamsOpen(this);
        }
        catch(IOException e)
        {
            // open failed: close any connections/streams and
            // inform listener that the open failed

            close(); // also tells listener to delete handler

            listener.handleStreamsOpenError(this, e.getMessage());
            return;
        }


        // 2. wait to receive and read messages
        while (!aborting)
        {
            int length = 0;
            try
            {
                byte[] lengthBuf = new byte[LENGTH_MAX_DIGITS];
                readFully(in, lengthBuf);
                length = readLength(lengthBuf);
                byte[] temp = new byte[length];
                readFully(in, temp);

                listener.handleReceivedMessage(this, temp);
            }
            catch (IOException e)
            {
                close();
                if (length == 0)
                {
                   listener.handleClose(this);
                }
                else
                {
                   // we were in the middle of reading...
                   listener.handleErrorClose(this, e.getMessage());
                }
            }
        }
    }


    private static void readFully(InputStream in, byte[] buffer)
        throws IOException
    {
        int bytesRead = 0;

        while (bytesRead < buffer.length)
        {
            int count = in.read(buffer,
                                bytesRead,
                                buffer.length - bytesRead);

            if (count == -1)
            {
                throw new IOException("Input stream closed");
            }
            bytesRead += count;
        }
    }


    private static int readLength(byte[] buffer)
    {
        int value = 0;

        for (int i = 0; i < LENGTH_MAX_DIGITS; ++i)
        {
            value *= 10;
            value += buffer[i] - ZERO;
        }
        return value;
    }


    private void sendMessage(OutputStream out, byte[] data)
        throws IOException
    {
        if (data.length > MAX_MESSAGE_LENGTH)
        {
            throw new IllegalArgumentException(
                          "Message too long: limit is: " +
                          MAX_MESSAGE_LENGTH + " bytes");
        }
        byte[] buf = new byte[LENGTH_MAX_DIGITS + data.length];
        writeLength(data.length, buf);
        System.arraycopy(data, 0, buf, LENGTH_MAX_DIGITS, data.length);
        out.write(buf);
        out.flush();
    }


    private static void writeLength(int value, byte[] buffer)
    {
        for (int i = LENGTH_MAX_DIGITS -1; i >= 0; --i)
        {
            buffer[i] = (byte) (ZERO + value % 10);
            value = value / 10;
        }
    }


    private class Writer
        implements Runnable
    {
        private final ClientConnectionHandler handler;


        Writer(ClientConnectionHandler handler)
        {
            this.handler = handler;
        }


        public void run()
        {
            while (!aborting)
            {
                synchronized(sendMessages)
                {
                    Enumeration e = sendMessages.keys();
                    if (e.hasMoreElements())
                    {
                        // send any pending messages
                        Integer id = (Integer) e.nextElement();
                        byte[] sendData = (byte[]) sendMessages.get(id);
                        try
                        {
                            sendMessage(out, sendData);

                            // remove sent message from queue
                            sendMessages.remove(id);

                            // inform listener that it was sent
                            listener.handleQueuedMessageWasSent(
                                         handler,
                                         id);
                        }
                        catch (IOException ex)
                        {
                            close(); // stop the networking thread

                            // inform that we got an error close
                            listener.handleErrorClose(
                                         handler,
                                         ex.getMessage());
                        }
                    }

                    if (sendMessages.isEmpty())
                    {
                        try
                        {
                            sendMessages.wait();
                        }
                        catch (InterruptedException ex)
                        {
                            // this can't happen in MIDP: ignore it
                        }
                    }
                }
            }
        }
    }
}

ClientConnectionHandlerListener

package example.btsppecho.client;


public interface ClientConnectionHandlerListener
{
    // The handler's accept and open of a new connection
    // has occurred, but the I/O streams are not yet open.
    // The I/O streams must be open to send or receive
    // messages.
    public void handleAcceptAndOpen(ClientConnectionHandler handler);


    // The handler's I/O streams are now open and the
    // handler can now be used to send and receive messages.
    public void handleStreamsOpen(ClientConnectionHandler handler);


    // Opening of the handler's I/O streams failed. The handler has
    // closed any connections or streams that were open.
    // The handler should not be used anymore,
    // and should be discarded.
    public void handleStreamsOpenError(ClientConnectionHandler handler,
                                       String errorMessage);


    // The handler got an inbound message.
    public void handleReceivedMessage(
                    ClientConnectionHandler handler,
                    byte[] messageBytes);


    // A message that had been previously queued for sending
    // (identified by id) by the handler, has been sent successfully.
    public void handleQueuedMessageWasSent(
                    ClientConnectionHandler handler,
                    Integer id);


    // The handler has closed its connections and streams.
    // The handler should not be used anymore, and should be discarded.
    // Only handlers which have previously called 'handleOpen' may
    // call 'handleClose', and only just once.
    public void handleClose(ClientConnectionHandler handler);


    // An error related to the handler occurred. The handler
    // has closed the connection, and the handler should no
    // longer be used.
    public void handleErrorClose(ClientConnectionHandler handler,
                                 String errorMessage);

}

ConnectionService

package example.btsppecho.client;


import java.io.IOException;
import javax.microedition.io.ConnectionNotFoundException;
import javax.microedition.io.Connector;
import javax.microedition.io.StreamConnection;
import javax.microedition.io.StreamConnectionNotifier;

import example.btsppecho.ClientForm;
import example.btsppecho.LogScreen;

public class ConnectionService
    implements Runnable
{
    private final ClientForm listener;
    private final String url;

    private StreamConnectionNotifier connectionNotifier = null;
    private volatile boolean aborting;


    public ConnectionService(String url,
                             ClientForm listener)
    {
        this.url = url;
        this.listener = listener;

        LogScreen.log("ConnectionService: waiting to " +
                      "accept connections on '" +
                      url + "'\n");

        // start waiting for a connection
        Thread thread = new Thread(this);
        thread.start();
    }


    public String getClientURL()
    {
        return url;
    }


    public void close()
    {
        if (!aborting)
        {
            synchronized(this)
            {
                aborting = true;
            }

            // Ideally, one might want to give the run method's
            // loop a chance to abort before calling the
            // subsequent close, but the run loop is anyways
            // likely to be sitting on the acceptAndOpen
            // (i.e. blocked).

            try
            {
                connectionNotifier.close();
            }
            catch (IOException e)
            {
                // There is nothing very useful that
                // we can do for this case.
            }
        }
    }


    public void run()
    {
        aborting = false;

        try
        {
            connectionNotifier =
                (StreamConnectionNotifier) Connector.open(url);

            // It might useful in some cases to add a service to the
            // 'Public Browse Group'. For example by doing something
            // approximately as follows:
            // -----------------------------------------------------
            // Retrieve the service record template
            // LocalDevice ld = LocalDevice.getLocalDevice();
            // ServiceRecord rec = ld.getRecord(connectionNotifier);
            // DataElement element =
            //             new DataElement(DataElement.DATSEQ);
            //
            // The service class for PublicBrowseGroup (0x1002)
            // is defined in the Bluetooth Assigned Numbers document.
            // element.addElement(new DataElement(DataElement.UUID,
            //                                    new UUID(0x1002)));
            //
            // Add to the public browse group:
            // rec.setAttributeValue(0x0005, element);
            // -----------------------------------------------------
        }
        catch (IOException e)
        {
            // ConnectionNotFoundException is an IOException
            String errorMessage =
                   "Error while starting ConnectionService: " +
                   e.getMessage();

            listener.handleError(null, errorMessage);

            aborting = true;
        }
        catch (SecurityException e)
        {
            String errorMessage =
                "SecurityException while starting ConnectionService: " +
            e.getMessage();

            listener.handleError(null, errorMessage);

            aborting = true;
        }

        while (!aborting)
        {
            try
            {
                // 1. wait to accept & open a new connection
                StreamConnection connection =
                    (StreamConnection)
                    connectionNotifier.acceptAndOpen();

                LogScreen.log("ConnectionService: new connection\n");

                // 2. create a handler to take care of
                // the new connection and inform
                // the listener
                if (!aborting)
                {
                    ClientConnectionHandler handler =
                        new ClientConnectionHandler(this,
                                                    connection,
                                                    listener);
                    listener.handleAcceptAndOpen(handler);
                }

                // One could consider exiting the
                // ConnectionService when the Client
                // reaches the maximum number of allowed
                // open connections. In that case (i.e.
                // when the maximum number of possible
                // connections is already open), the
                // ConnectionService will not be able
                // to accept any new connections and one
                // might possibly want to consider whether
                // or not the ConnectionService thread
                // could then be terminated.
                //
                // However, existing connections can also
                // be disconnected (e.g. the Server is
                // terminated or closes some/all of its
                // existing connections). In that case,
                // one may want to keep the
                // ConnectionService alive and running:
                // in order to accept later new connections
                // without the need to restart the
                // ConnectionService or MIDlet.
                //
                // (This MIDlet uses the latter approach.)
            }
            catch (IOException e)
            {
                if (!aborting)
                {
                    String errorMessage =
                               "IOException occurred during " +
                               "accept and open: " +
                               e.getMessage();

                    listener.handleError(null, errorMessage);
                }
            }
            catch (SecurityException e)
            {
                if (!aborting)
                {
                    String errorMessage =
                               "IOException occurred during " +
                               "accept and open: " +
                               e.getMessage();

                    listener.handleError(null, errorMessage);
                }
            }
        }
    }
}

ServerConnectionHandler

package example.btsppecho.server;


import java.io.InputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Hashtable;
import java.util.Enumeration;
import javax.bluetooth.ServiceRecord;
import javax.microedition.io.Connector;
import javax.microedition.io.StreamConnection;
import javax.microedition.io.StreamConnectionNotifier;

import example.btsppecho.MIDletApplication;
import example.btsppecho.LogScreen;


public class ServerConnectionHandler
    implements Runnable
{
    private final static byte ZERO = (byte) '0';
    private final static int LENGTH_MAX_DIGITS = 5;

    // this is an arbitrarily chosen value:
    private final static int MAX_MESSAGE_LENGTH =
                             65536 - LENGTH_MAX_DIGITS;

    private final ServiceRecord serviceRecord;
    private final int requiredSecurity;
    private final ServerConnectionHandlerListener listener;
    private final Hashtable sendMessages = new Hashtable();

    private StreamConnection connection;
    private OutputStream out;
    private InputStream in;
    private volatile boolean aborting;
    private Writer writer;


    public ServerConnectionHandler(
               ServerConnectionHandlerListener listener,
               ServiceRecord serviceRecord,
               int requiredSecurity)
    {
        this.listener = listener;
        this.serviceRecord = serviceRecord;
        this.requiredSecurity = requiredSecurity;
        aborting = false;

        connection = null;
        out = null;
        in = null;
        listener = null;

        // the caller must call method 'start'
        // to start the reader and writer
    }


    public ServiceRecord getServiceRecord()
    {
        return serviceRecord;
    }


    public synchronized void start()
    {
        Thread thread = new Thread(this);
        thread.start();
    }


    public void close()
    {
        if (!aborting)
        {
            synchronized(this)
            {
                aborting = true;
            }

            synchronized(sendMessages)
            {
                sendMessages.notify();
            }

            if (out != null)
            {
                try
                {
                    out.close();
                    synchronized (this)
                    {
                        out = null;
                    }
                }
                catch(IOException e)
                {
                    // there is nothing we can do: ignore it
                }
            }

            if (in != null)
            {
                try
                {
                    in.close();
                    synchronized (this)
                    {
                        in = null;
                    }
                }
                catch(IOException e)
                {
                    // there is nothing we can do: ignore it
                }
            }

            if (connection != null)
            {
                try
                {
                    connection.close();
                    synchronized (this)
                    {
                        connection = null;
                    }
                }
                catch (IOException e)
                {
                    // there is nothing we can do: ignore it
                }
            }
        }
    }



    public void queueMessageForSending(Integer id, byte[] data)
    {
        if (data.length > MAX_MESSAGE_LENGTH)
        {
            throw new IllegalArgumentException(
                          "Message too long: limit is " +
                          MAX_MESSAGE_LENGTH + " bytes");
        }

        synchronized(sendMessages)
        {
            sendMessages.put(id, data);
            sendMessages.notify();
        }
    }


    private void sendMessage(byte[] data)
        throws IOException
    {
        byte[] buf = new byte[LENGTH_MAX_DIGITS + data.length];
        writeLength(data.length, buf);
        System.arraycopy(data,
                         0,
                         buf,
                         LENGTH_MAX_DIGITS,
                         data.length);
        out.write(buf);
        out.flush();
    }


    public void run()
    {
        // the reader

        // 1. open the connection and streams, start the writer
        String url = null;
        try
        {
            // 'must be master': false
            url = serviceRecord.getConnectionURL(
                                  requiredSecurity,
                                  false);

            connection = (StreamConnection) Connector.open(url);
            in = connection.openInputStream();
            out = connection.openOutputStream();

            LogScreen.log("Opened connection & streams to: '" +
                      url + "'\n");

            // start the writer
            Writer writer = new Writer(this);
            Thread writeThread = new Thread(writer);
            writeThread.start();

            LogScreen.log("Started a reader & writer for: '" +
                      url + "'\n");

            // open succeeded, inform listener
            listener.handleOpen(this);
        }
        catch(IOException e)
        {
            // open failed, close any connections/streams, and
            // inform listener that the open failed

            LogScreen.log("Failed to open " +
                          "connection or streams for '" +
                           url + "' , Error: " +
                           e.getMessage());

            close();

            listener.handleOpenError(
                         this,
                         "IOException: '" + e.getMessage() + "'");

            return;
        }
        catch (SecurityException e)
        {
            // open failed, close any connections/streams, and
            // inform listener that the open failed

            LogScreen.log("Failed to open " +
                          "connection or streams for '" +
                           url + "' , Error: " +
                           e.getMessage());

            close();

            listener.handleOpenError(
                         this,
                         "SecurityException: '" + e.getMessage() + "'");

            return;
        }

        // 2. wait to receive and read messages
        while (!aborting)
        {
            int length = 0;
            try
            {
                byte[] lengthBuf = new byte[LENGTH_MAX_DIGITS];
                readFully(in, lengthBuf);
                length = readLength(lengthBuf);
                byte[] temp = new byte[length];
                readFully(in, temp);

                listener.handleReceivedMessage(this, temp);
            }
            catch (IOException e)
            {
                close();
                if (length == 0)
                {
                   listener.handleClose(this);
                }
                else
                {
                   // we were in the middle of reading...
                   listener.handleErrorClose(this, e.getMessage());
                }
            }
        }
    }


    private static void readFully(InputStream in, byte[] buffer)
        throws IOException
    {
        int bytesRead = 0;

        while (bytesRead < buffer.length)
        {
            int count = in.read(buffer,
                                bytesRead,
                                buffer.length - bytesRead);

            if (count == -1)
            {
                throw new IOException("Input stream closed");
            }
            bytesRead += count;
        }
    }


    private static int readLength(byte[] buffer)
    {
        int value = 0;

        for (int i = 0; i < LENGTH_MAX_DIGITS; ++i)
        {
            value *= 10;
            value += buffer[i] - ZERO;
        }
        return value;
    }


    private void sendMessage(OutputStream out, byte[] data)
        throws IOException
    {
        if (data.length > MAX_MESSAGE_LENGTH)
        {
            throw new IllegalArgumentException(
                          "Message too long: limit is: " +
                          MAX_MESSAGE_LENGTH + " bytes");
        }
        byte[] buf = new byte[LENGTH_MAX_DIGITS + data.length];
        writeLength(data.length, buf);
        System.arraycopy(data,
                         0,
                         buf,
                         LENGTH_MAX_DIGITS,
                         data.length);
        out.write(buf);
        out.flush();
    }


    private static void writeLength(int value, byte[] buffer)
    {
        for (int i = LENGTH_MAX_DIGITS -1; i >= 0; --i)
        {
            buffer[i] = (byte) (ZERO + value % 10);
            value = value / 10;
        }
    }


    private class Writer
        implements Runnable
    {
        private final ServerConnectionHandler handler;


        Writer(ServerConnectionHandler handler)
        {
            this.handler = handler;
        }


        public void run()
        {
            while (!aborting)
            {
                synchronized(sendMessages)
                {
                    Enumeration e = sendMessages.keys();
                    if (e.hasMoreElements())
                    {
                        // send any pending messages
                        Integer id = (Integer) e.nextElement();
                        byte[] sendData =
                               (byte[]) sendMessages.get(id);
                        try
                        {
                            sendMessage(out, sendData);

                            // remove sent message from queue
                            sendMessages.remove(id);

                            // inform listener that it was sent
                            listener.handleQueuedMessageWasSent(
                                         handler,
                                         id);
                        }
                        catch (IOException ex)
                        {
                            close(); // stop the networking thread

                            // inform that we got an error close
                            listener.handleErrorClose(handler,
                                                      ex.getMessage());
                        }
                    }

                    if (sendMessages.isEmpty())
                    {
                        try
                        {
                            sendMessages.wait();
                        }
                        catch (InterruptedException ex)
                        {
                            // this can't happen in MIDP: ignore it
                        }
                    }
                }
            }
        }
    }
}

ServerConnectionHandlerListener

package example.btsppecho.server;


public interface ServerConnectionHandlerListener
{
    // The handler's open succeeded. It can now be used for sending
    // and receiving messages.
    public void handleOpen(ServerConnectionHandler handler);


    // The hadler's open failed. It has closed any connections or
    // streams that were open. The handler should not be used anymore,
    // and should be discarded.
    public void handleOpenError(ServerConnectionHandler handler,
                                String errorMessage);


    // The handler got an inbound message.
    public void handleReceivedMessage(
                    ServerConnectionHandler handler,
                    byte[] messageBytes);


    // A message that had been previously queued for sending
    // (identified by id) by the handler, has been sent successfully.
    public void handleQueuedMessageWasSent(
                    ServerConnectionHandler handler,
                    Integer id);


    // The handler has closed its connections and streams.
    // The handler should not be used anymore, and should be discarded.
    // Only handlers which have previously called 'handleOpen' may
    // call 'handleClose', and only just once.
    public void handleClose(ServerConnectionHandler handler);


    // An error related to the handler occurred. The handler
    // has closed the connection, and the handler should no
    // longer be used.
    public void handleErrorClose(ServerConnectionHandler handler,
                                 String errorMessage);

}