This section provides the source code for the btsppEcho. For the complete Eclipse project ZIP file, see Forum Nokia.
The example includes the following classes:
package example.btsppecho; import java.io.IOException; import java.util.Vector; import javax.microedition.midlet.MIDlet; import javax.microedition.lcdui.Display; import javax.microedition.lcdui.Displayable; import javax.bluetooth.BluetoothStateException; import javax.bluetooth.LocalDevice; import javax.bluetooth.ServiceRecord; import example.btsppecho.client.ConnectionService; public class MIDletApplication extends MIDlet { private final static String UUID = "A55665EE9F9146109085C2055C888B39"; private final static String SERVICE_URL = "btspp://localhost:" + UUID; private final SettingsList settingsList; private boolean restoreDiscoverableModeOnExit; private int initialDiscoverableMode; // Client server private ConnectionService ConnectionService = null; private ClientForm clientForm = null; // Server client private ServiceDiscoveryList serviceDiscoveryList = null; private ServerForm serverForm = null; boolean serverUseAuthentication = false; boolean serverUseEncryption = false; public MIDletApplication() { // Try to read the initial discoverable mode of // device, so it can be restored on exit. try { restoreDiscoverableModeOnExit = true; initialDiscoverableMode = LocalDevice.getLocalDevice() .getDiscoverable(); } catch (BluetoothStateException e) { restoreDiscoverableModeOnExit = false; } settingsList = new SettingsList(this); } private void exit() { destroyApp(false); notifyDestroyed(); } public void startApp() { Display display = Display.getDisplay(this); display.setCurrent(settingsList); ErrorScreen.init(null, display); } public void pauseApp() { // we can ignore this } public void destroyApp(boolean unconditional) { // stop any networking, service discovery, etc. // threads that the MIDlet may have started if (serviceDiscoveryList != null) { serviceDiscoveryList.cancelPendingSearches(); serviceDiscoveryList.abort(); } if (ConnectionService != null) { ConnectionService.close(); } if (clientForm != null) { clientForm.closeAll(); } if (serverForm != null) { serverForm.closeAll(); } // I restore the discoverable mode to the initial // value on exit, so the behaviour of this MIDlet // might be more similar in this respect on all // vendors' devices. (You might want to rethink // this in your MIDlet, for example if a device // allows you to run multiple simultaneous Bluetooth // applications, each having varying start & // exit times.) if (restoreDiscoverableModeOnExit) { try { LocalDevice ld = LocalDevice.getLocalDevice(); ld.setDiscoverable(initialDiscoverableMode); } catch (BluetoothStateException e) { // there is nothing we can do // to handle this case: ignore it } } } // screen callbacks // ClientForm public void clientFormExitRequest() { exit(); } public void clientFormViewLog(Displayable next) { LogScreen logScreen = new LogScreen(this, next, "Log", "Back"); Display.getDisplay(this).setCurrent(logScreen); } // SettingsList callbacks public void settingsListStart(boolean isServer, int inquiryAccessCode, boolean useAuthentication, boolean useAuthorization, boolean useEncryption) { // set inquiry access mode for ConnectionService if (!isServer) { try { LocalDevice.getLocalDevice() .setDiscoverable(inquiryAccessCode); } catch(BluetoothStateException e) { String msg = "Error changing inquiry access type: " + e.getMessage(); ErrorScreen.showError(msg, settingsList); } } if (isServer) { // start application in server role // we only run one server at a time, // so the following is safe serverUseAuthentication = useAuthentication; serverUseEncryption = useEncryption; serviceDiscoveryList = new ServiceDiscoveryList( this, UUID, inquiryAccessCode); Display.getDisplay(this) .setCurrent(serviceDiscoveryList); } else { // start application in client role clientForm = new ClientForm(this); String url = SERVICE_URL; if (useAuthentication) { url += ";authenticate=true"; } else { url += ";authenticate=false"; } if (useAuthorization) { url += ";authorize=true"; } else { url += ";authorize=false"; } if (useEncryption) { url += ";encrypt=true"; } else { url += ";encrypt=false"; } url += ";name=btsppEcho"; ConnectionService = new ConnectionService(url, clientForm); Display.getDisplay(this).setCurrent(clientForm); } } public void settingsListPropertiesRequest() { String[] keys = { "bluetooth.api.version", "bluetooth.connected.devices.max", "bluetooth.connected.inquiry", "bluetooth.connected.inquiry.scan", "bluetooth.connected.page", "bluetooth.connected.page.scan", "bluetooth.l2cap.receiveMTU.max", "bluetooth.master.switch", "bluetooth.sd.attr.retrievable.max", "bluetooth.sd.trans.max", }; String str = ""; try { str = "my bluetooth address: " + LocalDevice.getLocalDevice() .getBluetoothAddress() + "\n"; } catch (BluetoothStateException e) { // there is nothing we can do: ignore it } for (int i=0; i < keys.length; i++) { str += keys[i] + ": " + LocalDevice.getProperty(keys[i]) + "\n"; } TextScreen textScreen = new TextScreen(this, settingsList, "Device properties", str, "Back"); Display.getDisplay(this).setCurrent(textScreen); } public void settingsListExitRequest() { exit(); } // ServiceDiscoveryList callbacks public void serviceDiscoveryListFatalError(String errorMessage) { ErrorScreen.showError(errorMessage, serviceDiscoveryList); Display.getDisplay(this).setCurrent(settingsList); } public void serviceDiscoveryListError(String errorMessage) { ErrorScreen.showError(errorMessage, serviceDiscoveryList); } public void serviceDiscoveryListOpen(Vector selectedServiceRecords) { int security; if (serverUseAuthentication) { if (serverUseEncryption) { security = ServiceRecord.AUTHENTICATE_ENCRYPT; } else { security = ServiceRecord.AUTHENTICATE_NOENCRYPT; } } else { security = ServiceRecord.NOAUTHENTICATE_NOENCRYPT; } if (serverForm == null) { serverForm = new ServerForm(this); } serverForm.makeConnections(selectedServiceRecords, security); Display.getDisplay(this).setCurrent(serverForm); } public void serviceDiscoveryListExitRequest() { exit(); } public void serviceDiscoveryListBackRequest(Displayable next) { Display.getDisplay(this).setCurrent(next); } public void serviceDiscoveryListViewLog(Displayable next) { LogScreen logScreen = new LogScreen(this, next, "Log", "Back"); Display.getDisplay(this).setCurrent(logScreen); } // TextScreen public void textScreenClosed(Displayable next) { Display.getDisplay(this).setCurrent(next); } // LogScreen public void logScreenClosed(Displayable next) { Display.getDisplay(this).setCurrent(next); } // ServerForm public void serverFormSearchRequest(int numConnectionsOpen) { // cleanup for new search serviceDiscoveryList.init(numConnectionsOpen); if (numConnectionsOpen > 0) { serviceDiscoveryList.addBackCommand(serverForm); } Display.getDisplay(this).setCurrent(serviceDiscoveryList); } public void serverFormExitRequest() { exit(); } public void serverFormAddConnection(Vector alreadyOpen) { // I took a simple approach of simply changing the // screen to the ServiceDiscovery screen when the // user wants to try and add a new connection, or // perform both a new device inquiry + service search // and then add more connections. // // However, reality can be a bit more complicated: // - How many previously discovered items (e.g. device // running the desired service) have we already // connected to, or not connected to? // - How many additional new connections can this device // open below its maximum limit? The maximum number // of simultaneous connections can vary in different // devices (i.e. see "bluetooth.connected.devices.max"). // - Can new inquiries/searches be started while // already connected? // Depending on your MIDlet's needs + use cases and // the devices it is likely to be deployed in, it // might employ a bit more user friendly approach than // the simplistic/generic one used here. serviceDiscoveryList.remove(alreadyOpen); serviceDiscoveryList.addBackCommand(serverForm); Display.getDisplay(this).setCurrent(serviceDiscoveryList); } public void serverFormViewLog() { LogScreen logScreen = new LogScreen(this, serverForm, "Log", "Back"); Display.getDisplay(this).setCurrent(logScreen); } }
package example.btsppecho; import java.io.IOException; import java.util.Vector; import javax.bluetooth.BluetoothStateException; import javax.bluetooth.LocalDevice; import javax.microedition.lcdui.Command; import javax.microedition.lcdui.CommandListener; import javax.microedition.lcdui.Displayable; import javax.microedition.lcdui.Form; import javax.microedition.lcdui.Item; import javax.microedition.lcdui.StringItem; import javax.microedition.lcdui.TextField; import example.btsppecho.client.ClientConnectionHandler; import example.btsppecho.client.ClientConnectionHandlerListener; import example.btsppecho.client.ConnectionService; public class ClientForm extends Form implements CommandListener, ClientConnectionHandlerListener { private final MIDletApplication midlet; private final StringItem numConnectionsField; private final TextField sendDataField; private final StringItem receivedDataField; private final StringItem statusField; private final Command sendCommand; private final Command quitCommand; private final Command logCommand; private final Vector handlers = new Vector(); private StringItem btAddressField = null; private volatile int numReceivedMessages = 0; private volatile int numSentMessages = 0; private int sendMessageId = 0; public ClientForm(MIDletApplication midlet) { super("Client"); this.midlet = midlet; try { String address = LocalDevice.getLocalDevice() .getBluetoothAddress(); btAddressField = new StringItem("My address", address); append(btAddressField); } catch (BluetoothStateException e) { // nothing we can do, don't add field } numConnectionsField = new StringItem("Connections", "0"); append(numConnectionsField); statusField = new StringItem("Status", "listening"); append(statusField); sendDataField = new TextField("Send data", "Client says: 'Hello, world.'", 64, TextField.ANY); append(sendDataField); receivedDataField = new StringItem("Last received data", null); append(receivedDataField); sendCommand = new Command("Send", Command.SCREEN, 1); quitCommand = new Command("Exit", Command.EXIT, 1); logCommand = new Command("View log", Command.SCREEN, 2); addCommand(quitCommand); addCommand(logCommand); setCommandListener(this); } void closeAll() { for (int i=0; i < handlers.size(); i++) { ClientConnectionHandler handler = (ClientConnectionHandler) handlers.elementAt(i); handler.close(); } } public void commandAction(Command cmd, Displayable disp) { if (cmd == logCommand) { midlet.clientFormViewLog(this); } if (cmd == sendCommand) { String sendData = sendDataField.getString(); try { sendMessageToAllClients(++sendMessageId, sendData); } catch (IllegalArgumentException e) { // Message length longer than // ServerConnectionHandler.MAX_MESSAGE_LENGTH String errorMessage = "IllegalArgumentException while trying " + "to send a message: " + e.getMessage(); handleError(null, errorMessage); } } else if (cmd == quitCommand) { // the MIDlet aborts the ConnectionService, etc. midlet.clientFormExitRequest(); } } public void removeHandler(ClientConnectionHandler handler) { // Note: we assume the caller has aborted/closed/etc. // the handler if that needed to be done. This method // simply removes it from the list of handlers maintained // by the ConnectionService. handlers.removeElement(handler); } public void sendMessageToAllClients(int sendMessageId, String sendData) throws IllegalArgumentException { Integer id = new Integer(sendMessageId); for (int i=0; i < handlers.size(); i++) { ClientConnectionHandler handler = (ClientConnectionHandler) handlers.elementAt(i); // throws IllegalArgumentException if message length // > ServerConnectionHandler.MAX_MESSAGE_LENGTH handler.queueMessageForSending( id, sendData.getBytes()); } } // interface L2CAPConnectionListener public void handleAcceptAndOpen(ClientConnectionHandler handler) { handlers.addElement(handler); // start the reader and writer, it also causes underlying // InputStream and OutputStream to be opened. handler.start(); statusField.setText("'Accept and open' for new connection"); } public void handleStreamsOpen(ClientConnectionHandler handler) { // first connection if (handlers.size() == 1) { addCommand(sendCommand); } String str = Integer.toString(handlers.size()); numConnectionsField.setText(str); statusField.setText("I/O streams opened on connection"); } public void handleStreamsOpenError(ClientConnectionHandler handler, String errorMessage) { handlers.removeElement(handler); String str = Integer.toString(handlers.size()); numConnectionsField.setText(str); statusField.setText("Error opening I/O streams: " + errorMessage); } public void handleReceivedMessage(ClientConnectionHandler handler, byte[] messageBytes) { numReceivedMessages++; String msg = new String(messageBytes); receivedDataField.setText(msg); statusField.setText( "# messages read: " + numReceivedMessages + " " + "sent: " + numSentMessages); } public void handleQueuedMessageWasSent( ClientConnectionHandler handler, Integer id) { numSentMessages++; statusField.setText("# messages read: " + numReceivedMessages + " " + "sent: " + numSentMessages); } public void handleClose(ClientConnectionHandler handler) { removeHandler(handler); if (handlers.size() == 0) { removeCommand(sendCommand); } String str = Integer.toString(handlers.size()); numConnectionsField.setText(str); statusField.setText("Connection closed"); } public void handleErrorClose(ClientConnectionHandler handler, String errorMessage) { removeHandler(handler); if (handlers.size() == 0) { removeCommand(sendCommand); } statusField.setText("Error: (close)\"" + errorMessage + "\""); } public void handleError(ClientConnectionHandler hander, String errorMessage) { statusField.setText("Error: \"" + errorMessage + "\""); } }
package example.btsppecho; import java.io.IOException; import java.util.Vector; import javax.bluetooth.BluetoothStateException; import javax.bluetooth.LocalDevice; import javax.bluetooth.ServiceRecord; import javax.microedition.lcdui.Command; import javax.microedition.lcdui.CommandListener; import javax.microedition.lcdui.Displayable; import javax.microedition.lcdui.Form; import javax.microedition.lcdui.Item; import javax.microedition.lcdui.StringItem; import javax.microedition.lcdui.TextField; import example.btsppecho.server.ServerConnectionHandler; import example.btsppecho.server.ServerConnectionHandlerListener; class ServerForm extends Form implements ServerConnectionHandlerListener, CommandListener { private final MIDletApplication midlet; private final StringItem numConnectionsField; private final TextField sendDataField; private final StringItem receivedDataField; private final StringItem statusField; private final Command sendCommand; private final Command addConnectionCommand; private final Command searchCommand; private final Command logCommand; private final Command quitCommand; private final Command clearStatusCommand; private final Vector handlers; private int maxConnections; private StringItem btAddressField = null; private volatile int numReceivedMessages = 0; private volatile int numSentMessages = 0; private int sendMessageId = 0; ServerForm(MIDletApplication midlet) { super("Server"); this.midlet = midlet; handlers = new Vector(); String value = LocalDevice.getProperty( "bluetooth.connected.devices.max"); try { maxConnections = Integer.parseInt(value); } catch (NumberFormatException e) { maxConnections = 0; } // 1. add Form items try { String address = LocalDevice.getLocalDevice() .getBluetoothAddress(); btAddressField = new StringItem("My address", address); append(btAddressField); } catch (BluetoothStateException e) { // nothing we can do, don't add field } numConnectionsField = new StringItem("Connections", "0"); append(numConnectionsField); statusField = new StringItem("Status", ""); append(statusField); sendDataField = new TextField("Send data", "Server says: 'Hello, world.'", 64, TextField.ANY); append(sendDataField); receivedDataField = new StringItem("Last received data", null); append(receivedDataField); // 2. create commands sendCommand = new Command("Send", Command.SCREEN, 1); searchCommand = new Command("Search for more", Command.SCREEN, 1); addConnectionCommand = new Command("Add connection", Command.SCREEN, 2); logCommand = new Command("View log", Command.SCREEN, 3); clearStatusCommand = new Command("Clear status", Command.SCREEN, 4); quitCommand = new Command("Quit", Command.EXIT, 1); // 3. add commands and set command listener addCommand(searchCommand); addCommand(addConnectionCommand); addCommand(logCommand); addCommand(clearStatusCommand); addCommand(quitCommand); // The 'sendCommand' is added later to screen, // if at least one connection is open. setCommandListener(this); } public void makeConnections(Vector serviceRecords, int security) { for (int i=0; i < serviceRecords.size(); i++) { ServiceRecord serviceRecord = (ServiceRecord) serviceRecords.elementAt(i); boolean found = false; for (int j=0; j < handlers.size(); j++) { ServerConnectionHandler old = (ServerConnectionHandler) handlers.elementAt(j); String oldAddr = old.getServiceRecord(). getHostDevice(). getBluetoothAddress(); String newAddr = serviceRecord.getHostDevice() .getBluetoothAddress(); if (oldAddr.equals(newAddr)) { found = true; break; } } if (!found) { ServerConnectionHandler newHandler = new ServerConnectionHandler( this, serviceRecord, security); newHandler.start(); // start reader & writer } } } private void removeHandler(ServerConnectionHandler handler) { if (handlers.contains(handler)) { handlers.removeElement(handler); String str = Integer.toString(handlers.size()); numConnectionsField.setText(str); if (handlers.size() == 0) { removeCommand(sendCommand); addCommand(searchCommand); } } } void closeAll() { for (int i=0; i < handlers.size(); i++) { ServerConnectionHandler handler = (ServerConnectionHandler) handlers.elementAt(i); handler.close(); removeHandler(handler); } } public void commandAction(Command cmd, Displayable disp) { if (cmd == addConnectionCommand) { Vector v = new Vector(); for (int i=0; i < handlers.size(); i++) { ServerConnectionHandler handler = (ServerConnectionHandler) handlers.elementAt(i); String btAddress = handler.getServiceRecord() .getHostDevice() .getBluetoothAddress(); v.addElement(btAddress); } midlet.serverFormAddConnection(v); } else if (cmd == clearStatusCommand) { statusField.setText(""); } else if (cmd == logCommand) { midlet.serverFormViewLog(); } else if (cmd == sendCommand) { String sendData = sendDataField.getString(); Integer id = new Integer(sendMessageId++); for (int i=0; i < handlers.size(); i++) { ServerConnectionHandler handler = (ServerConnectionHandler) handlers.elementAt(i); try { handler.queueMessageForSending( id, sendData.getBytes()); statusField.setText( "Queued a send message request"); } catch(IllegalArgumentException e) { // Message length longer than // ServerConnectionHandler.MAX_MESSAGE_LENGTH String errorMessage = "IllegalArgumentException while trying " + "to send a message: " + e.getMessage(); handleError(handler, errorMessage); } } } else if (cmd == searchCommand) { midlet.serverFormSearchRequest(handlers.size()); } else if (cmd == quitCommand) { closeAll(); midlet.serverFormExitRequest(); } // To keep this MIDlet simple, I didn't add any way // to drop individual connections. But you might // want to do so. } // ServerConnectionHandlerListener interface methods public void handleOpen(ServerConnectionHandler handler) { handlers.addElement(handler); // for the first open connection if (handlers.size() == 1) { removeCommand(searchCommand); removeCommand(sendCommand); addCommand(sendCommand); } // Remove the 'Add connection' command // when the device already has open the // maximum number of connections it can // support. if (handlers.size() >= maxConnections) { removeCommand(addConnectionCommand); } statusField.setText("Connection opened"); String str = Integer.toString(handlers.size()); numConnectionsField.setText(str); } public void handleOpenError( ServerConnectionHandler handler, String errorMessage) { statusField.setText("Error opening outbound connection: " + errorMessage); } public void handleReceivedMessage( ServerConnectionHandler handler, byte[] messageBytes) { numReceivedMessages++; String message = new String(messageBytes); receivedDataField.setText(message); statusField.setText( "# messages read: " + numReceivedMessages + " " + "sent: " + numSentMessages); // Broadcast message to all clients for (int i=0; i < handlers.size(); i++) { ServerConnectionHandler h = (ServerConnectionHandler) handlers.elementAt(i); Integer id = new Integer(sendMessageId++); try { h.queueMessageForSending(id, messageBytes); } catch (IllegalArgumentException e) { String errorMessage = "IllegalArgumentException while trying to " + "send message: " + e.getMessage(); handleError(handler, errorMessage); } } } public void handleQueuedMessageWasSent( ServerConnectionHandler handler, Integer id) { numSentMessages++; statusField.setText("# messages read: " + numReceivedMessages + " " + "sent: " + numSentMessages); } public void handleClose(ServerConnectionHandler handler) { removeHandler(handler); if (handlers.size() == 0) { removeCommand(sendCommand); addCommand(searchCommand); } // If the number of currently open connections // drops below the maximum number that this // device could have open, restore // 'Add connection' to the screen commands. if (handlers.size() < maxConnections) { removeCommand(addConnectionCommand); addCommand(addConnectionCommand); } statusField.setText("Connection closed"); } public void handleErrorClose(ServerConnectionHandler handler, String errorMessage) { removeHandler(handler); if (handlers.size() == 0) { removeCommand(sendCommand); addCommand(searchCommand); } statusField.setText("Error (close): '" + errorMessage + "'"); } public void handleError(ServerConnectionHandler handler, String errorMessage) { statusField.setText("Error: '" + errorMessage + "'"); } }
package example.btsppecho; import java.util.Vector; import javax.microedition.lcdui.*; import javax.bluetooth.DiscoveryAgent; import javax.bluetooth.DiscoveryListener; // This class represents a simple log screen. In the ideal case, // the actual log would be encapsulated by a different class // than the presentation (LogScreen). It is a bit less elegant, // but eliminates one class, to combine the log and log // screen in the same class. This MIDlet uses the LogScreen // mainly as a simple aid during device inquiry + service discovery // to help a user follow the progress if they wish to. So the // log isn't persistently saved in the record store. (That would // be easy to add if it were needed.) For unsophisticated users, // a MIDlet would use some other visual aid rather than a log to show // progress through the device inquiry + service discovery phases. // The target users of this MIDlet are mainly developers learning // to use Bluetooth, so a LogScreen is probably more helpful for them, // as it helps show how which Bluetooth devices were found during // device inquiry, which devices of those are running the desired // service, and so on. public class LogScreen extends Form implements CommandListener { private static final Vector entries = new Vector(); private static final String FIRST_ENTRY = "-- Log started: --\n\n"; // We place a limit the maximum number of entries logged. // Only the 1 .. MAX_ENTRIES last entries will be kept // in the log. If the log exceeds MAX_ENTRIES, the // earliest entries will be deleted. private static final int MAX_ENTRIES = 300; static { log(FIRST_ENTRY); } private final MIDletApplication midlet; private final Displayable next; private final Command refreshCommand; private final Command deleteCommand; private final Command closeCommand; public LogScreen(MIDletApplication midlet, Displayable next, String title, String closeLabel) { super(title); this.midlet = midlet; this.next = next; refresh(); // add any text already present refreshCommand = new Command("Refresh", Command.SCREEN, 1); deleteCommand = new Command("Delete", Command.SCREEN, 2); closeCommand = new Command(closeLabel, Command.SCREEN, 3); addCommand(refreshCommand); addCommand(deleteCommand); addCommand(closeCommand); setCommandListener(this); } public static void log (String string) { if (entries.size() > MAX_ENTRIES) { entries.removeElementAt(0); } entries.addElement(string); } private void refresh() { // clear the display's text while(size() > 0) { delete(0); } // get the lastest status and display that as text String text = ""; for (int i=0; i < entries.size(); i++) { String str = (String) entries.elementAt(i); if (str != null) { text += str; } } append(text); } public void commandAction(Command command, Displayable d) { if (command == closeCommand) { midlet.logScreenClosed(next); } else if (command == refreshCommand) { refresh(); } else if (command == deleteCommand) { // The deletion of all log strings affects // all LogScreen instances. synchronized(this) { entries.removeAllElements(); log(FIRST_ENTRY); } refresh(); } } // It was somewhat convenient to place these helper // methods inside the LogScreen class. public static String inquiryAccessCodeString(int iac) { String str = null; switch(iac) { case DiscoveryAgent.CACHED: str = "CACHED"; break; case DiscoveryAgent.GIAC: str = "GIAC"; break; case DiscoveryAgent.LIAC: str = "LIAC"; break; case DiscoveryAgent.PREKNOWN: str = "PREKNOWN"; break; } return str; } public static String responseCodeString(int responseCode) { String str = null; switch (responseCode) { case DiscoveryListener.SERVICE_SEARCH_COMPLETED: str = "SERVICE_SEARCH_COMPLETED"; break; case DiscoveryListener.SERVICE_SEARCH_DEVICE_NOT_REACHABLE: str = "SERVICE_SEARCH_DEVICE_NOT_REACHABLE"; break; case DiscoveryListener.SERVICE_SEARCH_ERROR: str = "SERVICE_SEARCH_ERROR"; break; case DiscoveryListener.SERVICE_SEARCH_NO_RECORDS: str = "SERVICE_SEARCH_NO_RECORDS"; break; case DiscoveryListener.SERVICE_SEARCH_TERMINATED: str = "SERVICE_SEARCH_TERMINATED"; break; } return str; } }
package example.btsppecho; import java.util.Hashtable; import java.util.Vector; import java.util.Enumeration; import javax.bluetooth.BluetoothStateException; import javax.bluetooth.DataElement; import javax.bluetooth.DeviceClass; import javax.bluetooth.DiscoveryAgent; import javax.bluetooth.DiscoveryListener; import javax.bluetooth.LocalDevice; import javax.bluetooth.RemoteDevice; import javax.bluetooth.ServiceRecord; import javax.bluetooth.UUID; import javax.microedition.lcdui.Command; import javax.microedition.lcdui.CommandListener; import javax.microedition.lcdui.Displayable; import javax.microedition.lcdui.Image; import javax.microedition.lcdui.ImageItem; import javax.microedition.lcdui.List; import javax.microedition.lcdui.Form; import example.btsppecho.MIDletApplication; class ServiceDiscoveryList extends List implements CommandListener, DiscoveryListener, Runnable { private final static String TITLE = "New search"; private final static int WAIT_MILLIS = 500; // milliseconds private final static String[] ACTIVITY = { "", ".", "..", "...", "...." }; private final UUID uuid; private final MIDletApplication midlet; private final Command backCommand; private final Command searchCommand; private final Command openCommand; private final Command stopCommand; private final Command logCommand; private final Command exitCommand; private final int sdTransMax; private final int inquiryAccessCode; private DiscoveryAgent discoveryAgent; private volatile boolean inquiryInProgress = false; private volatile int numServiceSearchesInProgress = 0; private volatile boolean aborting = false; private volatile Thread thread; private Displayable backDisplayable; private volatile Thread activityIndicatorThread; private boolean activityIndicatorRunning = false; private Vector unsearchedRemoteDevices = new Vector(); private Vector transIds = new Vector(); private Hashtable matchingServiceRecords = new Hashtable(); private int numConnectionsAlreadyOpen = 0; ServiceDiscoveryList(MIDletApplication midlet, String uuidString, int inquiryAccessCode) { super(TITLE, List.IMPLICIT); this.midlet = midlet; uuid = new UUID(uuidString, false); this.inquiryAccessCode = inquiryAccessCode; openCommand = new Command("Open connection", Command.SCREEN, 1); searchCommand = new Command("Search", Command.SCREEN, 2); logCommand = new Command("View log", Command.SCREEN, 3); stopCommand = new Command("Stop", Command.SCREEN, 4); backCommand = new Command("Back", Command.BACK, 5); exitCommand = new Command("Exit", Command.EXIT, 0); String property = LocalDevice.getProperty("bluetooth.sd.trans.max"); sdTransMax = Integer.parseInt(property); // create discovery agent try { discoveryAgent = LocalDevice.getLocalDevice().getDiscoveryAgent(); addCommand(logCommand); addCommand(exitCommand); addCommand(searchCommand); setCommandListener(this); start(); } catch(BluetoothStateException e) { midlet.serviceDiscoveryListFatalError( "Couldn't get a discovery agent: '" + e.getMessage() + "'"); } } static Image makeImage(String filename) { Image image = null; try { image = Image.createImage(filename); } catch (Exception e) { // there's nothing we can do, so ignore } return image; } public void addBackCommand(Displayable backDisplayable) { this.backDisplayable = backDisplayable; removeCommand(backCommand); addCommand(backCommand); } public synchronized void start() { thread = new Thread(this); thread.start(); } public synchronized void abort() { thread = null; } public void init(int numConnectionsAlreadyOpen) { this.numConnectionsAlreadyOpen = numConnectionsAlreadyOpen; // stop any pending searches if (inquiryInProgress || numServiceSearchesInProgress > 0) { cancelPendingSearches(); } // remove any old list elements while (size() > 0) { delete(0); } } private synchronized void setInquiryInProgress(boolean bool) { inquiryInProgress = bool; } public void commandAction(Command command, Displayable d) { if (command == logCommand) { midlet.serviceDiscoveryListViewLog(this); } else if (command == searchCommand && !inquiryInProgress && (numServiceSearchesInProgress == 0)) { // new inquiry started removeCommand(openCommand); // remove old device lists unsearchedRemoteDevices.removeAllElements(); for (Enumeration keys = matchingServiceRecords.keys(); keys.hasMoreElements();) { matchingServiceRecords.remove(keys.nextElement()); } // delete all old List items while (size() > 0) { delete(0); } try { // disable page scan and inquiry scan LocalDevice dev = LocalDevice.getLocalDevice(); dev.setDiscoverable(DiscoveryAgent.NOT_DISCOVERABLE); String iacString = LogScreen.inquiryAccessCodeString(inquiryAccessCode); LogScreen.log("startInquiry (" + iacString + ")\n"); //startActivityIndicator(); // this is non-blocking discoveryAgent.startInquiry(inquiryAccessCode, this); setInquiryInProgress(true); addCommand(stopCommand); removeCommand(searchCommand); } catch (BluetoothStateException e) { addCommand(searchCommand); removeCommand(stopCommand); midlet.serviceDiscoveryListError( "Error during startInquiry: '" + e.getMessage() + "'"); } } else if (command == stopCommand) { // stop searching if (cancelPendingSearches()) { setInquiryInProgress(false); removeCommand(stopCommand); addCommand(searchCommand); } } else if (command == exitCommand) { midlet.serviceDiscoveryListExitRequest(); } else if (command == openCommand) { int size = this.size(); boolean[] flags = new boolean[size]; getSelectedFlags(flags); Vector selectedServiceRecords = new Vector(); for (int i=0; i < size; i++) { if (flags[i]) { String key = getString(i); ServiceRecord rec = (ServiceRecord) matchingServiceRecords.get(key); selectedServiceRecords.addElement(rec); } } // try to perform an open on selected items if (selectedServiceRecords.size() > 0) { String value = LocalDevice.getProperty( "bluetooth.connected.devices.max"); int maxNum = Integer.parseInt(value); int total = numConnectionsAlreadyOpen + selectedServiceRecords.size(); if (total > maxNum) { midlet.serviceDiscoveryListError( "Too many selected. " + "This device can connect to at most " + maxNum + " other devices"); } else { midlet.serviceDiscoveryListOpen( selectedServiceRecords); } } else { midlet.serviceDiscoveryListError( "Select at least one to open"); } } else if (command == backCommand) { midlet.serviceDiscoveryListBackRequest(backDisplayable); } } boolean cancelPendingSearches() { LogScreen.log("Cancel pending inquiries and searches\n"); boolean everythingCancelled = true; if (inquiryInProgress) { // Note: The BT API (v1.0) isn't completely clear // whether cancelInquiry is blocking or non-blocking. if (discoveryAgent.cancelInquiry(this)) { setInquiryInProgress(false); } else { everythingCancelled = false; } } for (int i=0; i < transIds.size(); i++) { // Note: The BT API (v1.0) isn't completely clear // whether cancelServiceSearch is blocking or // non-blocking? Integer pendingId = (Integer) transIds.elementAt(i); if (discoveryAgent.cancelServiceSearch( pendingId.intValue())) { transIds.removeElement(pendingId); } else { everythingCancelled = false; } } return everythingCancelled; } // DiscoveryListener callbacks public void deviceDiscovered(RemoteDevice remoteDevice, DeviceClass deviceClass) { LogScreen.log("deviceDiscovered: " + remoteDevice.getBluetoothAddress() + " major device class=" + deviceClass.getMajorDeviceClass() + " minor device class=" + deviceClass.getMinorDeviceClass() + "\n"); // Note: The following check has the effect of only // performing later service searches on phones. // If you intend to run the MIDlet on some other device (e.g. // handheld computer, PDA, etc. you have to add a check // for them as well.) You might also refine the check // using getMinorDeviceClass() to check only cellular phones. boolean isPhone = (deviceClass.getMajorDeviceClass() == 0x200); // Setting the following line to 'true' is a workaround // for some early beta SDK device emulators. Set it // to false when compiling the MIDlet for download to // real MIDP phones! boolean isEmulator = true; //false; if (isPhone || isEmulator) { unsearchedRemoteDevices.addElement(remoteDevice); } } public void inquiryCompleted(int discoveryType) { LogScreen.log("inquiryCompleted: " + discoveryType + "\n"); setInquiryInProgress(false); if (unsearchedRemoteDevices.size() == 0) { setTitle(TITLE); addCommand(searchCommand); removeCommand(stopCommand); midlet.serviceDiscoveryListError( "No Bluetooth devices found"); } } public void servicesDiscovered(int transId, ServiceRecord[] serviceRecords) { LogScreen.log("servicesDiscovered: transId=" + transId + " # serviceRecords=" + serviceRecords.length + "\n"); // Remove+Add: ensure there is at most one open command removeCommand(openCommand); addCommand(openCommand); // Use the friendly name and/or bluetooth address // to identify the matching Device + ServiceRecord // (Note: Devices with different Bluetooth addresses // might have the same friendly name, for example a // device's default friendly name.) // there should only be one record if (serviceRecords.length == 1) { RemoteDevice device = serviceRecords[0].getHostDevice(); String name = device.getBluetoothAddress(); // This MIDlet only uses the first matching service // record found for a particular device. if (!matchingServiceRecords.containsKey(name)) { matchingServiceRecords.put(name, serviceRecords[0]); append(name, null); // The List should have at least one entry, // before we add an open command. if (size() == 1) { addCommand(openCommand); } } } else { midlet.serviceDiscoveryListError( "Error: Unexpected number (" + serviceRecords.length + ") of service records " + "in servicesDiscovered callback, transId=" + transId); } } public void serviceSearchCompleted(int transId, int responseCode) { setTitle("New search"); String responseCodeString = LogScreen.responseCodeString(responseCode); LogScreen.log("serviceSearchCompleted: transId=" + transId + " (" + responseCodeString + ")\n"); // For any responseCode, decrement the counter numServiceSearchesInProgress--; // remove the transaction id from the pending list for (int i=0; i < transIds.size(); i++) { Integer pendingId = (Integer) transIds.elementAt(i); if (pendingId.intValue() == transId) { transIds.removeElement(pendingId); break; } } // all the searches have completed if (!inquiryInProgress && (transIds.size() == 0)) { removeCommand(stopCommand); addCommand(searchCommand); if (matchingServiceRecords.size() == 0) { midlet.serviceDiscoveryListError( "No matching services found"); } } } // Interface Runnable public void run() { Thread currentThread = Thread.currentThread(); int i = 0; running: while (thread == currentThread) { synchronized (this) { if (thread != currentThread) { break running; } else if (!inquiryInProgress) { doServiceSearch(); } if (inquiryInProgress || numServiceSearchesInProgress > 0) { setTitle("Searching " + ACTIVITY[i]); if (++i >= ACTIVITY.length) { i = 0; } } try { wait(WAIT_MILLIS); } catch (InterruptedException e) { // we can safely ignore this } } } } public void doServiceSearch() { if ((unsearchedRemoteDevices.size() > 0) && (numServiceSearchesInProgress < sdTransMax)) { synchronized(this) { RemoteDevice device = (RemoteDevice) unsearchedRemoteDevices .elementAt(0); UUID[] uuids = new UUID[1]; uuids[0] = uuid; try { int[] attrSet = null; // default attrSet numServiceSearchesInProgress++; int transId = discoveryAgent.searchServices( attrSet, uuids, device, this); LogScreen.log("starting service search on device=" + device.getBluetoothAddress() + " transId=" + transId + "\n"); transIds.addElement(new Integer(transId)); unsearchedRemoteDevices.removeElementAt(0); } catch (BluetoothStateException e) { numServiceSearchesInProgress--; midlet.serviceDiscoveryListError( "Error, could not perform " + "service search: '" + e.getMessage()); } } } } public void remove(Vector alreadyOpen) { if (size() > 0) { for (int i=0; i < alreadyOpen.size(); i++) { // Bluetooth address of slave device that // we already have a connection open to String btAddress = (String) alreadyOpen.elementAt(i); boolean found = false; for (int j = 0; j < size(); j++) { if (getString(j).equals(btAddress)) { found = true; // if the element we are about to // remove is selected, select something else if (getSelectedIndex() == j) { setSelectedIndex(j, false); } delete(j); break; } } if (found) { break; } } } } }
package example.btsppecho; import javax.microedition.lcdui.Command; import javax.microedition.lcdui.CommandListener; import javax.microedition.lcdui.Displayable; import javax.microedition.lcdui.List; import javax.bluetooth.DiscoveryAgent; class SettingsList extends List implements CommandListener { // UI strings private static final String SERVER = "Server"; private static final String CLIENT = "Client"; // (I abbreviated the strings "Authentication", // "Authorization" and "Encryption" because they are a // bit long on some MIDP device's List items. Another // MIDlet might hard code its preference anyways.) private static final String AUTHENTICATION_TRUE = "Authen.: true"; private static final String AUTHENTICATION_FALSE = "Authen.: false"; private static final String AUTHORIZATION_TRUE = "Authoriz.: true"; private static final String AUTHORIZATION_FALSE = "Authoriz.: false"; private static final String ENCRYPTION_TRUE = "Encrypt: true"; private static final String ENCRYPTION_FALSE = "Encrypt: false"; private static final String RETRIEVE_DEVICES_TRUE = "Use known devices"; private static final String RETRIEVE_DEVICES_FALSE = "Use inquiry"; private static final String INQUIRY_TYPE_LIAC = "LIAC"; private static final String INQUIRY_TYPE_GIAC = "GIAC"; private static final String INQUIRY_TYPE_NOT_DISCOVERABLE = "not discoverable"; private static final String INQUIRY_TYPE_CACHED = "cached"; private static final String INQUIRY_TYPE_PREKNOWN = "preknown"; // settings private int inquiryType; private int protocol; private boolean isServer; private boolean useAuthorization; // client-only private boolean useAuthentication; // when useAuthenication is false, useEncryption is also false private boolean useEncryption; // MIDlet stuff private final MIDletApplication midlet; private final Command startCommand; private final Command propCommand; private final Command exitCommand; SettingsList(MIDletApplication midlet) { super("Settings", List.IMPLICIT); this.midlet = midlet; // default setting values // You should think about what is the preferred // default inquiry type used to make your service // discoverable. This MIDlet uses LIAC as the default, // but you might want to use GIAC. inquiryType = DiscoveryAgent.LIAC; isServer = false; // client by default useAuthentication = false; useEncryption = false; // false when auth. not used useAuthorization = false; // false when auth. not used updateListElements(); // add screen commands startCommand = new Command("Start application", Command.SCREEN, 0); propCommand = new Command("BT properties", Command.SCREEN, 1); exitCommand = new Command("Exit", Command.EXIT, 0); addCommand(startCommand); addCommand(propCommand); addCommand(exitCommand); setCommandListener(this); } private void updateListElements() { // remove all old list items while(size() > 0) { delete(0); } // Index 0: Server / Client String string; if (isServer) { string = SERVER; } else { string = CLIENT; } append(string, null); // Index 3: LIAC / GIAC if (inquiryType == DiscoveryAgent.LIAC) { append(makeInquiryLabel(isServer, INQUIRY_TYPE_LIAC), null); } else if (inquiryType == DiscoveryAgent.GIAC) { append(makeInquiryLabel(isServer, INQUIRY_TYPE_GIAC), null); } else if (inquiryType == DiscoveryAgent.PREKNOWN) { append(makeInquiryLabel(isServer, INQUIRY_TYPE_PREKNOWN), null); } else if (inquiryType == DiscoveryAgent.CACHED) { append(makeInquiryLabel(isServer, INQUIRY_TYPE_CACHED), null); } else if (inquiryType == DiscoveryAgent.NOT_DISCOVERABLE) { append(makeInquiryLabel(isServer, INQUIRY_TYPE_CACHED), null); } // Index 2: use authentication true / false // (encryption and authorization can only be // used if authentication is used) if (useAuthentication) { append(AUTHENTICATION_TRUE, null); // Index 3: use encryption true / false if (useEncryption) { append(ENCRYPTION_TRUE, null); } else { append(ENCRYPTION_FALSE, null); } // Index 4: ConnectionService only : use auth. true / false if (!isServer) { if (useAuthorization) { append(AUTHORIZATION_TRUE, null); } else { append(AUTHORIZATION_FALSE, null); } } } else { useAuthentication = false; useEncryption = false; append(AUTHENTICATION_FALSE, null); } } private String makeInquiryLabel(boolean searching, String string) { if (searching) { // we are searching return "Discover: " + string; } else { // we will be searched for return "Discoverable: " + string; } } public void commandAction(Command command, Displayable d) { if (command == startCommand) { midlet.settingsListStart(isServer, inquiryType, useAuthentication, useAuthorization, useEncryption); } else if (command == propCommand) { midlet.settingsListPropertiesRequest(); } else if (command == exitCommand) { midlet.settingsListExitRequest(); } else if (command == List.SELECT_COMMAND) { int index = getSelectedIndex(); switch(index) { // Index 0: "Server client" (isServer=true) or // "Client server" (isServer=false) case 0: isServer = !isServer; break; // Index 1: "Discovery mode: LIAC" or // "Discovery mode: GIAC" case 1: // toggle between LIAC and GIAC if (inquiryType == DiscoveryAgent.LIAC) { inquiryType = DiscoveryAgent.GIAC; } else { inquiryType = DiscoveryAgent.LIAC; } break; // Index 2: "Authentication: true" or // "Authentication: false" case 2: // toggle useAuthentication = !useAuthentication; if (!useAuthentication) { // Authorization and encryption are only // settable if authentication is true, otherwise // they are false and we should remove them. // (The order of removal is important.) // Only a client has this setting option // and not a server, thus the size check. if (size() == 5) { delete(4); // remove authorization from List useAuthorization = false; } this.delete(3); // remove encryption from List useEncryption = false; } break; // Index 3: "Encryption: true" or // "Encryption: false" case 3: useEncryption = !useEncryption; // toggle break; // Index 4: "Authorization: true" or // "Authorization: false" case 4: // toggle useAuthorization = !useAuthorization; break; } updateListElements(); setSelectedIndex(index, true); } } }
package example.btsppecho; import javax.microedition.lcdui.*; // A closeable screen for displaying text. class TextScreen extends Form implements CommandListener { private final MIDletApplication midlet; private final Displayable next; TextScreen(MIDletApplication midlet, Displayable next, String title, String text, String closeLabel) { super(title); this.midlet = midlet; this.next = next; append(text); addCommand(new Command(closeLabel, Command.BACK, 1)); setCommandListener(this); } public void commandAction(Command c, Displayable d) { // The application code only adds a 'close' command. midlet.textScreenClosed(next); } }
package example.btsppecho; import javax.microedition.lcdui.Alert; import javax.microedition.lcdui.AlertType; import javax.microedition.lcdui.Display; import javax.microedition.lcdui.Displayable; import javax.microedition.lcdui.Image; import java.util.Timer; import java.util.TimerTask; class ErrorScreen extends Alert { private static Image image; private static Display display; private static ErrorScreen instance = null; private ErrorScreen() { super("Error"); setType(AlertType.ERROR); setTimeout(2000); setImage(image); } static void init(Image img, Display disp) { image = img; display = disp; } static void showError(String message, Displayable next) { if (instance == null) { instance = new ErrorScreen(); } if (message == null) { message = ""; } instance.setString(message); display.setCurrent(instance, next); } }
package example.btsppecho.client; import java.io.InputStream; import java.io.IOException; import java.io.OutputStream; import java.util.Hashtable; import java.util.Enumeration; import java.util.Vector; import javax.microedition.io.StreamConnection; import javax.microedition.io.StreamConnectionNotifier; public class ClientConnectionHandler implements Runnable { private final static byte ZERO = (byte) '0'; private final static int LENGTH_MAX_DIGITS = 5; // this is an arbitrarily chosen value: private final static int MAX_MESSAGE_LENGTH = 65536 - LENGTH_MAX_DIGITS; private final ConnectionService ConnectionService; private final ClientConnectionHandlerListener listener; private final Hashtable sendMessages = new Hashtable(); private StreamConnection connection; private InputStream in; private OutputStream out; private volatile boolean aborting; public ClientConnectionHandler( ConnectionService ConnectionService, StreamConnection connection, ClientConnectionHandlerListener listener) { this.ConnectionService = ConnectionService; this.connection = connection; this.listener = listener; aborting = false; in = null; out = null; // Our caller uses method 'start' to start the reader // and writer. (This allows the ConnectionService a // chance to add us to its list of handlers before // the reader and writer start running.) } ClientConnectionHandlerListener getListener() { return listener; } public synchronized void start() { Thread thread = new Thread(this); thread.start(); } public void close() { if (!aborting) { synchronized(this) { aborting = true; } synchronized(sendMessages) { sendMessages.notify(); } if (out != null) { try { out.close(); synchronized (this) { out = null; } } catch(IOException e) { // there is nothing we can do: ignore it } } if (in != null) { try { in.close(); synchronized (this) { in = null; } } catch(IOException e) { // there is nothing we can do: ignore it } } if (connection != null) { try { connection.close(); synchronized (this) { connection = null; } } catch (IOException e) { // there is nothing we can do: ignore it } } } } public void queueMessageForSending(Integer id, byte[] data) { if (data.length > MAX_MESSAGE_LENGTH) { throw new IllegalArgumentException( "Message too long: limit is " + MAX_MESSAGE_LENGTH + " bytes"); } synchronized(sendMessages) { sendMessages.put(id, data); sendMessages.notify(); } } private void sendMessage(byte[] data) throws IOException { byte[] buf = new byte[LENGTH_MAX_DIGITS + data.length]; writeLength(data.length, buf); System.arraycopy(data, 0, buf, LENGTH_MAX_DIGITS, data.length); out.write(buf); out.flush(); } public void run() { // the reader // 1. open the streams, start the writer try { in = connection.openInputStream(); out = connection.openOutputStream(); // start the writer Writer writer = new Writer(this); Thread writeThread = new Thread(writer); writeThread.start(); listener.handleStreamsOpen(this); } catch(IOException e) { // open failed: close any connections/streams and // inform listener that the open failed close(); // also tells listener to delete handler listener.handleStreamsOpenError(this, e.getMessage()); return; } // 2. wait to receive and read messages while (!aborting) { int length = 0; try { byte[] lengthBuf = new byte[LENGTH_MAX_DIGITS]; readFully(in, lengthBuf); length = readLength(lengthBuf); byte[] temp = new byte[length]; readFully(in, temp); listener.handleReceivedMessage(this, temp); } catch (IOException e) { close(); if (length == 0) { listener.handleClose(this); } else { // we were in the middle of reading... listener.handleErrorClose(this, e.getMessage()); } } } } private static void readFully(InputStream in, byte[] buffer) throws IOException { int bytesRead = 0; while (bytesRead < buffer.length) { int count = in.read(buffer, bytesRead, buffer.length - bytesRead); if (count == -1) { throw new IOException("Input stream closed"); } bytesRead += count; } } private static int readLength(byte[] buffer) { int value = 0; for (int i = 0; i < LENGTH_MAX_DIGITS; ++i) { value *= 10; value += buffer[i] - ZERO; } return value; } private void sendMessage(OutputStream out, byte[] data) throws IOException { if (data.length > MAX_MESSAGE_LENGTH) { throw new IllegalArgumentException( "Message too long: limit is: " + MAX_MESSAGE_LENGTH + " bytes"); } byte[] buf = new byte[LENGTH_MAX_DIGITS + data.length]; writeLength(data.length, buf); System.arraycopy(data, 0, buf, LENGTH_MAX_DIGITS, data.length); out.write(buf); out.flush(); } private static void writeLength(int value, byte[] buffer) { for (int i = LENGTH_MAX_DIGITS -1; i >= 0; --i) { buffer[i] = (byte) (ZERO + value % 10); value = value / 10; } } private class Writer implements Runnable { private final ClientConnectionHandler handler; Writer(ClientConnectionHandler handler) { this.handler = handler; } public void run() { while (!aborting) { synchronized(sendMessages) { Enumeration e = sendMessages.keys(); if (e.hasMoreElements()) { // send any pending messages Integer id = (Integer) e.nextElement(); byte[] sendData = (byte[]) sendMessages.get(id); try { sendMessage(out, sendData); // remove sent message from queue sendMessages.remove(id); // inform listener that it was sent listener.handleQueuedMessageWasSent( handler, id); } catch (IOException ex) { close(); // stop the networking thread // inform that we got an error close listener.handleErrorClose( handler, ex.getMessage()); } } if (sendMessages.isEmpty()) { try { sendMessages.wait(); } catch (InterruptedException ex) { // this can't happen in MIDP: ignore it } } } } } } }
package example.btsppecho.client; public interface ClientConnectionHandlerListener { // The handler's accept and open of a new connection // has occurred, but the I/O streams are not yet open. // The I/O streams must be open to send or receive // messages. public void handleAcceptAndOpen(ClientConnectionHandler handler); // The handler's I/O streams are now open and the // handler can now be used to send and receive messages. public void handleStreamsOpen(ClientConnectionHandler handler); // Opening of the handler's I/O streams failed. The handler has // closed any connections or streams that were open. // The handler should not be used anymore, // and should be discarded. public void handleStreamsOpenError(ClientConnectionHandler handler, String errorMessage); // The handler got an inbound message. public void handleReceivedMessage( ClientConnectionHandler handler, byte[] messageBytes); // A message that had been previously queued for sending // (identified by id) by the handler, has been sent successfully. public void handleQueuedMessageWasSent( ClientConnectionHandler handler, Integer id); // The handler has closed its connections and streams. // The handler should not be used anymore, and should be discarded. // Only handlers which have previously called 'handleOpen' may // call 'handleClose', and only just once. public void handleClose(ClientConnectionHandler handler); // An error related to the handler occurred. The handler // has closed the connection, and the handler should no // longer be used. public void handleErrorClose(ClientConnectionHandler handler, String errorMessage); }
package example.btsppecho.client; import java.io.IOException; import javax.microedition.io.ConnectionNotFoundException; import javax.microedition.io.Connector; import javax.microedition.io.StreamConnection; import javax.microedition.io.StreamConnectionNotifier; import example.btsppecho.ClientForm; import example.btsppecho.LogScreen; public class ConnectionService implements Runnable { private final ClientForm listener; private final String url; private StreamConnectionNotifier connectionNotifier = null; private volatile boolean aborting; public ConnectionService(String url, ClientForm listener) { this.url = url; this.listener = listener; LogScreen.log("ConnectionService: waiting to " + "accept connections on '" + url + "'\n"); // start waiting for a connection Thread thread = new Thread(this); thread.start(); } public String getClientURL() { return url; } public void close() { if (!aborting) { synchronized(this) { aborting = true; } // Ideally, one might want to give the run method's // loop a chance to abort before calling the // subsequent close, but the run loop is anyways // likely to be sitting on the acceptAndOpen // (i.e. blocked). try { connectionNotifier.close(); } catch (IOException e) { // There is nothing very useful that // we can do for this case. } } } public void run() { aborting = false; try { connectionNotifier = (StreamConnectionNotifier) Connector.open(url); // It might useful in some cases to add a service to the // 'Public Browse Group'. For example by doing something // approximately as follows: // ----------------------------------------------------- // Retrieve the service record template // LocalDevice ld = LocalDevice.getLocalDevice(); // ServiceRecord rec = ld.getRecord(connectionNotifier); // DataElement element = // new DataElement(DataElement.DATSEQ); // // The service class for PublicBrowseGroup (0x1002) // is defined in the Bluetooth Assigned Numbers document. // element.addElement(new DataElement(DataElement.UUID, // new UUID(0x1002))); // // Add to the public browse group: // rec.setAttributeValue(0x0005, element); // ----------------------------------------------------- } catch (IOException e) { // ConnectionNotFoundException is an IOException String errorMessage = "Error while starting ConnectionService: " + e.getMessage(); listener.handleError(null, errorMessage); aborting = true; } catch (SecurityException e) { String errorMessage = "SecurityException while starting ConnectionService: " + e.getMessage(); listener.handleError(null, errorMessage); aborting = true; } while (!aborting) { try { // 1. wait to accept & open a new connection StreamConnection connection = (StreamConnection) connectionNotifier.acceptAndOpen(); LogScreen.log("ConnectionService: new connection\n"); // 2. create a handler to take care of // the new connection and inform // the listener if (!aborting) { ClientConnectionHandler handler = new ClientConnectionHandler(this, connection, listener); listener.handleAcceptAndOpen(handler); } // One could consider exiting the // ConnectionService when the Client // reaches the maximum number of allowed // open connections. In that case (i.e. // when the maximum number of possible // connections is already open), the // ConnectionService will not be able // to accept any new connections and one // might possibly want to consider whether // or not the ConnectionService thread // could then be terminated. // // However, existing connections can also // be disconnected (e.g. the Server is // terminated or closes some/all of its // existing connections). In that case, // one may want to keep the // ConnectionService alive and running: // in order to accept later new connections // without the need to restart the // ConnectionService or MIDlet. // // (This MIDlet uses the latter approach.) } catch (IOException e) { if (!aborting) { String errorMessage = "IOException occurred during " + "accept and open: " + e.getMessage(); listener.handleError(null, errorMessage); } } catch (SecurityException e) { if (!aborting) { String errorMessage = "IOException occurred during " + "accept and open: " + e.getMessage(); listener.handleError(null, errorMessage); } } } } }
package example.btsppecho.server; import java.io.InputStream; import java.io.IOException; import java.io.OutputStream; import java.util.Hashtable; import java.util.Enumeration; import javax.bluetooth.ServiceRecord; import javax.microedition.io.Connector; import javax.microedition.io.StreamConnection; import javax.microedition.io.StreamConnectionNotifier; import example.btsppecho.MIDletApplication; import example.btsppecho.LogScreen; public class ServerConnectionHandler implements Runnable { private final static byte ZERO = (byte) '0'; private final static int LENGTH_MAX_DIGITS = 5; // this is an arbitrarily chosen value: private final static int MAX_MESSAGE_LENGTH = 65536 - LENGTH_MAX_DIGITS; private final ServiceRecord serviceRecord; private final int requiredSecurity; private final ServerConnectionHandlerListener listener; private final Hashtable sendMessages = new Hashtable(); private StreamConnection connection; private OutputStream out; private InputStream in; private volatile boolean aborting; private Writer writer; public ServerConnectionHandler( ServerConnectionHandlerListener listener, ServiceRecord serviceRecord, int requiredSecurity) { this.listener = listener; this.serviceRecord = serviceRecord; this.requiredSecurity = requiredSecurity; aborting = false; connection = null; out = null; in = null; listener = null; // the caller must call method 'start' // to start the reader and writer } public ServiceRecord getServiceRecord() { return serviceRecord; } public synchronized void start() { Thread thread = new Thread(this); thread.start(); } public void close() { if (!aborting) { synchronized(this) { aborting = true; } synchronized(sendMessages) { sendMessages.notify(); } if (out != null) { try { out.close(); synchronized (this) { out = null; } } catch(IOException e) { // there is nothing we can do: ignore it } } if (in != null) { try { in.close(); synchronized (this) { in = null; } } catch(IOException e) { // there is nothing we can do: ignore it } } if (connection != null) { try { connection.close(); synchronized (this) { connection = null; } } catch (IOException e) { // there is nothing we can do: ignore it } } } } public void queueMessageForSending(Integer id, byte[] data) { if (data.length > MAX_MESSAGE_LENGTH) { throw new IllegalArgumentException( "Message too long: limit is " + MAX_MESSAGE_LENGTH + " bytes"); } synchronized(sendMessages) { sendMessages.put(id, data); sendMessages.notify(); } } private void sendMessage(byte[] data) throws IOException { byte[] buf = new byte[LENGTH_MAX_DIGITS + data.length]; writeLength(data.length, buf); System.arraycopy(data, 0, buf, LENGTH_MAX_DIGITS, data.length); out.write(buf); out.flush(); } public void run() { // the reader // 1. open the connection and streams, start the writer String url = null; try { // 'must be master': false url = serviceRecord.getConnectionURL( requiredSecurity, false); connection = (StreamConnection) Connector.open(url); in = connection.openInputStream(); out = connection.openOutputStream(); LogScreen.log("Opened connection & streams to: '" + url + "'\n"); // start the writer Writer writer = new Writer(this); Thread writeThread = new Thread(writer); writeThread.start(); LogScreen.log("Started a reader & writer for: '" + url + "'\n"); // open succeeded, inform listener listener.handleOpen(this); } catch(IOException e) { // open failed, close any connections/streams, and // inform listener that the open failed LogScreen.log("Failed to open " + "connection or streams for '" + url + "' , Error: " + e.getMessage()); close(); listener.handleOpenError( this, "IOException: '" + e.getMessage() + "'"); return; } catch (SecurityException e) { // open failed, close any connections/streams, and // inform listener that the open failed LogScreen.log("Failed to open " + "connection or streams for '" + url + "' , Error: " + e.getMessage()); close(); listener.handleOpenError( this, "SecurityException: '" + e.getMessage() + "'"); return; } // 2. wait to receive and read messages while (!aborting) { int length = 0; try { byte[] lengthBuf = new byte[LENGTH_MAX_DIGITS]; readFully(in, lengthBuf); length = readLength(lengthBuf); byte[] temp = new byte[length]; readFully(in, temp); listener.handleReceivedMessage(this, temp); } catch (IOException e) { close(); if (length == 0) { listener.handleClose(this); } else { // we were in the middle of reading... listener.handleErrorClose(this, e.getMessage()); } } } } private static void readFully(InputStream in, byte[] buffer) throws IOException { int bytesRead = 0; while (bytesRead < buffer.length) { int count = in.read(buffer, bytesRead, buffer.length - bytesRead); if (count == -1) { throw new IOException("Input stream closed"); } bytesRead += count; } } private static int readLength(byte[] buffer) { int value = 0; for (int i = 0; i < LENGTH_MAX_DIGITS; ++i) { value *= 10; value += buffer[i] - ZERO; } return value; } private void sendMessage(OutputStream out, byte[] data) throws IOException { if (data.length > MAX_MESSAGE_LENGTH) { throw new IllegalArgumentException( "Message too long: limit is: " + MAX_MESSAGE_LENGTH + " bytes"); } byte[] buf = new byte[LENGTH_MAX_DIGITS + data.length]; writeLength(data.length, buf); System.arraycopy(data, 0, buf, LENGTH_MAX_DIGITS, data.length); out.write(buf); out.flush(); } private static void writeLength(int value, byte[] buffer) { for (int i = LENGTH_MAX_DIGITS -1; i >= 0; --i) { buffer[i] = (byte) (ZERO + value % 10); value = value / 10; } } private class Writer implements Runnable { private final ServerConnectionHandler handler; Writer(ServerConnectionHandler handler) { this.handler = handler; } public void run() { while (!aborting) { synchronized(sendMessages) { Enumeration e = sendMessages.keys(); if (e.hasMoreElements()) { // send any pending messages Integer id = (Integer) e.nextElement(); byte[] sendData = (byte[]) sendMessages.get(id); try { sendMessage(out, sendData); // remove sent message from queue sendMessages.remove(id); // inform listener that it was sent listener.handleQueuedMessageWasSent( handler, id); } catch (IOException ex) { close(); // stop the networking thread // inform that we got an error close listener.handleErrorClose(handler, ex.getMessage()); } } if (sendMessages.isEmpty()) { try { sendMessages.wait(); } catch (InterruptedException ex) { // this can't happen in MIDP: ignore it } } } } } } }
package example.btsppecho.server; public interface ServerConnectionHandlerListener { // The handler's open succeeded. It can now be used for sending // and receiving messages. public void handleOpen(ServerConnectionHandler handler); // The hadler's open failed. It has closed any connections or // streams that were open. The handler should not be used anymore, // and should be discarded. public void handleOpenError(ServerConnectionHandler handler, String errorMessage); // The handler got an inbound message. public void handleReceivedMessage( ServerConnectionHandler handler, byte[] messageBytes); // A message that had been previously queued for sending // (identified by id) by the handler, has been sent successfully. public void handleQueuedMessageWasSent( ServerConnectionHandler handler, Integer id); // The handler has closed its connections and streams. // The handler should not be used anymore, and should be discarded. // Only handlers which have previously called 'handleOpen' may // call 'handleClose', and only just once. public void handleClose(ServerConnectionHandler handler); // An error related to the handler occurred. The handler // has closed the connection, and the handler should no // longer be used. public void handleErrorClose(ServerConnectionHandler handler, String errorMessage); }