The test-develop cycle for writing J2ME applications can be significantly slowed down by the need to deploy the application onto the device. Given that most network operators charge extortionate data rates, the only real option is bluetooth deployment. In this post I present a solution that can be included as an Ant task. Note that it might not work for everyone as some operators and handset manufacturers intentionally disable bluetooth deployment of MIDlets.
In OS X, this is as simple as using the Bluetooth File Exchange application and sending both the jar and the jad (so that the digital signature can be used to set the correct permissions). However, this is very repetitive, non-scriptable and platform specific.
It occurred to me recently that the Bluetooth API for J2ME has implementations that work in J2SE… and there is no reason why I couldn’t write a Bluetooth MIDlet Deployer, for use in an ant task. In this post, I’ll give you the source and an example ant task for automating this annoying part of the J2ME development process. The source itself should work as a light introduction to putting multiple files on a device with the OBEX libraries of JSR-82.
Hat tip to Vlad Skarzhevskyy of Bluecove and MicroEmu who tells me that he has been using his Maven task obex-maven-plugin for 2 years.
UPDATE: (16th Jan 2008) This is now split across 3 files. The referenced files are available at BluetoothDeviceDiscover.java and BluetoothServiceDiscover.java. These are part of J2ME library I am working on. The source code to the file presented here is also available BluetoothDeploy.java.
First of all, you’ll have to get an implementation of JSR-82. I use Bluecove which works great on OS X and Windows. Avetana is available commercially for OS X. If you use Linux you may wish to check out BlueZ for the latest list of implementations, although future releases of Bluecove will support Linux.
Then you can bundle the 3 classes into a jar (or download my binary bluetoothdeploy.jar), invoking it using the command line
java -cp bluetoothdeploy.jar:jsr82.jar thinktank.j2me.j2se.BluetoothDeploy HWADDR app.jad app.jar
where HWADDR is your bluetooth device’s connection URL. This may be obtained by running the jar without any parameters.
On my phone (RAZR V3i), this works most of the time… but if I have an old copy of my jar/jad saved on the phone, it will throw an error if the jad is the first to arrive, or it will attempt to install the unsigned version if the jar is first to arrive. I typically clear out any old versions from the phone first.
If you’re using Ant, the following task should automate the process for you (make sure your paths are correct for your setup)
<!-- Deploy to a device using Bluetooth -->
<target name="deploy" depends="build">
<java fork="yes" classname="thinktank.j2me.j2se.BluetoothDeploy" classpath="bluetoothdeploy.jar:bluecove.jar">
<arg line="btgoep://0123456789ab:8;authenticate=false;encrypt=false;master=false" />
<arg line="app.jad" />
<arg line="app.jar" />
</java>
</target>
If you’ve set up the build task to build your preverified jar and jad… deployment is a breeze! In my next post, I’ll go into more detail about setting up your IDE (and ant) for J2ME development without the buggy EclipseME.
Note that you can use this for sending any kind of file… I just wrote it with jar/jad specifically in mind.
/*
* Copyright ThinkTank Mathematics Limited 2007
*
* This file is free software: you can redistribute it and/or modify it under the terms of
* the GNU Lesser General Public License as published by the Free Software Foundation, either
* version 3 of the License, or (at your option) any later version.
*
* This file is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
* PURPOSE. See the GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License along with this file.
* If not, see <http://www.gnu.org/licenses/>.
*/
package thinktank.j2me.j2se;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.List;
import java.util.Vector;
import javax.bluetooth.RemoteDevice;
import javax.bluetooth.ServiceRecord;
import javax.microedition.io.Connector;
import javax.obex.ClientSession;
import javax.obex.HeaderSet;
import javax.obex.Operation;
import javax.obex.ResponseCodes;
import thinktank.j2me.BluetoothDeviceDiscover;
import thinktank.j2me.BluetoothServiceDiscover;
/**
* Application for sending a file over bluetooth, perfect for automatically deploying J2ME
* MIDlets to devices as part of a J2ME Ant task.
*
* @author Samuel Halliday, ThinkTank Mathematics Limited
*/
public class BluetoothDeploy {
/**
* Warning, this is very slow.
*
* @param file
* @return the MIME type for the given file
* @throws IOException
*/
public static String getMimeType(File file) throws IOException {
// some typical cases that don't work
String name = file.getName();
if (name.endsWith(".jar"))
return "application/java-archive";
if (name.endsWith(".jad"))
return "text/vnd.sun.j2me.app-descriptor";
URL u = file.toURL();
URLConnection uc = u.openConnection();
return uc.getContentType();
}
public static void main(String[] args) throws IOException {
if (args.length < 2) {
// do a scan
BluetoothDeviceDiscover deviceDiscover =
new BluetoothDeviceDiscover();
@SuppressWarnings("unchecked")
Hashtable<String, RemoteDevice> devices = deviceDiscover.discover();
for ( String name : devices.keySet()) {
RemoteDevice device = devices.get(name);
// attribute 0x0100 is the attribute for the service name element
// 0x1105 is the UUID for the Object Push Profile
BluetoothServiceDiscover serviceDiscover =
new BluetoothServiceDiscover(device, 0x0100, 0x1105);
@SuppressWarnings("unchecked")
Vector<ServiceRecord> records = serviceDiscover.discover();
for ( ServiceRecord record : records) {
String url = record.getConnectionURL(
ServiceRecord.NOAUTHENTICATE_NOENCRYPT, false);
System.out.println(name + " " + url);
}
}
return;
}
// find the files
List<File> files = new ArrayList<File>(args.length);
for ( int i = 1; i <
args.length; i++) {
File file = new File(args[i]);
if (file.exists() &&
file.isFile())
files.add(file);
else
System.err.println(file + " is not a file, skipping.");
}
BluetoothDeploy deployer = new BluetoothDeploy(files);
// send them
deployer.deploy(args[0]);
}
private final List<File> files;
/**
* @param files
*/
public BluetoothDeploy(List<File> files) {
this.files = files;
}
/**
* Deploy the files to the given bluetooth address, using OBEX.
*
* @param address
* @throws IOException
*/
private void deploy(String address) throws IOException {
if (!address.startsWith("btgoep://"))
throw new IllegalArgumentException("address " + address +
" does not look like an OBEX address");
ClientSession cs = (ClientSession) Connector.open(address);
HeaderSet hs = cs.createHeaderSet();
hs.setHeader(HeaderSet.COUNT, new Long(files.size()));
HeaderSet response = cs.connect(hs);
if (response.getResponseCode() !=
ResponseCodes.OBEX_HTTP_OK) {
System.err.println("Failed to connect " +
response.getHeader(HeaderSet.DESCRIPTION));
cs.close();
}
try {
for (File file : files) {
System.out.println("Sending " + file.getName() + " to " +
address);
byte[] data = readFile(file);
HeaderSet header = cs.createHeaderSet();
header.setHeader(HeaderSet.NAME, file.getName());
header.setHeader(HeaderSet.TYPE, getMimeType(file));
header.setHeader(HeaderSet.LENGTH, new Long(data.length));
Operation putOperation = cs.put(header);
OutputStream outputStream = putOperation.openOutputStream();
outputStream.write(data);
outputStream.close();
putOperation.close();
}
} finally {
cs.disconnect(null);
cs.close();
}
}
/**
* @param file
* @return
* @throws IOException
*/
private byte[] readFile(File file) throws IOException {
FileInputStream stream = new FileInputStream(file);
byte[] bytes = new byte[(int) file.length()];
try {
stream.read(bytes);
} finally {
stream.close();
}
return bytes;
}
}
Vlad wrote:
January 1st, 2008 at 12:35 am