Posts related to the Android platform and Android software development.
tl;dr
APRSdroid is an Amateur Radio geo-location (APRS) app for Android licensed under the GPL. It started as a Scala learning experience two years ago, and has become a nice auxiliary income since, despite being Open Source and offering free downloads from the homepage. However, using Scala was not always the easiest path to go.
Project history
In mid-December of 2009 a HAM radio friend asked me: "it can't be too hard to make an Android APRS app, can it?" and because there was none yet, I started pondering. On December 31rd, instead of having fun and alcohol, I made the first commit. January 1st, at 03:37 local time, the first placeholder release 0.2 was created.
Over the course of the next weeks I discovered step-by-step what I had encumbered myself with. APRS is a protocol with a long history of organic growth, firmware limitation workarounds, many different ways to say the same thing (at least four just for position reports), countless protocol amendments and, to add insult to injury, base-91 ASCII encoding.
There was no Java code available to abstract away the protocol and to allow me to keep my sanity. So I read the spec, implemented position encoding, re-read the spec, implemented HTTP and UDP sending code, read the amendments, re-re-re-read the spec, etc.
The first usable release became 0.4 from the end of January. Because APRSdroid always was a leisure time project, phases of activity alternated with idle phases, and the app slowly grew features through 2010.
In early 2011, one year into it, I decided it was high time to make the project pay for itself. Real APRS gear (radio transceivers with GPS and packet radio support) was expensive (on the order of US$500), and the app was not only easier to use but also grew more and more features (except for direct access to the amateur radio spectrum, which does not work well on cell phones).
For some time, I went underground (by omitting git push to github and
only providing nightly builds to some friends) and worked on the code
behind closed doors (there were no other people contributing source
anyway).
On April 1st, I decided to fool the community a little, but was not taken too seriously. In the meantime I was polishing a 1.0 release for Android Market.
Income Report
On April 18th, 2011, APRSdroid 1.0 was "commercially" launched to Android Market. It was important for me to keep up the OSS spirit, so I kept providing source code and APK files from the home page. By buying the app instead of just downloading it, the users got automatic updates and a good feeling of supporting what they liked. Also, I did not make it too obvious in the Market description that the app can be downloaded for free as well ;-)
So far, this scheme has paid off very well. Since the beginning, more than 60% of all app users actually bought it (it is possible to monitor the global user activity on the APRS network), with an average of 350 sales per month, at 2.99€ / 4.49US$ (minus the Google "tax" and subject to local income taxes).
Most users I had contact with were ready to pay for the app even though they knew they could download it for free. Only one person so far demanded the free version to be made available on Android Market (using CAPS and three consecutive Twitter messages, though, so I did not feel too pressed).
So far, I invested the income into real APRS hardware, a Desire Z (or G2 or HTC Vision) and am eagerly awaiting the availability of ICS tablets, aiming at finally adding Fragments support to the app.
Scala + Android = Pitfalls
I decided to use Scala because I do my coding in
vim and Java is so crammed up with boilerplate
code that you can not sensibly use it with anything but a bloated
refactoring IDE. Another reason was that I do not like to repeat myself,
and Java provides even less usable abstractions than the good old C
language with its #define.
Scala was the language of the day, and I liked what I had read about it
so far. It sounded good enough for an experiment anyways.
Fortunately, people had already
figured
out
how to make it work on Android without carrying the bloat of the full
Scala runtime, so all I had to do were some refinements of build.xml.
The first warning sign was that I had to
override def $tag()
to work around an issue in the 2.7 beta compiler (IIRC). I complied by
cargo-cult-copying
the code from some place and moved on.
Another major issue was Android's
AsyncTask.
The API requires the developer to override protected SomeType
doInBackground(OtherType... params). Unfortunately, Scala has trouble with
overriding abstract varargs methods from Java,
and thus your app crashes with the opaque java.lang.AbstractMethodError:
abstract method not implemented exception. After triangulating the
source of the problem (who would have suspected a compiler bug?), a
wrapper class in Java
was written. Another bunch of days well spent.
One of my biggest hopes in Scala was to be able to reduce the boilerplate for Android's numerous single-abstract-method function parameter workarounds. Unfortunately, this problem is not yet solved in Scala, requiring to write explicit implicit conversion functions for each SAM type.
However, not everything was bad in Scala-land. Scala's traits allowed to
reuse the same code
in descendants of Android's
Activity,
ListActivity
and
MapActivity.
Working string comparisons, type based match and a huge amount of
syntactic sugar, added on top of a proper
ctags config,
actually made life good.
Further, the base-91 decoding was elegantly implemented as a map/reduce operation on the ASCII string. Other interesting solutions were: an UrlOpener for buttons and regex based packet matching (Warning: please do not try to understand the regexes!).
What remains in the end is build time (compilation + proguard), which is
subjectively higher for the Scala app than for a Java-only project of
comparable size. However, that might be due to a bug in my build.xml
and so far I was not impatient enough to investigate.
Conclusion
After two years, I am really glad to have gone this way. Learning Scala was a very pleasant experience, and it improved my ability to see problems from different points of view. However, it also significantly restricted the number of people able to contribute. Of over 500 commits to APRSdroid, only three were by another developer. The APRS parsing code has been replaced by javAPRSlib, a Java library with major contributions from several other people.
APRSdroid remains my only Scala project. My other Android projects are written in Java, either because I did not want to restrict contributors, or because I did not expect the Java code to become complex enough.
Would I start a new Scala project on Android? Probably no, as it is already hard enough to find people who would like to contribute to your pet project if it is written in Java. Scala makes that almost impossible.
Would I contribute to an existing Scala project? Yes!
P.S: Starting around March 2012, I will be looking for Android/IT-Sec related freelance jobs. Check github and Android Market for my other projects.
Sometimes it is plain frustrating to see how people try to be smarter than you and hard-code functionality which is (almost) impossible to override.
The rant (skip this section or don't complain!)
The one single most-frustrating feature of my HTC Dream/G1 phone looks like an attempt to be smart and save batteries: Whenever the phone connects to a WLAN, the 3G/mobile data connection is terminated.
"What's the problem?" you might retort. Now, most Internet applications are using TCP as their protocol of choice, and TCP maintains a connection bound to an IP address. Whenever you change your Internet access, you switch IP addresses and all existing TCP connections vanish. Your downloads are aborted, your SSH connections are closed, your IM session is terminated (or, even worse, it looks like online but is not).
The smart G* engineers of course have provided a way to detect the change of connectivity, using a NetworkConnectivityListener. They also probably implemented some really smart synchronization protocol into their binary-only applications, to improve the user experience.
However, they did not provide a way to prevent the deactivation of 3G data
service. They added in some complicated code to keep a 3G data connection open
to the MMS service, but the "normal" data session is just terminated whenever
a WLAN is found. This would not be as bad as it sounds if such a state change
would only happen twice a day (WLAN → 3G when you leave home; 3G →
WLAN when you come back). However, WLAN is eating your batteries really really
fast. Thus, the smart G* engineers made the phone automatically switch WLAN
off one minute after the display backlight is disabled. That means: you look
at the phone clock, it finds a WLAN, terminates all your connections, goes to
sleep, turns off WLAN, terminates all your connections, ... GOTO 10
To add insult to injury, they added
ConnectivityManager.requestRouteToHost(int networkType, int hostAddress),
which looks like it would set up a route to your destination using the
specified network interface.
Ha-ha!
Fail!
That function only works if the requested interface is already up!
For application developers, this basically means that they have to catch the
CONNECTIVITY_ACTION events, terminate the stale connection and open a new
connection, synchronizing all of the state between the client and server. This
of course implies that the application protocol must support
re-synchronization. HTTP for example provides the
Range: x-
header to continue a partial download. For Jabber, there is XEP-0198
(which is still missing in most implementations). Other protocols, like SSH,
are basically screwed.
For developers working at a mobile carrier, this is also bad news - there is no way to access the 3G data network when the user is surfing via WLAN.
Compared to this, the Symbian way of presenting the user a list of available networks when an application opens a socket is just a heavenly dream. Sorry, smart G* developers, you f'ed up this one.
The hack
After following the PdpConnectionConnectivityManagerConnectivityService twine of bloat, I saw some really fascinating code in ConnectivityService:
if (!mTestMode && deadnet != null) { if (DBG) Log.v(TAG, "Policy requires " + deadnet.getNetworkInfo().getTypeName() + " teardown"); toredown = teardown(deadnet); if (DBG && !toredown) { Log.d(TAG, "Network declined teardown request"); } }
Now, what it means is basically that if mTestMode is enabled, the old
connection is not terminated when a new one is established. mTestMode is set
as:
mTestMode = SystemProperties.get("cm.test.mode").equals("true") && SystemProperties.get("ro.build.type").equals("eng");
On a rooted phone, all we need to get it is to change /system/build.props,
reboot, and call requestRouteToHost().
Fortunately, the smart G* engineers fixed this evil exploit for the 2.0
release! GOTO 10 again!
