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:
Create the SettingsList
class
file.
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;
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
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); } } }
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:
Create the TextScreen
class
file.
Assign the
class to the package example.btsppecho
and import the
required classes.
package example.btsppecho; import javax.microedition.lcdui.*;
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); } }
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:
Create the LogScreen
class
file.
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;
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(); } }
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; } }
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:
Create the ClientForm
class
file.
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;
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 + "\""); } }
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:
Create the ServiceDiscoveryList
class
file.
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;
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; }
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; }
Implement discovery listener callbacks.
To implement the callbacks, use the following classes:
The RemoteDevice
class
is used for representing a remote device. The method getBluetoothAddress
returns
the Bluetooth address of the remote device.
DeviceClass
class
represents the class of device (CoD) record. The method getMajorDeviceClass
returns
the major device class.
ServiceRecord
class
represents the characteristics of a Bluetooth service.
// 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; } } } } }
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:
Create the ServerForm
class
file.
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;
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); }
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. }
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 + "'"); } }