Porting Boardspace.net using Codenameone


Executive summary:

In two months of intense work, I ported a large java program to IOS and Android using codenameone, and maintained a shared code base with the starting version.   There were many speed bumps along the way, but the successful result did not entail any substantial rewrites or major bodies of new code.   After the initial effort to get it working, I ported a second (related) large program in about a month, spent another 4 months tuning, completing features, and adding touch screen friendly behaviors to both programs.

The Problem Space:

Boardspace.net uses a java client to allow users to play real time board games over the internet.  The core code that runs all of the games is packaged in a small number of jar files, about a megabyte in all, built from 160 source files containing 50,000 lines of Java.     The bulk of the porting effort is to retain the functionality of this core, so that about 75 individual games, consisting of a further 800 source files and 300,000 lines of Java could be used essentially unchanged.  The total amount of compiled code in the system (with 75 games) is about 4MB, plus a lot of graphics.   As a porting problem there are several strong constraints.
  • The biggest problem is the sheer size of the code.  Any porting approach that would require a substantial rewrite is a non-starter. 
  • Reliable TCP streams are required.  Http transactions are also used.
  • There is heavy use of custom graphics, constructed and scaled on the fly.

On the other side, non-constraints include:

  • Swing, fancy UI widgets, or third party libraries, are not required.  Standard Java APIs from the AWT era are all that are needed.
  • The games UIs are already laid out adaptively for varying screen size and geometry,  and the UI is already known to work with single-finger touch interfaces.  The physical parameters of tablets won't be a problem, except the physically small size on some devices.
  • There is no requirement to use platform specific capability (such as GPS) or services (such as embedding Ads).
In view of the above, codenameone looked pretty attractive; TCP sockets are supported,  the user interface API is similar to AWT, and it promised to provide IOS and Android ports without major rewrites or paradigm shifts. 
deployed windows
lobby and games in progress

The Plan:

My overall plan was to build the codenameone version so it shared all except the list of imports at the top of each file with the current code, so that in the future, the codename1 and standard java branches would be substantially identical.

  • core Java classes would have to be used "as is". Examples are Image,  Graphics, Font.  These are pervasive in all the UI code, and trying to replace or substantially alter them is not a good plan.

  • Where codenameone functionality is missing, or substantially different from standard Java, create adapter classes to either provide the missing functionality or bridge between what codenameone provides and standard Java.  Examples: Color,  Component (the base class for all windows), and Event.

  • Where adapting or using as-is is not possible, drop into platform-specific code which implements a new API that is neither java or codename1, but which does what is needed.  Examples; Rectangles and Font selection.  Here, it is necessary, edit the code to use the new API, but once edited it will be the same in all branches.

  • Initially, set aside all but one game.  All of the games are substantially similar, and should be used pretty much as-is if the core files are ported according to plan.

  • First get the whole body of the codename1 branch through the compiler.  Then get it working.  Then back port the codename1 version to standard java and get that working, iterate until both versions are working and substantially identical (except for "import" lines at the top of each file).  Isolate irreconcilable differences in a few files that are not intended to be identical.

the desired result
typical source compare between standard java and codename1 versions


Phase 1: getting through the compiler


Starting with a duplicate branch of my source hierarchy as a codenameone project, one file at a time, eliminate compiler errors.  This is a tedious and difficult process, because nothing works yet, nothing can be tested, and every error requires a treasure hunt and an educated guess how best to resolve it.   These guesses were frequently wrong, creating a backtrack, undo and re-edit cycle.

Two of the most difficult cases in this process proved to be Rectangle and Event.   These aren't particularly classed as bugs or problems, only as illustration of the kind of issue you can expect to contend with.

The class Rectangle proved to be one of those awful birthmarks that can never be erased. 

     Standard Java defines
        public integer x
        public double getX()

    Codenameone defines
       public int getX()

there is absolutely no way these can be reconciled.  Rectangles are so common in the UI, it's just not practical to replace them with an encapsulating class, and switching uniformly to the accessor function getX()  will cause all manner of calculations to switch to floating point in standard Java, causing compatibility nightmares.    Unfortunately rectangles are ubiquitous, and in the overall code base, thousands of instances of rectangle manipulation had to be manually edited.  Since my goal was to have one source, I created functions like  "public static int Width(Rectangle r)" with different implementations for Codename1 and standard Java.

The class Event, and all its subclasses, is another nasty case.  The concept is the same in Codename1 and standard Java, but the details of the event types and handlers differ wildly.  

I ended up creating a shadow hierarchy of common window types, "bridge.Container extends com.codename1.UI.Container" and so on.  The bridge classes implement the standard java "addxxListener" methods, and handle them using a  "MouseAdapter" class. 

MouseAdapter receives codename1 events, transforms them, and dispatches them to standard java event handlers.  The messiest part of this is that I couldn't simply implement bridge.Component and let the rest of the window hierarchy inherit from it.   In any case, all of the window types I use from AWT have quirks that are not implemented in the corresponding Codename1 classes, so I needed a bridge.xx class for every window type I use.
 

Phase 2: getting the Codename1 (Android) version working.

Since this was a large body of legacy code, there were few unit tests or test beds in place; but once the whole project compiles, it's simple to insert scaffolding to test various pieces. I had flagged a few key pieces as critical, likely to be troublesome, and separable from the overall application.

The first of these was networking.  The framework requires HTTP connections to negotiate login, collect information from the server site, and report problems back to be logged.  The games themselves require a TCP stream to be opened, and a continuous, reliable, low bandwidth chitchat with the server.  

HTTP connections, using a low level interface, turned out to work perfectly.

TCP sockets immediately proved to be unreliable.   My framework was already paranoid about the reliability of the streams, so the problems showed up immediately, rather than escaping into the operational code.  This prompted my first venture into the Codename1 VM.  I was able to find and fix the TCP problems in a few days, and got the changes pushed back into the main development line with remarkably little hassle.
networking

The second likely source of problems I had identified was image manipulation.  My code uses an unusual strategy to construct images with transparency for use at run time, and unless that graphics pipeline could be preserved, a potentially huge redesign might be required.   Specifically, my transparent images are constructed from pairs of ordinary jpg images, which requires efficient access to the original image RGB values, and the ability to construct new images from raw RGB values.   Fortunately this turned out to be close to a non-issue.  All the essential methods for loading, manipulating, scaling and displaying images existed and worked as advertised. image creation



There were many more steps like these to get the whole project working, but the underlying fact that the code was all known to work (in the original) greatly smoothed the process.  Hundreds of bridge classes with "Error, not implemented" methods were resolved one at a time, with the original behavior as a model for what needed to be done.

Phase 3: getting the Codename1 (IOS) version working.

Theoretically, after getting the Android port working, the IOS port should have been a matter of submitting a new build. In practice it didn't work out that way.  The IOS branch of Codenameone is a much less mature product, and there were quite  a few problems getting an initial version running at all, and then getting it running reliably. 

Sometimes these required very painful debugging because the IOS version crashed with no useful information about why.  For example, I spent 3 days generating builds containing less and less code, until I finally discovered than an extra "static main" method, left over from testing, was causing the whole program to crash immediately.  This has since been fixed; the build process flags it as  a build error.

On another occasion, IOS runs starting throwing errors, complaining of internal inconsistencies, which turned out to be that the implementation of "Random.nextLong()" was generating bad values.  Random had definitely been working the day before, and by the time I diagnosed and documented it, Random() mysteriously started working again.  Some development work at Codenameone had broken something for a few days, and I was the victim.

Several ordinary Java constructions related to interfaces, or default methods in interfaces, either generated build errors, or built but created buggy code that crashed or misbehaved.  These were mostly fixed by restructuring my source code to avoid the forms that the IOS branch of codenameone doesn't handle.  Some of these have since been fixed. 

Several IOS "code generator" errors caused misbehavior at  build time, or (worse) at runtime.  Some have been diagnosed and fixed, others have been worked around. 

Surprises along the way.

There were a number of noteworthy surprises that required changes along the way.

Missing and/or Mis-features

The Codenameone bug/fix cycle:

Support on the Codenameone forums and stack overflow has been generally good.   Posts always get a response, and usually good follow up.

During 7 months, I've filed 27 issues on Github, ranging from pretty trivial to extremely serious.  About half have been closed, most of those in a manner satisfactory to me.   There are some serious, probably systemic problems affecting other users, in the unresolved category.  I've been fortunate that I've been able to work around the issues that haven't been fixed.

I've made 11 pull requests ranging from minor to major bug fixes, and a few features big enough to affected multiple files.  The critical things were all accepted.

Overall, Codenameone staff has been helpful and responsive.  My general expectation is that the larger the company, the worse they treat small customers.   They're a small company, and still act like it.

Continuing annoyances

Conclusion

The proof of the pudding, as they say, is in the eating.  My project is definitely a difficult outlier for Codenameone, but I've now got two completed mobile apps

tantrix lobby
tantrix game boardspace game
The process to create them was sometimes rocky, but I can't imagine any similar process, using another implementation strategy, to be faster or easier.  I'm particularly pleased that I can now continue development in my traditional environment and simply deploy the result to IOS and Android.   It's also notable that I remain completely ignorant of all the details of Android OS and IOS.  Not needing to know is a good thing.

I can't emphasize enough that this would not have been possible with a closed source product.   The ability to immediately see what was going on "under the hood" is worth more than an army of tech support and a mountain of documentation.  The ability to actually fix bugs in the product, rather than just report them, definitely made the difference between a project that could keep moving forward and one that was stalled, waiting for a fix.