Implementing the UI elements

SettingsList class

This is the first screen displayed by the MIDlet. It is used to make a few settings and then start the MIDlet. The settings are as follows:

  • An option to select either client or server mode

  • The inquiry type used to be discoverable by a client, or when performing discovery by a server

  • Use of authentication: true/false

  • If authentication is used:

    • Use of encryption: true/false

    • Use of authorization: true/false

The Start command is used to start the MIDlet in the appropriate role and using the appropriate settings. This is done via an appropriate callback to the MIDletApplication, since it manages the screen transitions.

There is also a Bluetooth Properties command to examine certain system properties related to use of Bluetooth. For example, a Bluetooth device might only allow a limited number of other devices to be connected, etc.

To create the class:

  1. Create the SettingsList class file.

  2. Assign the class to the package example.btsppecho and import the required classes.

    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;
  3. Implement the functionality of the class.

    For more information on developing user interfaces, see UI and Graphics.

    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
    
    
    
  4. Use the DiscoveryAgent class to set the inquiry type. The inquiry type is used to be discoverable by a client, or when performing discovery by a server.

    Note: The S60 3rd Edition FP 1 implementation of the Bluetooth API does not support using LIAC because the Symbian Bluetooth stack does not support the operation. Also changing the discovery mode is not supported at all.

            // 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 class

This is a simple read-only text screen. It has just one command (for example, Back), which is used to transition to the next appropriate UI screen state via an appropriate MIDletApplication callback.

To create the class:

  1. Create the TextScreen class file.

  2. Assign the class to the package example.btsppecho and import the required classes.

    package example.btsppecho;
    
    import javax.microedition.lcdui.*;
  3. Implement the functionality of the class.

    // 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);
        }
    }

LogScreen class

It can be useful to give end users a way to follow the progress of the device inquiry and service discovery phases.

The target end users for this example MIDlet are developers learning to use the Java APIs For Bluetooth. A log screen is used to follow the progress of these phases if desired. It is also a helpful debugging aid for these somewhat more sophisticated end users.

To create the class:

  1. Create the LogScreen class file.

  2. Assign the class to the package example.btsppecho and import the required classes.

    package example.btsppecho;
    
    
    import java.util.Vector;
    import javax.microedition.lcdui.*;
    
    import javax.bluetooth.DiscoveryAgent;
    import javax.bluetooth.DiscoveryListener;
  3. Implement the basic functionality of the class.

    
    // 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();
            }
        }
  4. Use DiscoveryAgent and the DiscoveryListener classes for the helper methods.

    
        // 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;
        }
    }

ClientForm class

This screen is the main screen of the MIDlet when it is run in client mode. It displays the number of open connections, status strings as connections are created/deleted or as messages are sent, and simple text messages as they are received from a remote server.

To create the class:

  1. Create the ClientForm class file.

  2. Assign the class to the package example.btsppecho and import the required classes.

    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;
  3. Implement the functionality of the class.

    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 + "\"");
        }
    }

ServiceDiscoveryList class

When the MIDlet is run in server mode, it must first search for suitable clients to create connections to. This screen is used to perform device inquiry, service discovery, and open connections to selected clients. After the connections are opened, the screen map is changed (via a callback to the MIDlet) to the ServerForm screen.

The ServiceDiscoveryList class uses the setTitle method to dynamically modify the screen’s title string during device inquiry and service to indicate to the end user that some longer term activity is taking place. This approach for providing an ‘activity indicator’ was used to keep the MIDlet code relatively simple. Your MIDlet may want to use a separate transient canvas screen to graphically indicate that an activity (which may take some time to complete) is taking place. The amount of time that device inquiry and service search need to complete depends on how many Bluetooth devices are within range, and on the number of those which might be, and/or actually are, running the service. You are likely to want to test your Bluetooth MIDlets in locations where there are just a few nearby Bluetooth devices, and also in locations where there are many Bluetooth devices within range (for example, dozens of devices).

To create the class:

  1. Create the ServiceDiscoveryList class file.

  2. Assign the class to the package example.btsppecho and import the required classes.

    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;
  3. Use the getProperty method of the LocalDevice class with the property bluetooth.sd.trans.max to request for the maximum number of concurrent service discovery transactions.

    To create the discovery agent, use the getLocalDevice and getDiscoveryAgent methods of the LocalDevice class.

    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;
        }
    
    
    
  4. Implement the command action functionality.

        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);
                }
    
    

    Use the setDiscoverable(DiscoveryAgent.NOT_DISCOVERABLE) method to take the device out of discoverable mode. Then disable inquiries.

                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);
                    }
                }
    
    

    Use the LocalDevice.getProperty() method with the bluetooth.connected.devices.max property to request the maximum number of connected devices supported.

                // 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;
        }
    
    
    
  5. Implement discovery listener callbacks.

    To implement the callbacks, use the following classes:

        // 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;
                    }
                }
            }
        }
    }

ServerForm class

This is the main UI screen of a connected MIDlet when run in server mode. It is used to receive messages from a remote client and echo those back to all connected clients. This allows the application to be used a very simple chat-like server. This screen shows the number of connected clients. If one client disconnects, it doesn’t affect the connectivity between the server and other clients.

To create the class:

  1. Create the ServerForm class file.

  2. Assign the class to the package example.btsppecho and import the required classes.

    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;
  3. Add Form items, create commands, add the commands, and set a command listener.

    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);
        }
    
    
    
  4. Implement connections.

        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.
        }
    
    
    
  5. Implement ServerConnectionHandlerListener interface methods.

        // 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 + "'");
        }
    }