Building Java ME Apps with Modern Tooling

Java ME (Micro Edition, also referred to as J2ME) is a platform for running Java applications on small resource constrained devices, such as feature phones from before the iPhone era. It is largely considered a dead platform with the advent of smartphones, and modern feature phones such as KaiOS use web technologies instead for their apps.

However it is likely that you or someone you know have one of these old phones lying around, and you may be interested in how you can make Java apps that will run on it. This post goes through how to build a simple Java ME app solely with modern tooling in a modern environment, without needing anything from the old Sun Java ME SDKs.

Requirements

  • You will need some distribution of OpenJDK 8, such as the Temurin builds for various platforms. You should ideally make the binaries available on the PATH (and check that it’s the right version with java -version), for convenience.
  • Also download MicroEmulator, because we will use their stub JARs that provides the Java ME APIs for compilation.
  • Also download ProGuard (I use 7.1.1 for this), which will be used for the preverification step.

The instructions in this blog post assume a Linux-y terminal environment, but you could also follow along with Git Bash or a similar environment on Windows.

Hello World

Here is a simple Hello World MIDlet that we will use. It will show a simple form using the LCDUI API with the text “Hello World!”:

import javax.microedition.midlet.MIDlet;
import javax.microedition.lcdui.Display;
import javax.microedition.lcdui.Form;

public class HelloMIDlet extends MIDlet {
    public void startApp() {
        Form form = new Form("Hello");
        form.append("Hello World!");
        Display.getDisplay(this).setCurrent(form);
    }
    public void pauseApp() {}
    public void destroyApp(boolean unconditional) {}
}

Save it as src/HelloMIDlet.java.

Compile the class

We will compile this with javac, but we first need the stub JARs from MicroEmulator available in the lib directory. Extract cldcapi11.jar and midpapi20.jar and put them in a lib directory next to the src directory.

The cldcapi11.jar provides the CLDC 1.1 API and midpapi20.jar provides the MIDP 2.0 API. MIDP 2.0 is circa 2002, and is probably what you want to use for widest compatibility, while it has some important features that MIDP 1.0 lacks.

Then we can compile the Java source file with the following command:

mkdir -p build/classes
javac -source 1.3 -target 1.1 -bootclasspath "lib/cldcapi11.jar:lib/midpapi20.jar" \
    -d build/classes src/HelloMIDlet.java
  • -source 1.3 sets the source compatibility to 1.3, which means that your source code will be Java 1.3 code.
  • -target 1.1 sets the target compatibility to 1.1, which means that the generated bytecode will be compatible with Java 1.1 (bytecode version 45.3). This is what is required for MIDP 2.0, which is probably what you want to target.
  • -bootclasspath specifies the libraries it should include on the classpath for compiling code, and specifically we want these libraries to override all the built-in libraries in OpenJDK 8 which are for the regular Java SE API.

(To compile multiple .java files in src, you would put $(find src -name "*.java") instead.)

Package the JAR

MIDlets need some metadata in the META-INF manifest file to work. Most of the fields are self-explanatory:

Manifest-Version: 1.0
MIDlet-Name: hello-midlet
MIDlet-Version: 1.0.0
MIDlet-Vendor: The Author
MIDlet-1: HelloMIDlet,/icon.png,HelloMIDlet
MicroEdition-Configuration: CLDC-1.1
MicroEdition-Profile: MIDP-2.0

For the MIDlet-1 field, the format is a comma-separated list of three values: The human-readable name of the MIDlet, the path to the icon inside of the JAR, and the fully qualified class name of the entrypoint class. Since the above Java code specifies no package, it will be in the default package at the root of the JAR.

Save this to resources/META-INF/MANIFEST.MF, creating a resources/ directory to house assets to put in the final JAR. Also put a 16x16 PNG icon in resources/icon.png. Then let’s package everything together into a JAR:

cp -r resources/* build/classes
jar cfm build/input.jar resources/META-INF/MANIFEST.MF -C build/classes .

Now we have a JAR file build/input.jar… But we need to preverify it before it can be run on a Java ME device.

Preverification

Preverification is a process required for the KVM (the minimal Java virtual machine used in Java ME) to ensure that the bytecode is valid. Traditionally this would be done by a preverify tool from the original Java ME SDK, but ProGuard supports doing the same thing and is more modern.

java -jar "lib/proguard.jar" \
    -injars "build/input.jar" -outjars "app.jar" \
    -libraryjars "lib/cldcapi11.jar" \
    -libraryjars "lib/midpapi20.jar" \
    -dontshrink -dontoptimize -dontobfuscate \
    -microedition

You can also use ProGuard to shrink, optimise and obfuscate your code to reduce the footprint of the resulting JAR, which is especially important for Java ME apps where the JAR size limit can be very strict. But for this example we disable all of that for now and just specify -microedition to do the preverification.

Now you have a app.jar file that can actually be run on a Java ME device or in an emulator. You can use MicroEmulator to run it, or something with more features like FreeJ2ME-plus, currently the best J2ME emulator for desktop platforms.

Hello World

Getting the JAR to your phone

Now that you have gotten it running in an emulator it would be nice to get it running on the real hardware. The feature phone I had lying around that’s the most functional is the Sony Ericsson S312, a budget feature phone from 2009. It even has a camera!

A Dawn Blue coloured Sony Ericsson S312 feature phone, on the screen is the photo viewer showing a selfie taken with the phone.

Java feature phones tend to have connectivity and storage mediums that are generally considered obsolete nowadays. My Sony Ericsson S312 has a proprietary charger cable with no USB on either end - supposedly you can use a special USB transfer cable, but that was a separate accessory that doesn’t seem worthwhile to obtain. In addition to this, the physical external memory storage is some strange card format that is distinct from any standard kind of SD card (M2 memory stick, not to be confused with the M.2 connector).

Bluetooth ended up being the easiest way to transfer files to and from the phone. After connecting the phone to my computer and installing bluez-obex for OBEX, used for transferring files over Bluetooth, I was able to explore the files on the phone like I had plugged it in via USB and then copy over the JAR file.

It’s quite slow - very slow in fact, but a simple app should only be a few kilobytes in size. Once transferred over, I was able to go into the file manager on my phone, find the JAR and then install it. After some time, it showed up in the list of games.

The games list on the Sony Ericsson S312, showing a new 'hello-midlet' app with a raccoon icon.

Sample app

After getting the Hello World working, I expanded upon it a bit to draw images to the screen. I used the same image that was used for the app icon, a raccoon pixel art. Then I made them bounce across the screen. Just something simple to get the satisfaction of seeing something moving on the screen.

A screenshot of the bouncing raccoons app in the FreeJ2ME emulator.

This code and the final build.sh script to build a functioning JAR of it can be found on GitHub for reference. One thing you might need to customise is the path to where OpenJDK 8 is installed - I just hardcoded it to where I have it on my system at the top of the buildscript:

export PATH="/usr/lib/jvm/java-8-openjdk/bin/:$PATH"

Limitations of the platform

I’d also like to end with a section of the limitations of the Java ME platform, of which there are many important ones to be aware of.

First of all, you will be writing Java 1.3 code. While the basic language features are the same, it is very limited compared to modern Java or even something like Java 8. In addition to this, the API that MIDP 2.0 provides is very limited compared to the Java SE API when it comes to classes in the standard library. A full reference of all the classes you have available is available in the old MIDP 2.0 API documentation.

Different phone manufacturers have different implementations of the Java ME platform, and some of them implement their own APIs on top of the standard ones. Developers back in the day would typically ship several different versions of their app for Nokia, Sony Ericsson and generic MIDP 2.0 baselines to make use of these additional features. While MIDP 2.0 is the common denominator, you may also find that there are some discrepancies between different phone manufacturers. A lot of work was made for Android to not make the same mistake, despite its own diverse ecosystem of devices.

As for the specs of the phones you are targeting… Well, they are very limited. One thing to note is that many older feature phones have no hardware accelerated FPU - while the float type is available in Java, it will be emulated in software and be very slow on devices without a FPU. Generally you would always want to use fixed point math when doing heavier calculations.

As the size of your app will also be constrained, general advise for size optimisation applies. You can use ProGuard to shrink your code, PNG optimisers to reduce the size of your assets, and try to minimise the amount of separate classes and image files you have to take the most advantage from the per-file compression of JAR files. Your Java code will likely end up looking the complete opposite of what is considered clean code by Java OOP standards, but that’s okay.

参与讨论