Thursday 13 November 2008

Getting USGS Earthquake Data into Android

This is the second instalment on how to display USGS Earthquake data on Android. The last post dealt with displaying the data, this post with getting it.

Here we use the USGS RSS feed containing the last day or so of earthquakes, which is available here. This is a standard XML document, which I decided to parse and store in a content provider, which hides a SQLite Database. I do not show the content provider here, but it stores some text, the magnitude and the location. I decided not to update the database, but to wipe it clean every time. While it is possible to add to existing earthquake data, care must be taken, because the USGS updates data on earthquakes it has reported on earlier, so for some quakes an insert is required, for others an update. Here I want to get only the latest data, so I can afford to wipe the data.

I decided to implement it as a service that schedules a task using the Java Timer class. My runnable gets the feed, parses it and builds up info on the earthquakes. This is then, in a separate step, inserted into the database, where it is picked up from the Overlay as explained in the last post. The parser is simply a SAX parser that implements a little state machine and accumulates all the earthquake info from the feed (some questions to USGS: why not put magnitude and time stamp into a more readable format in their own elements? I am pretty sure most people want this.)

The schedule task looks like this:

private class Task extends TimerTask {
URL url;
SAXParser sp;
XMLReader xmlReader;
USGSHandler handler;
public Task() {
handler = new USGSHandler();
try {
url = new URL("http://earthquake.usgs.gov/eqcenter/catalogs/1day-M2.5.xml");
SAXParserFactory spf = SAXParserFactory.newInstance();
sp = spf.newSAXParser();
xmlReader = sp.getXMLReader();
xmlReader.setContentHandler(handler);
xmlReader.setErrorHandler(handler);
} catch (IOException e){
Log.e(TAG, e.toString());
} catch (SAXException e){
Log.e(TAG, e.toString());
} catch (ParserConfigurationException e){
Log.e(TAG, e.toString());
}
}
@Override
public void run() {
Log.e(TAG, "refreshing earthquakes");
handler.reset();
try {
xmlReader.parse(new InputSource(url.openStream()));
} catch (IOException e) {
Log.e(TAG, e.toString());
} catch (SAXException e) {
Log.e(TAG, e.toString());
}
List quakes = handler.getQuakes();
getContentResolver().delete(EarthquakeContentProvider.EarthquakeData.CONTENT_URI, "1 = 1", null);
for (Earthquake e : quakes){
ContentValues values = new ContentValues();
values.put(EarthquakeContentProvider.EarthquakeData.TEXT, e.mText);
values.put(EarthquakeContentProvider.EarthquakeData.LON, Math.round(e.mLon * 1.e6));
values.put(EarthquakeContentProvider.EarthquakeData.LAT, Math.round(e.mLat * 1e6));
values.put(EarthquakeContentProvider.EarthquakeData.MAGNITUDE, e.mMagnitude);
getContentResolver().insert(EarthquakeContentProvider.EarthquakeData.CONTENT_URI, values);

}
}

}

As usual lousy error handling.

Displaying USGS Earthquake Data on Android


The United States Geological Survey (USGS) has a helpful RSS feed for earthquakes all around the globe. You can find the various feeds here. Here I illustrate how to load the data and display them as an overlay on Google maps on Android.

The principal way I have chosen is to have a service running in the background that periodically gets the latest earthquakes and stores them in a ContentProvider. The map overlay queries the ContentProvider for the earthquakes for the given map extent.

We start off with the display. The display is an overlay that redefines the draw() method. There are two steps: first to get all the earthquakes from the content provider, secondly drawing them (with a bit of hint on the magnitude).

Finding all earthquakes is easy, bar the pain of accommodating that map views can span the dateline, where the query to the content provider gets a bit more difficult. First I thought that simply changing the where clause according to the left hand side longitude being smaller than the right hand side longitude was sufficient, but then it turned out that one can zoom out so much that the right hand side longitude is larger than the left hand side, but the map still contains the dateline. I handle this case by checking if the longitude span is larger than 360 degrees. That means that earthquakes regardless of longitude need to be returned. (The same does not apply to latitude: Google maps always just cuts off at 80 degrees and does not wrap around. (For engineers of the Google phone the world seems to be a wrap-around band. They seem to smoke weird stuff down in California, but I guess it makes their life a bit easier.)

Once I have all the earthquakes, I iterate through the list and display them according to magnitude: one degree of magnitude gets one circle and using the zoom-level I scale this a little bit, so that the concentric circles get smaller if one zooms out, resembling a bit an impact zone.

This is the code in all its glory:

import android.database.Cursor;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Point;
import android.util.Log;

import com.google.android.maps.GeoPoint;
import com.google.android.maps.MapView;
import com.google.android.maps.Overlay;
import com.google.android.maps.Projection;

class EarthquakeOverlay extends Overlay {

public final static String TAG = "EarthquakeOverlay";

EarthquakeOverlay(){}

@Override
public void draw(Canvas canvas, MapView mapView,
boolean shadow) {
super.draw(canvas, mapView, shadow);
GeoPoint[] cornerCoords = MapUtils.getCornerCoordinates(mapView.getProjection(), canvas);
String selection;
if (mapView.getLongitudeSpan() >= 360 * 1e6){
selection = EarthquakeContentProvider.EarthquakeData.LAT + " < " + ((Integer)cornerCoords[0].getLatitudeE6()).toString() + " and " +
EarthquakeContentProvider.EarthquakeData.LAT + " > " + ((Integer)cornerCoords[1].getLatitudeE6()).toString();
} else if (cornerCoords[0].getLongitudeE6() < cornerCoords[1].getLongitudeE6()){
selection = EarthquakeContentProvider.EarthquakeData.LAT + " < " + ((Integer)cornerCoords[0].getLatitudeE6()).toString() + " and " +
EarthquakeContentProvider.EarthquakeData.LAT + " > " + ((Integer)cornerCoords[1].getLatitudeE6()).toString() + " and " +
EarthquakeContentProvider.EarthquakeData.LON + " > " + ((Integer)cornerCoords[0].getLongitudeE6()).toString() + " and " +
EarthquakeContentProvider.EarthquakeData.LON + " < " + ((Integer)cornerCoords[1].getLongitudeE6()).toString();
} else {
selection = EarthquakeContentProvider.EarthquakeData.LAT + " < " + ((Integer)cornerCoords[0].getLatitudeE6()).toString() + " and " +
EarthquakeContentProvider.EarthquakeData.LAT + " > " + ((Integer)cornerCoords[1].getLatitudeE6()).toString() + " and ( " +
EarthquakeContentProvider.EarthquakeData.LON + " > " + ((Integer)cornerCoords[0].getLongitudeE6()).toString() + " or " +
EarthquakeContentProvider.EarthquakeData.LON + " < " + ((Integer)cornerCoords[1].getLongitudeE6()).toString() + ")";
}

Projection p = mapView.getProjection();
Cursor cursor = null;
Paint paint = new Paint();
paint.setStrokeWidth(0);
paint.setStyle(Paint.Style.STROKE);
try {
cursor = mapView.getContext().getContentResolver().query(EarthquakeContentProvider.EarthquakeData.CONTENT_URI, null, selection, null, null);
if (cursor != null) {
cursor.moveToFirst();
while (!cursor.isAfterLast()){
GeoPoint gp = MapUtils.geopointFromLatLongE6(cursor.getInt(EarthquakeContentProvider.EarthquakeData.LAT_CN), cursor.getInt(EarthquakeContentProvider.EarthquakeData.LON_CN));
Point point = p.toPixels(gp, null);
Float magnitude = cursor.getFloat(EarthquakeContentProvider.EarthquakeData.MAGNITUDE_CN);
canvas.drawPoint(point.x, point.y, paint);
int zoomLevel = mapView.getZoomLevel() + 1;
for (int i = 0; i < magnitude; i++){
canvas.drawCircle(point.x, point.y, i * zoomLevel, paint);
}
cursor.moveToNext(); }
}
} finally {
if (cursor != null){
cursor.close();
}
}
}
}

How to get the data from USGS follows in one of the next posts...

Monday 3 November 2008

An Editable Waypoint Layer

A GPS without waypoints is like an Android without Activities, so here I illustrate what needs to be done to add an editable waypoint layer to Android.

First of all we need a waypoint content provider, which is just a wrapper around a database table. I am not planning to elaborate on this here as it is just another content provider.

We start with the map. The idea is just to tap on the map at the location where we want to position the waypoint. However, this conflicts with the usual use of touch to pan the map around. I worked around this by having a button on the map that puts the map in different modes. One is the panning mode, another the editing mode for (right now just) adding waypoints. At startup the map is in panning mode. Once a user clicks the button it toggles into editing mode and the next touch will create a waypoint and start the waypoint editor. This is how this looks:

This is all easily done within the onClick() method on the map:

protected synchronized void setDrawingMode(boolean drawing){
Button button = (Button)findViewById(R.id.modeButton);
if (!drawing){
drawingMode = false;
button.setText("Add Waypoint");
} else {
drawingMode = true;
button.setText("Pan");
}
}

@Override
public void onClick(View v) {
setDrawingMode(!drawingMode);
}


Now my onTouch() method distinguishes between the drawing and panning mode and when in editing mode takes the current location and call the waypoint editor (which is hard-coded here, would be better with intent negotation):

public boolean onTouch(View v, MotionEvent event){
if (drawingMode){
setDrawingMode(false);
MapView mv = (MapView)v;
Projection p = mv.getProjection();
GeoPoint point = p.fromPixels(Math.round(event.getX()), Math.round(event.getY()));
ComponentName comp = new ComponentName(this.getPackageName(), WaypointEditor.class.getName());
Intent intent = new Intent().setComponent(comp);
intent.putExtra(WaypointEditor.LAT, point.getLatitudeE6());
intent.putExtra(WaypointEditor.LON, point.getLongitudeE6());
startActivity(intent);
return true;
} else {
return false;
}
}

You might have noticed that I set the drawing mode to false as soon as the onTouch() call happens. This is because quite often I get more than one touch event from what my clumsy fingers believe is a single tap. We catch only the first, the last ones will be translated into panning events again.

I will not show the waypoint editor here, but waypoints are shown through an ItemizedOverlay, more or less as it comes out of the box. But waypoints should be editable as well, so I override the onTap() method on the ItemizedOverlay:

@Override
protected boolean onTap(int index) {
Waypoint i = items.get(index);
ComponentName comp = new ComponentName(WaypointEditor.class.getPackage().getName(), WaypointEditor.class.getName());
Intent intent = new Intent().setComponent(comp);
intent.putExtra(WaypointEditor.ID, i.id());
context.startActivity(intent);
return true;
}

Saturday 1 November 2008

Selecting WMS Layers

WMS (Web Map Services) provide a standard means of downloading maps from the internet and a MapActivity allows to layer maps obtained this way onto the Google baselayer. The mechanism of this was a bit explained in a previous post, but here I want to talk a bit about managing a number of user selectable WMS Layers.

  1. First of all WMS servers provide all the information about their maps in a capabilities document, an XML description of maps and their properties provided by that server. Every user-downloadable map comes as a Layer element in that document. So if we give users the ability to put in the base url of an WMS server, parsing the document for available layers is not so hard and can be done with an XmlPullParser. The base URL for such an WMS Layer is the URL in the picture, for the ICEDS server hosted at University College London. This is the complete URL for the capabilities document from that server: http://iceds.ge.ucl.ac.uk/cgi-bin/icedswms?service=wms&version=1.1.0&request=GetCapabilities.
  2. I have quite a simple pull parser that will not trap all errors and such, but is good enough for a start to extract all the layers. These are then displayed in a ListActivity, which has a ListAdapter to display the layer titles:

    public void onCreate(Bundle savedInstanceState){
    super.onCreate(savedInstanceState);
    setContentView(R.layout.wmslayerselector);

    Button b1 = (Button) findViewById(R.id.button);
    b1.setOnClickListener(new View.OnClickListener() {
    public void onClick(View arg0) {
    finish();
    }
    });

    Intent intent = getIntent();
    String wmsUrl = intent.getExtras().getString("wmsurl");
    WMSService wmsservice = new WMSService(wmsUrl);
    layers = wmsservice.getLayers();

    ListAdapter adapter = new ArrayAdapter(
    this, // Context.
    android.R.layout.simple_list_item_1,
    layers); // Pass in the cursor to bind to.

    // Bind to our new adapter.
    setListAdapter(adapter);
    }


  3. My onListItemClick() then inserts the selected layer into a ContentProvider for future use:

    @Override
    protected void onListItemClick(ListView l, View v, int position, long id) {
    WMSLayer layer = layers.get(position);
    Log.e(TAG, layer.toString());
    ContentValues values = new ContentValues();
    values.put(WMSLayerContentProvider.WMSLayerData.TITLE, layer.getTitle());
    values.put(WMSLayerContentProvider.WMSLayerData.NAME, layer.getName());
    values.put(WMSLayerContentProvider.WMSLayerData.BASEURL, layer.getBaseUrl());

    getContentResolver().insert(WMSLayerContentProvider.WMSLayerData.CONTENT_URI, values);}

    (There is a little issue with that WMS Server: technically, all layers with a NAME should be displayable, but this servers has some layers with names that are just used for grouping and will just display an error message when requested).

    Whenever I display a map, it now checks for those added WMS Layers:

    protected void loadWMSLayers(){
    List overlays = view.getOverlays();
    overlays.clear();

    Cursor wmsservers = null;
    try {
    String selection = WMSLayerContentProvider.WMSLayerData.VISIBLE + " = 1";
    wmsservers = getContentResolver().query(WMSLayerContentProvider.WMSLayerData.CONTENT_URI,
    null, selection, null, WMSLayerContentProvider.WMSLayerData.DEFAULT_SORT_ORDER);
    if (wmsservers != null){
    while (wmsservers.moveToNext()){
    WMSService wms = new WMSService(wmsservers.getString(WMSLayerContentProvider.WMSLayerData.BASEURL_CN));
    WMSLayer wmslayer = new WMSLayer(wms, wmsservers.getString(WMSLayerContentProvider.WMSLayerData.NAME_CN), wmsservers.getString(WMSLayerContentProvider.WMSLayerData.TITLE_CN));
    view.getOverlays().add(new WMSOverlay(wmslayer));
    }
    }
    } finally {
    if (wmsservers != null){
    wmsservers.close();
    }
    }
    }


Thursday 23 October 2008

Dealing with the Android Market

With the G1 now available, everyone is waiting for the Android Market to come online.

Google has taken the decision to only allow free applications onto the market until some time early in 2009, which has clearly pissed off some developers who had hoped to be able to charge from day one. I am not surprised by this decision. While there might be reasons of simply not getting the payment system ready (which should have been not too difficult for Google), I assume the main reason is that it is quite easy to get lots of bad publicity if you have crappy paid-for applications in the official market. While these applications are third-party, Google would take part of the blame. With free, free as in beer, stuff that is not likely to be the case as long as no application can do any damage.

Anyway, the reason I am mentioning this is that developers have now to decide what to do with their applications. They have the following options:
  1. Give away for free what they wanted to charge for. Bad idea, I think. Not because of the bad vibes that it will create later, but mainly because it gives competitors some three months to do it better.
  2. Just wait. Maybe not such a bad idea. One can see how it all shapes up, but one might lose momentum against others. Of the 10,000 or so developers at least one will have had a similar idea and they will occupy the high spot.
  3. Give away a 'beta' or cut-down version for now, with an upgrade later to a fully functional version. Possibly my choice - in combination with 2). It means not losing out on interest and also getting valuable feed-back.
Now how to do such a beta. 
  1. Time-limit the application: bound to piss people off, but not bad if one really has a valuable application.
  2. Tempt people to pay with better functionality. Of course one does not really know right now what that might be, so I decided to put a WebView into my application, so that I can change the details of what is available and also take a user then to whereever I decide to sell my app.
I embed a download link to the upgraded up, so that it would install on the emulator, but that hit a snag. For those who are not in the US and are thus pretty much unable to get a G1, the emulator unfortunately poses an unsurmountable restriction for installing apps from the web directly onto the emulator, as it is locked to only accept apps from the official store and the settings module is not compiled in. Here you can read more.

There is also a site with deals with embedding licensing information in the app: http://www.andlicensing.com/.


Sunday 19 October 2008

Dealing with large resources

I have a rather large set of resources for my application. At this point I do not really want to talk about what they are, but they could well be larger sets of built-in geographical points.

First of all why do I want this data to be inside my application when Android is, after all, a phone, ie a device where connectivity to the outside is essential to its functioning? There are several reasons:
  • my app is to be taken where connectivity might not be available, e.g. out in the sticks or when connectivity fails. 
  • I would like to avoid having to maintain a website that provides all this data to users on the go. The overall download would exceed the once-off one. Plus I have to deal with it.
  • I would like the app to be complete and functional from the very beginning, without the need to install additional data packs from the web.
Turns out that this is not as straight-forward as I thought.

A bad way of packaging the information is to hard-code it all in Java routines. But individual routines have a limit of 65635 for their size, and hacking this by breaking this up into individual routines then results in the translator to the DEX format to fail. Hacking is never a good idea...

A better way is to put it all into an XML resource file and extract the data like this:

XmlResourceParser xpp = mContext.getResources().getXml(R.xml.extensiondata);
try {
int eventType = xpp.getEventType();
while (eventType != XmlPullParser.END_DOCUMENT) {
if(eventType == XmlPullParser.START_TAG) {
if (xpp.getName().equals("p")){
String i = xpp.getAttributeValue(null, "h");
String j = xpp.getAttributeValue(null, "p");
insertData(db, i, j);
}
}
eventType = xpp.next();
}
} catch (XmlPullParserException e){
Log.e(TAG, e.toString());
} catch (IOException e){
Log.e(TAG, e.toString());
}

This assumed that my XML (for exhibition) contained elements named 'c' with attributes 'p' and 'h' and that I have a routine insertData() that puts this in the right table in my DB. This works fine up to a certain size, then I get the error:
'Data exceeds UNCOMPRESS_DATA_MAX', where the maximum size of a decompressed file for this purpose seems to be 1048576. Fair enough, there is a limit on memory on the unit.

You might at this point argue that that is too much data anyway. But I do not think I am actually that much implementing for the G1 phone. Quite frankly, if I was to pay that much money for a phone, I would buy an iPhone, as it has a higher sex appeal. In my view G1 is a poor attempt of competing with a design company that is way ahead of Google. The real appeal lies in the open platform and in the next year we will I would be surprised if we did not see devices much more powerful in computing power, but perhaps smaller screens.

Anyway. For now it is to downsize the XML files further, in a first attempt by breaking it into a few pieces.

Breaking my XML file down into three parts and iterating through them makes this work. That is good news as it shows that the limitation was alone at the inflated file size. 

I check with my database and the size of the database file is about 500KB, which is not bad considering how much stuff I have in there. The total size of the XML files is about the same, which is not so good, as it suggests that SQLite stores a lot of overhead: the payload size is about 150K bytes. But access speed is not bad on the emulator.

I will try to find a way of tweaking the size a bit and I will also need a good way of getting the data in at first start-up. At the moment I just go for a coffee when I load the DB. A real user would probably reboot the phone.


Tuesday 7 October 2008

Testing with the Android Framework

The second installment on testing with Android. There is a dearth of decent documentation (or indeed any documentation) out on how to test Android applications.

The best starting point is to have a look at the ApiDemos sample that comes with the SDK. It has a complete and working setup for a variety of tests. The first steps are to make sure that this actually works on your machine.

  1. First of all, build the officially supplied test suite. This does not work out of Eclipse as far as I can tell, so just go to a command line window and go to /samples/ApiDemos/tests and do a build with ant.
  2. You might find that the ApiDemos itself was not built before, so go there first.
  3. Once the ApiDemos is built, try building the test again.
  4. Now you need to install the ApiDemos and the tests. This is done by going into the respective bin directories and run adb install ApiDemos-debug.apk and adb install ApiDemosTest-debug.apk. This requires that the emulator is running. Mine is, as I started it from Eclipse.
  5. Now restart the emulator. This is important as otherwise it might not know that these new apks have been installed. I made the mistake of letting it run, and I always got an error when running with the suggested commands.
  6. Run something like 
    $ adb shell am instrument -w com.example.android.apis.tests/android.test.InstrumentationTestRunner

    The tests should now run (some fail for me, but the main machinery is up and running).

Now ist the time to get my own tests in:
  1. I recreate the directory structure for my tests from the Google template and copy the AndroidManifest.xml and build.xml. Both will need some modification to make sure that my classes will be tested.
  2. In the src directory I adjust the files for the top-level test runners and add a directory that will exercise my own TracklogViewer.
  3. The build with ant fails on a few points. -e option as usual gives a bit more information. Some directories need to be created by hand as they were expected but are not there.
  4. I install my own test apk on the emulator.
  5. I run the instrumentation.
  6. It fails. The reason, it transpires, is that I have not restarted the emulator.
  7. It finally runs. As the tests are pretty empty, there is no error. 
Now it comes to actually implementing a few tests. But that is another story.

Testing with Android

Testing with Android is a pain.

While the normal JUnit test does work, once one makes a few adjustments to the setup on Eclipse, this does not mean that any of the Android classes are available for these tests. So even if my classes are not Activities, Services etc, but plain old computations, they cannot be tested with JUnit as soon as I make use of any of the classes in Android.

I think that some of this reveals bad design decisions by those developing Android. Take android.graphics.Rect. This is a class that has keeps tab of four corner points plus gives some simple operations such as intersections. There would be nothing Android specific in them were it not for the methods to parcel it for the Android IPC. Would this not have been done better by exposing these methods with standard serialisation mechanisms via a custom toByteArray or some such?

Another solution would have been to have an Android-only jar which contains only the Android classes, with some things suitably stubbed out, so it can be linked into the testing framework on the host machine, rather than the Emulator.

Looks like I have to bite the bullet and make all my testing run on the emulator. At the same time I am making the same mistake as Google: not observing a strict layering principle which will give me much better portability in the future.

I have to go away and think about that.

Friday 3 October 2008

Using Large Images with Android: A no-no

I want to be able to display satellite images not from Google, but from other sources on my Android GPS application.

I have already shown how to build a simple WMS client to display online satellite images. However, the use I envision for my device (which might not be a phone), is to be able to display such images offline, from the built-in disk or an SDCard.

The built-in BitmapFactory has not real support for larger images, anything over a certain size will just fail to load with a memory error.

I first thought that something like Java Advanced Imaging JAI will do the trick. Well, it would, were it not that all support for this has been disabled in Android, certainly to reduce the memory footprint and stop people doing stupid things, like processing large images.

Every other library then either uses java.awt's BufferedImage (again not available on Android) or JNI with some underlying C, C++ or whatever implementation.

So, this is all a dead end. I have thought for a long time, that there is room for a really light-weight WMS server implemented in Java, or something equally lightweight for image processing. A library that does NOT do everything, but just the bare minimals.

Now I have a new direction. As large files are really a no-no on Android, there has to be some preprocessing to make the files smaller.
Fortunately, there is gdal2tiles, a program from the GDAL Suite for geospatial processing. It turns a georeferenced file into a tree of much smaller ones together with an XML based description. Parsing the XML file will allow me to pick the tiles I need to put the overlay image back together.

Stay tuned.

Thursday 2 October 2008

An Icon in the Status Bar

A little excursion here, but now I am putting an icon into the status bar of Android to indicate whether tracklogging is enabled or not.

This is done with the NotificationManager system service.

The interface seems to be easy enough, but there is once again a pitfall that is somehow not really documented. At first sight, this is the only thing one needs to do to get it running.

protected void notify(Context context, Boolean on){
NotificationManager nm = (NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE);
if (on){
Notification n = new Notification(R.drawable.locationloggerservice, "GPS Logging started", System.currentTimeMillis());
nm.notify(22, n);
} else {
nm.cancel(22);
}
}
However, doing this always raises an exception and looking at the message it becomes clear that the notification cannot live without an associated intent that is raised whenever someone clicks on the notification. So in this sense a 'read-only' notification that simply says the LocationLoggerService is up and running is not an option.  

We therefore need to wire an Intent into it. What should the intent be? Well, in this case, lacking any better I want to go to the preferences screen where the location logger can be turned off. This Intent has to be wrapped into a PendingIntent, which is a little parcel around all the information that can be handed back to the Notification application to launch an activity within my GPS application with the same rights as anything run within my own application natively.

Once we have created this PendingIntent and passed it into the notification, all is happy.
 protected void notify(Context context, Boolean on){
NotificationManager nm = (NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE);
if (on){
Notification n = new Notification(R.drawable.locationloggerservice, "GPS Logging started", System.currentTimeMillis());

ComponentName comp = new ComponentName(context.getPackageName(), GPSPreferenceActivity.class.getName());
Intent intent = new Intent().setComponent(comp);
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, Intent.FLAG_ACTIVITY_NEW_TASK);
n.setLatestEventInfo(context, "Location Logger Service", "GPS Tracklogging Enabled", pendingIntent);
nm.notify(22, n);
} else {
nm.cancel(22);
}
}


I call this notify() message from within my service manager that turns tracklogging on and off.

However, there remains one thing not so pretty. Now if I toggle in the preferences the Logging service on, I get the notification, and when I click on it, it takes me to the Preferences Screen again (I now have two of them stacked up). To make sure, only one is shown I also set another flag in the notification:


PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, Intent.FLAG_ACTIVITY_NEW_TASK + Intent.FLAG_ACTIVITY_SINGLE_TOP);


Now if my preferences screen is already top, it will not be launched again.

What is 22? Well, the notification needs an application specific unique int associated with it. 22 was as good as any. It is of course better to code this somewhere so uniqueness is guaranteed and a symbolic value can be associated with it.

Wednesday 1 October 2008

An OpenStreetMap ContentProvider for Android

The Android ContentProvider is a wrapper for data access that allows applications to hide the specifics behind a small number of calls that are modelled on a database interface.
However, there is nothing that limits a ContentProvider to being implemented on top of a DB: any data source will do.

The OpenStreetMap Project is a collaborative mapping project, collecting map data all around the world for access under a liberal license (attribution/shared-alike). There are several ways of getting hold of OSM data, but one is the OSMXAPI interface. It allows getting data based on a bounding box and selection details. Returned from a query is (if successful) an XML document, which we parse with SAX.

Here, we implement a simple lookup only interface to OSMXAPI, which looks up public toilets.

Fundamentally, the query method, which is really the only we implement, goes like this:
  1. First of all we expect in the selection parameter something like "amenity=toilets". OSMXAPI is not a database, but this is as close as a DB selection parameter as we get in this context.
  2. Secondly in the selectionArgs we expect the four corner coordinates for which to check. This is a bit a rough-handling of this parameter, but the ContentProvider interface is a bit too tightly coupled to DBs to do this differently.
  3. Then we construct the URL for the OSMXAPI call and retrieve the XML document. If anything goes wrong here, we will just return an empty cursor.
  4. Now we parse the document with an XML parser, scanning for the nodes which contain the lat/long positions of our points. Every node is inserted into the data of a MatrixCursor, which is a generalised cursor essentially containing a table of data over which the user can iterate. 
  5. We return this cursor to the user.

This is the full code for the ContentProvider:

package com.ucont;

import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;

import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;

import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.DefaultHandler;

import android.content.ContentProvider;
import android.content.ContentValues;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.net.Uri;
import android.util.Log;

public class OSMPointsContentProvider extends ContentProvider {

private final String TAG = "OSMPointsContentProvider";

public static final String AUTHORITY = "org.osm.data";
public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/osmdata");

private final String osmxapiserver = "http://www.informationfreeway.org/api/0.5/";

private class OSMPointCursor extends MatrixCursor {
public OSMPointCursor(String[] columnNames) {
super(columnNames);
}
}

protected String constructQueryUrl(String selection, Double left, Double bottom, Double right, Double top) {
String query = String.format("node[%][bbox=%f,%f,%f,%f]", selection, left, bottom, right, top);
String url = osmxapiserver + query;
return url;
}

protected String constructQueryUrl(String selection, String top, String left, String bottom, String right) {
String query = String.format("node[%s][bbox=%s,%s,%s,%s]", selection, left, bottom, right, top);
String url = osmxapiserver + query;
return url;
}


@Override
public int delete(Uri arg0, String arg1, String[] arg2) {
// TODO Auto-generated method stub
return 0;
}

@Override
public String getType(Uri uri) {
// TODO Auto-generated method stub
return null;
}

@Override
public Uri insert(Uri uri, ContentValues values) {
// TODO Auto-generated method stub
return null;
}

@Override
public boolean onCreate() {
// TODO Auto-generated method stub
return false;
}

private class OSMHandler extends DefaultHandler {
private OSMPointCursor cursor;
OSMHandler(OSMPointCursor cursor){
super();
this.cursor = cursor;
}
public void startElement (String uri, String name,
String qName, Attributes atts){
if (name.equals("node")){
Object[] columnValues = {atts.getValue("lat"), atts.getValue("lon")};
cursor.addRow(columnValues);
}
}
}

@Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
String[] columnNames = {"lat", "lon" };
OSMPointCursor result = new OSMPointCursor(columnNames);
URL url = null;
if (null == selection){
selection = "*=*";
}
try {
url = new URL (constructQueryUrl(selection, selectionArgs[0], selectionArgs[1], selectionArgs[2], selectionArgs[3]));
SAXParserFactory spf = SAXParserFactory.newInstance();
SAXParser sp = spf.newSAXParser();
XMLReader xmlReader = sp.getXMLReader();
OSMHandler handler = new OSMHandler(result);
xmlReader.setContentHandler(handler);
xmlReader.setErrorHandler(handler);
xmlReader.parse(new InputSource(url.openStream()));
} catch (MalformedURLException e){
Log.e(TAG, e.getMessage());
} catch (IOException e){
Log.e(TAG, "IOException " + e.getMessage());
} catch (SAXException e){
Log.e(TAG, "SAX " + e.getMessage());
} catch (ParserConfigurationException e) {
Log.e(TAG, "PC Exception " + e.getMessage());
}

return result;
}

@Override
public int update(Uri uri, ContentValues values, String selection,
String[] selectionArgs) {
// TODO Auto-generated method stub
return 0;
}

}
What are the lessons learned from this?
  1. The ContentProvider interface is a bit too closely aligned with databases. A more general concept, based on key/values to match would have been much better. Android developers have somehow recognised this and provided the MatrixCursor for a generalised return value.
  2. It would be nice if a ContentProvider could be queried which operations it supported. That way there would be a general way of implementing a read-only or write-only ContentProvider. The interface for insert does not allow a ContentProvider to throw an exception to indicate that inserts are not supported.
  3. It would be good to have things like cost functions on ContentProviders (e.g. the cheapest way of providing a certain content or the most up-to-date). In that sense ContentProvider is some two decades behind what has been done in terms of service provision negotiation in distributed systems. 
Next will be to display the retrieved data on a map.




Friday 26 September 2008

A Simple WMS Client For Android

Google's maps are not the only ones of interest: for those interested in science there are many more maps out there that show things like population density, cloud images, etc.

Some of these maps are available via Web Map Service WMS interfaces as specified by the OGC. WMS is essentially a standardised interface to maps on the Web that allows a client to specify the format (e.g. PNG), the size (e.g. 320x460) and the exact geographical coordinates: and what the user gets in return in an image.

That makes it very easy to overlay Google Maps with all sorts of imagery, particularly satellite imagery.

For this we start out defining a new Overlay class, WMSClient:


private class WMSOverlay extends Overlay {
@Override
public void draw(Canvas canvas, MapView mapView,
boolean shadow) {
super.draw(canvas, mapView, shadow);
WMSLoader wmsclient = new WMSLoader();
GeoPoint[] cornerCoords = MapUtils.getCornerCoordinates(mapView.getProjection(), canvas);
Bitmap image = wmsclient.loadMap(canvas.getWidth(), canvas.getHeight(), cornerCoords[0], cornerCoords[1]);

Paint semitransparent = new Paint();
semitransparent.setAlpha(0x888);
canvas.drawBitmap(image, 0, 0, semitransparent);
}

}


It is very simple: it somehow gets a Bitmap image from a class WMSLoader, to which I come in a second. Then it simply draws that image on the canvas, making it here semitransparent, so we see the underlying Google image.

To get the WMS image we first need to get the corner coordinates of the screen, which is not very difficult as the projection object has a translation function for this (0,0) is the left top corner, and the right bottom is (width(), height()).

Now to our WMSLoader:

import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;

import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.util.Log;

import com.google.android.maps.GeoPoint;

public class WMSLoader {
public static String TAG = "WMSLoader";

public Bitmap loadMap(int width, int height, GeoPoint ul, GeoPoint lr) {
URL url = null;

try {
url = new URL(String.format("http://iceds.ge.ucl.ac.uk/cgi-bin/icedswms?" +
"LAYERS=lights&TRANSPARENT=true&FORMAT=image/png&SERVICE=WMS&VERSION=1.1.1&REQUEST=GetMap&STYLES=&EXCEPTIONS=application/vnd.ogc.se_inimage&SRS=EPSG:4326" + "" +
"&BBOX=%f,%f,%f,%f&WIDTH=%d&HEIGHT=%d",
MapUtils.longitude(ul), MapUtils.latitude(lr),
MapUtils.longitude(lr), MapUtils.latitude(ul), width, height));
} catch (MalformedURLException e) {
Log.e(TAG, e.getMessage());
}
InputStream input = null;
try {
input = url.openStream();
} catch (IOException e) {
Log.e(TAG, e.getMessage());
}
return BitmapFactory.decodeStream(input);
}

This class here has most of the URL hard-coded for proof of concept. Here we get an image from the ICEDS server, an academic WMS Server at University College London. In this case the 'lights' layer, which shows night-time light pollution - an indication of populatio density (or industrial activity). We just compose the complete URL, then open the input stream, where we get the image and pass it back to our Overlay. Bingo.

(There is a thing about projections: Google Maps uses a form of Mercator, the one supported by the ICEDS server is WGS84. That means pixels will not lie exactly on top of each other, but it matters most when the image is zoomed out. We would need a server supporting the Google Maps projection to be exact....)

Anyway, this is how it looks on the screen:

Thursday 25 September 2008

Integrating Preferences

Every application needs to be configured somehow and fortunately in Android the preferences framework takes away most of the pain.

Here I want to have a single simple preference whether the tracklogging service of the GPS application is turned on.

First of all, a user dialog should be derived from PreferenceActivity. Then the onCreate() method can simply load a declaration of preferences from an XML file.

This is how the simplest onCreate() looks like:

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
addPreferencesFromResource(R.xml.preferences);
prefs = getSharedPreferences("com.ucont_preferences", Context.MODE_PRIVATE);
prefs.registerOnSharedPreferenceChangeListener(this);

}


That references a the file xml/preferences.xml in our resources. For our single preference this file can look like this:

<?xml version="1.0" encoding="utf-8"?>

<PreferenceScreen
xmlns:android="http://schemas.android.com/apk/res/android">

<PreferenceCategory
android:title="@string/preferences_title">

<CheckBoxPreference
android:key="@string/preferences_key_tracklogging_enabled"
android:title="@string/preferences_tracklogging_enabled"
android:summary="@string/preferences_tracklogging_enabled_summary" />

</PreferenceCategory>
</PreferenceScreen>


And after we added the new PreferenceActivity to the manifest and put some wiring it to make it accessible, it shows up, quite nicely, in the dialog:


All changes are automagically persisted to an XML file in /data/data/myapp/shared_prefs/ from where it is easy to download the file and see that the changes actually happened.

That we store the preferences does of course not mean that any action is taken. This will be covered soon.

Wednesday 24 September 2008

Starting an Android Service at Boot time

A service that has to be started manually is an oxymoron, so starting a service at boot time is for many applications a must.

My tracklogging service is such an example, which should run whenever the phone is turned on, so that one can refer to the route travelled later. A further example is the much touted CarbonFootprint, which again relies on accurate measurements all the time, and not just when the user has turned it on.

The last post detailed how the TrackloggingService worked, but started the service only when the main activity was launched. Now comes the time to hook it into the Android boot sequence. Here is how:

After boot completes the Android system broadcasts an intent with the action android.intent.action.BOOT_COMPLETED. And now all we need is an IntentReceiver, now called a BroadcastReceiver, to listen and act on it. This is how this class looks:


public class LocationLoggerServiceManager extends BroadcastReceiver {

public static final String TAG = "LocationLoggerServiceManager";
@Override
public void onReceive(Context context, Intent intent) {
// just make sure we are getting the right intent (better safe than sorry)
if( "android.intent.action.BOOT_COMPLETED".equals(intent.getAction())) {
ComponentName comp = new ComponentName(context.getPackageName(), LocationLoggerService.class.getName());
ComponentName service = context.startService(new Intent().setComponent(comp));
if (null == service){
// something really wrong here
Log.e(TAG, "Could not start service " + comp.toString());
}
} else {
Log.e(TAG, "Received unexpected intent " + intent.toString());
}
}
}

The key is of course the onReceive() method. I have decided to check that it is actually the Intent I am expecting, but otherwise it is straightforward starting the service and return.

The receiver needs to be declared in the manifest, e.g. with the following entry:

<receiver android:name=".LocationLoggerServiceManager"
android:enabled="true"
android:exported="false"
android:label="LocationLoggerServiceManager">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>


Furthermore this class listen to this specific event needs to be declared in the security settings:

<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />


That's it. Now the service will be started as soon a Android has finished booting.
Now we will need something to make this user-configurable...

A simple Android Tracklogging Service

So far, I had wired my tracklogging to the main Activity just to get going, but in real life this is supposed to happen behind the scenes, so a Service is the right thing to implement. This turned out to be easier that I thought.

First of all the class needs to be derived from Service, which is a bit like an Acitivty but without any (ever) visible action. As a service can potentially communicate with other applications it has the onBind() method, but we return always null here: there is nothing this service has to communicate about.

A second thing is that my tracklogging service needs to get the location updates, so I made it implement the LocationListener interface. I do not care that much about providers at the moment (with the Mock providers gone in 1.0 there is not so very much one can do anyway), I only implement onLocationChanged().

In this I simply stuff the data into my ContentProvider (which has been in some aspects covered earlier, but awaits a bigger write-up here soon) and I am done.
Fortunately, this operation should be very short, so there is no need to start another thread for this service.

This is how the service looks like:


import android.app.Service;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.Bundle;
import android.os.IBinder;
import android.util.Log;

public class LocationLoggerService extends Service implements LocationListener {

private final static String TAG = "LocationLoggerService";
LocationManager lm;
GPXWriter writer;

public LocationLoggerService() {
}

@Override
public IBinder onBind(Intent intent) {
return null;
}

@Override
public void onCreate() {
subscribeToLocationUpdates();
}

public void onLocationChanged(Location loc) {
Log.d(TAG, loc.toString());
ContentValues values = new ContentValues();
values.put(GPSData.GPSPoint.LONGITUDE, loc.getLongitude());
values.put(GPSData.GPSPoint.LATITUDE, loc.getLatitude());
values.put(GPSData.GPSPoint.TIME, loc.getTime());
getContentResolver().insert(GPSDataContentProvider.CONTENT_URI, values);
}
public void onProviderEnabled(String s){
}
public void onProviderDisabled(String s){
}
public void onStatusChanged(String s, int i, Bundle b){
}

public void subscribeToLocationUpdates() {
this.lm = (LocationManager)getSystemService(Context.LOCATION_SERVICE);
this.lm.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0, this);
}
}


Now I only need to start the service. In the next post I will cover how to start the service at boot time, but here, I still link it to my main Activity, where in its onCreate() method I add a call to start the service, like so:

componentName comp = new ComponentName(getPackageName(), LocationLoggerService.class.getName());
ComponentName service = startService(new Intent().setComponent(comp));


The last step is to add the service to the AndroidManifest.xml:

<service android:name="LocationLoggerService"
android:enabled="true"
android:exported="false"
android:label="LocationLoggerService"
/>

Android SDK 1.0 released

Finally, Google has taken Android (almost) out of the beta camp with the release of the 1.0 version of the SDK, the little _r1 appendix indicating that this is in fact probably only the release candidate 1. But at least this means that the API should be stable for now, where 'now' means for the time that the HTC phone is the only with Android to be formally announced.

The Android Developers Mailing List has seen a marked increase in traffic in the last week, it looks like everybody is getting excited that Android is finally starting to leave the drawing board.

The good news is that there have been few changes in the API since 0.9 and my code seemed to be working almost without a change. The only two lines I had to comment out, had to do with the test provider. For now I left them uncommented, they had been copied from some example and I am now not so sure what they were for. The development code runs without them, so maybe it is good the lines are gone:

//this.lm.setTestProviderStatus(LocationManager.GPS_PROVIDER,LocationProvider.AVAILABLE, null, System.currentTimeMillis());
//this.lm.setTestProviderEnabled(LocationManager.GPS_PROVIDER,true);

Tuesday 23 September 2008

Displaying the Copyright Information for Google Maps

A condition of using the Google Maps API in Android is the display of the Copyright notice as it is defined in the terms and conditions.
I have decided just to add another simple Activity that will just display the mandated string. This is a very simple Activity, which just uses a TextView and looks like this:

import android.app.Activity;
import android.os.Bundle;

public class CopyrightInfo extends Activity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.copyrightinfo);
}
}
The view is defined via XML in my layout/copyrightinfo.xml declaration:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:padding="20dip"
android:layout_width="fill_parent"
android:layout_height="fill_parent">

<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="@string/copyrightinfo_fulltext"/>

</LinearLayout>


I have decided to bypass the Activity matching mechanisms for Intents here and when it comes to launching the CopyrightInfo Activity, I have just hard-coded the class into my main Activity:


ComponentName comp = new ComponentName(this.getPackageName(),CopyrightInfo.class.getName());
startActivity(new Intent().setComponent(comp));


The whole shebang just looks like this now:


(By the way, does the ScreenCapture button on DDMS work for anyone? It seems a bit of functionality that is simply broken)

That was an easy lesson, but it is preparing the way for more displays from the GPS.

Monday 22 September 2008

Displaying Tracklogs on Google Maps (Part 2)

Now that we have a simple MapView centered on the start of the track it comes to displaying the tracklog.

This is done via an Overlay. I first tried to display the tracklog as points using the ItemizedOverlay, but I ran into the issue that more than a few points seemed to degrade performance very markedly, as it is possible to interact with each of these points, that is not very surprising. (The big question was how much the emulator reflects the speed we will get from a real phone: it might be even slower -- or, since it runs on hardware, not software, much faster.)

The second approach is just to subclass Overlay and override the draw method, painting onto the canvas.
There were several issues to deal with:
  • tracklogs can be very long and it might for performance reason not be advisable to display all of them.
  • tracklogs can contain points off the map, which need not (and should not) be displayed.
  • if we select only those points on the map, we want to draw lines between adjacent points, but not between segments.
  • we will want to colour trackpoints, name them etc, but we do not tackle this issue at this point.
A first rough cut of the code looks like this:
private class TrackOverlay extends Overlay {

public TrackOverlay() {
}

private Point pointFromCursor(Projection p, Cursor c){
Double lat = c.getDouble(1);
Double lon = c.getDouble(2);
Point point = p.toPixels(MapUtils.geopointFromLatLong(lat, lon), null);
return point;
}

private Boolean moreThanMinimumDistance(Point a, Point b){
double distance = java.lang.Math.sqrt((double)(a.x - b.x) * (a.x - b.x)) + ((a.y - b.y) * (a.y -b.y));
if (distance > 3){
return true;
}
return false;
}

@Override
public void draw(Canvas canvas, MapView mapView,
boolean shadow) {
super.draw(canvas, mapView, shadow);
Log.e(TAG, "drawing tracks");
Projection proj = mapView.getProjection();

GeoPoint ul = proj.fromPixels(0,0);
GeoPoint lr = proj.fromPixels(canvas.getWidth(), canvas.getHeight());

// select only points in currently displayed window.
// TODO: this has the problem that we cut off the trail at the edges of the image.
// TODO: Ideally we would like to select one point further
String selection = GPSData.GPSPoint.LONGITUDE + " > " + ((Double)(ul.getLongitudeE6() / 1e6)).toString() + " and " +
GPSData.GPSPoint.LONGITUDE + " < " + ((Double)(lr.getLongitudeE6() / 1e6)).toString() + " and " + GPSData.GPSPoint.LATITUDE + " < " + ((Double)(ul.getLatitudeE6() / 1e6)).toString() + " and " + GPSData.GPSPoint.LATITUDE + " > " + ((Double)(lr.getLatitudeE6() / 1e6)).toString();

long points = 0;
Point previousPoint = null;
Integer previousId = 0;

Vector vector = new Vector();

Cursor cursor = getContentResolver().query(GPSDataContentProvider.CONTENT_URI, null, selection, null, null);

cursor.moveToFirst();
do {
// we do not draw isolated points here
if (null == previousPoint){
previousId = cursor.getInt(0);
previousPoint = pointFromCursor(proj, cursor);
continue;
}
Integer currentId = cursor.getInt(0);
if (previousId + 1 == currentId){
// points follow each other, so draw a line segment, if distance is
// large enough
Point currentPoint = pointFromCursor(proj, cursor);
if (moreThanMinimumDistance(previousPoint, currentPoint)){
vector.add(previousPoint.x);
vector.add(previousPoint.y);
vector.add(currentPoint.x);
vector.add(currentPoint.y);
points +=1;
previousPoint = currentPoint;
} else {
// distance is not large enough, just move on
}

} else {
// we are at a break in the track, no line to be painted
previousPoint = pointFromCursor(proj, cursor);
}
previousId = currentId;
} while (cursor.moveToNext() && points < i =" 0;" paint =" new">
First of all, from the ContentManager we only get those points that fall onto the currently displayed screen. There are some pitfalls to this:
  1. actually what we want is all points on the screen plus those that precede and follow them so the tracklog will be drawn to the edge of the screen. This could almost be done by selecting a slightly larger window for the selection, but there is no guarantee that the first outlying points will be in. This is yet to be resolved.
  2. what we get are potentially track-segments where the track meanders on and off the screen. To test for this the code relies on the autonumbering of IDs in the DB: where there is a gap between painted points, there is a gap in the track and we do not draw a line.
  3. depending on the zoomLevel, a lot of different geographical points (GeoPoints) will fall onto the same point on the screen and drawing a line from a to a is pointless and expensive. With the moreThanMinimumDistance I compute a simple pixel Pythagoras and exclude those points that are below a certain distance. This will be a bit inaccurate, but difficult to see on the real unit.
  4. I arbitrarily limit the number of points drawn to some X. This will at one point become a configuration issue, settable for preference and processing power of the unit.

Lastly, I add my new overlay to the MapView with the following line:

view.getOverlays().add(new TrackOverlay());
This is now how it all looks (with a tracklog over Cuba, where GPS are illegal):

Displaying Tracklogs on Google Maps (Part I)

The last couple of blogs where about how to store GPS information on an Android phone. Now it is time to pull the information out again and display it on top of a Google Map layer.

First let's start displaying a map on the phone. Earlier versions of the Android documentation had a dedicated tutorial on displaying maps, but the tutorial link that is still in the documentation is broken. Things have changed a bit since the early versions, so I guess Google will put out a new tutorial in due course. But it means that one has to work from the documentation alone.

Anyway, displaying a map is displaying a MapView, which can only be done inside a MapActivity. The reason is that the MapActivity handles all the back-ground threads needed to do (p)re-loading of Map tiles at runtime, significantly more work that a normal Activity has to do.

As with all views, MapViews can be constructed entirely programmatically or declaratively via XML. Here for simplicity's sake, we use the second approach and mix it with a bit of programming. Displaying a naked Google Map is as easy as installing the view:


<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<com.google.android.maps.MapView android:id="@+id/map"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:apiKey="myKey"
android:clickable="true" />
<LinearLayout android:id="@+id/zoom"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_alignParentLeft="true" />
</RelativeLayout>

Here we have already done more than the minimum, as in addition to the map view we have already defined a view element that will hold the zoom elements of the map. We store this as a resource with the name mapview.xml in the layout folder.

Now we need a MapActivity to display this view. The one thing we have to override here is the onCreate() method, which can be as simple as this:

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.mapview);
return;
}


This displays us the basic map, but has nothing to do with our tracks so far. The next steps are first to center the map on our tracklog. A very simple (expensive and possibly buggy) way is to get all the trackpoints and just center the map on the first of them.

Now our onCreate method gets a bit longer:

public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.mapview);
view = (MapView)findViewById(R.id.map);
Cursor cursor = getContentResolver().query(GPSDataContentProvider.CONTENT_URI, null, null, null, null);
cursor.moveToFirst();
Double lat = cursor.getDouble(1);
Double lon = cursor.getDouble(2);
cursor.close();

view.getController().setCenter(MapUtils.geopointFromLatLong(lat, lon));
view.getController().setZoom(12);
}


We get the tracklog data via a ContentProvider, which in its unfinished rudimentary form was covered earlier and will be covered in more detail later. Here we get the first point, convert it with a utility function into a GeoPoint and center the map on it, also setting the zoom level. Now we get a map centered on top of the beginning of the track, but no tracklog. That will come in the next installment.

Oh, one more thing. Just like me, you might run into the trouble that all your MapView displays is an empty map with a grid overlay. That is because by default your application does not have permission to access the internet. To enable Internet access you will have to declare this with a directive in the AndroidManifest.xml file, with this line:

<uses-permission android:name="android.permission.INTERNET" />


For a real application, you will also have to consider that there are additional licensing restrictions on the use of Google Maps and its API, as well as the obligation to put a copyright notice into your application. The details for this are on the Android Terms and Conditions Page.

Friday 19 September 2008

Writing to an SD Card in Android

The next task was to write GPS data to an SD card, so it can be pulled out later easily and independently of Android. This is not all quite there yet, but for now this is a "Hello world" example for writing to a card.

Essentially, writing to a file on an SD card is like all Java write operations, the only difference being that we first need to find the root of the SD card. This is done with a call to the android OS Environment:

try {
File root = Environment.getExternalStorageDirectory();
if (root.canWrite()){
File gpxfile = new File(root, "gpxfile.gpx");
FileWriter gpxwriter = new FileWriter(gpxfile);
BufferedWriter out = new BufferedWriter(gpxwriter);
out.write("Hello world");
out.close();
}
} catch (IOException e) {
Log.e(TAG, "Could not write file " + e.getMessage());
}


I would have thought that the root.canWrite() would return a false if there is no writable SD card there, but not so. I get the IOException when constructing the FileWriter.

In DDMS one can see that the /sdcard is read-only. Essentially there is not yet an SD card in the slot. First we need to create one. This is done with the
mksdcard command in tools, like this
mksdcard 512M sdcard.iso

Now we have a file containing a DOS file system (yes, it is good old FAT32). Now we need to tell the emulator to load this instead of the system card. This is done with the -sdcard option. (In eclipse go to Run->Debug Dialog and add the -sdcard to the options and restart the emulator).

Now the above code works and creates and writes a file to our virtual SD Card.

This can be read with the adb pull command:
adb pull /sdcard/gpxfile.gpx gpxfile.gpx

This has copied the file from the virtual card onto the filesystem and we can just read the content of the file with 'more'.

This was essentially an exercise in RTFM and you can read how to do this also at the official documentation.

Thursday 18 September 2008

Getting Locations

I had toyed with Android when it first came out, but now a lot has changed again. Hopefully for the better, but it looks like my old code is just ripe for the scrap-heap. That is good and bad. Good because it allows me to start from scratch and bad because I have to start from scratch.

The first trouble I run into is that somehow the location provider is not accessible, the call to

(LocationManager)getSystemService(Context.LOCATION_SERVICE);


returns null. It turns out that the access to these requires some security settings in the androidManifest.xml:

<uses-permission name="android.permission.ACCESS_LOCATION"/>
<uses-permission name="android.permission.ACCESS_GPS"/>
<uses-permission name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission name="android.permission.ACCESS_MOCK_LOCATION"/>


Not quite 100% sure which one control which exactly, but the combination of them all seems to do the trick. Fine-tuning will come later.

With this set, at least I get a location manager now. And I get a list of all the available providers, which on the emulator is just one with the name GPS. But the call to getLastKnownLocation just returns null again and running it in a loop does not do any good either even when trying to get data into the emulator using the console. It somehow transpires that this call does not really do what one would expect. It seems to only give locations if there is a subscription to location updates is set up with a LocationListener. There is a thread on the android developer groups but that seems to go into the wrong direction, as it is not strictly required to have a separate thread for all this.

It is enough to have your activity implement the LocationListener and its four abstract methods. Now I have my very first logger that prints the current location onto the screen:


import android.app.Activity;
import android.content.Context;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.location.LocationProvider;
import android.os.Bundle;
import android.util.Log;
import android.widget.TextView;


public class GPSLogger extends Activity implements LocationListener {
String TAG = "GPSLogger";

LocationManager lm;
TextView tv;
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
this.getLocation();
tv = new TextView(this);
tv.setText("Waiting");
setContentView(tv);
}


public void onLocationChanged(Location loc) {
Log.e(TAG, loc.toString());
tv.setText("Location " + loc.toString());
}
public void onProviderEnabled(String s){
}
public void onProviderDisabled(String s){
}
public void onStatusChanged(String s, int i, Bundle b){
}

public void getLocation() {
this.lm = (LocationManager)getSystemService(Context.LOCATION_SERVICE);
this.lm.setTestProviderStatus(LocationManager.GPS_PROVIDER,LocationProvider.AVAILABLE, null, System.currentTimeMillis());
this.lm.setTestProviderEnabled(LocationManager.GPS_PROVIDER,true);
this.lm.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0, this);
}
}


Not fantastic, but the first steps are always the most difficult.

Storing GPS points in a SQLite DB

Now that I managed to get data from a mock provider, the next task was to store the data in a database, so that I have a rudimentary GPS data logger.

There are two approaches for this: one is to simply write the stuff into a DB (or a file), the other is to implement a ContentProvider. The second approach is a bit more sophisticated, but it is where I will be heading anyway, plus there seem to be more working examples on the net.

Google provides the Notepad application as an example for a ContentProvider and I started with that code, chopping and changing as I went along. There is also a step by step guide in the online documentation.

I left most of the methods of the ContentProvider empty at the moment, only implementing the insert method, so I have a write-only, read-never DB, which is not terribly useful. Getting the data out will be the next step.
This is how the ContentProvider code looks right now:
package com.ucont;

import android.content.ContentProvider;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import android.database.sqlite.SQLiteOpenHelper;
import android.net.Uri;
import android.util.Log;

public class GPSDataContentProvider extends ContentProvider {

private static final String TAG = "GPSDataContentProvider";

private static final String DATABASE_NAME = "gpsdata.db";
private static final int DATABASE_VERSION = 2;
private static final String POINT_TABLE_NAME = "gpspoints";

public static final String AUTHORITY = "com.ucont";
public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/gpspoint");

/**
* This class helps open, create, and upgrade the database file.
*/
private static class DatabaseHelper extends SQLiteOpenHelper {

DatabaseHelper(Context context, String name) {
super(context, name, null, DATABASE_VERSION);
}

@Override
public void onCreate(SQLiteDatabase db) {
try {
Log.i(TAG, "Creating table " + POINT_TABLE_NAME);
db.execSQL("CREATE TABLE " + POINT_TABLE_NAME + " ("
+ GPSData.GPSPoint._ID + " INTEGER PRIMARY KEY,"
+ GPSData.GPSPoint.LATITUDE + " REAL,"
+ GPSData.GPSPoint.LONGITUDE + " REAL,"
+ GPSData.GPSPoint.TIME + " INTEGER"
+ ");");
} catch (SQLiteException e) {
Log.e(TAG, e.toString());
}
}

@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
Log.w(TAG, "Upgrading database from version " + oldVersion + " to "
+ newVersion + ", which will destroy all old data");
db.execSQL("DROP TABLE IF EXISTS " + POINT_TABLE_NAME);
onCreate(db);
}
}

private DatabaseHelper mOpenHelper;

public boolean onCreate() {
mOpenHelper = new DatabaseHelper(getContext(),DATABASE_NAME);
return true;
}


@Override
public int delete(Uri arg0, String arg1, String[] arg2) {
// TODO Auto-generated method stub
return 0;
}


@Override
public String getType(Uri uri) {
Log.i(TAG, "getting type for " + uri.toString());
// TODO Auto-generated method stub
return null;
}


@Override
public Uri insert(Uri uri, ContentValues values) {
Log.e(TAG, "inserting value " + values.toString());

SQLiteDatabase db = mOpenHelper.getWritableDatabase();
long rowId = db.insert(POINT_TABLE_NAME, "", values);
if (rowId > 0) {
Uri noteUri = ContentUris.withAppendedId(GPSDataContentProvider.CONTENT_URI, rowId);
getContext().getContentResolver().notifyChange(noteUri, null);
return noteUri;
}

throw new SQLException("Failed to insert row into " + uri);
}


@Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
// TODO Auto-generated method stub
return null;
}


@Override
public int update(Uri uri, ContentValues values, String selection,
String[] selectionArgs) {
// TODO Auto-generated method stub
return 0;
}
}



I was a bit stumped for a while that the onCreate call for the DB, which creates the table was called whenever I started the app. This is a topic also covered in the newsgroups, but I think that debate is a bit a red herring. I have not 100% pinned it down, but I have the feeling that the onCreate is called whenever my app has been recompiled and is in this sense a new app. Now that I am stable with this bit of code, the onCreate call is not fired every time. Nevertheless I have the create call in a try catch block, to trap the recreation of the table.

The other thing I am not 100% sure about is the second parameter to the insert method, a string. I have for now just left it empty, expecting an exception, but I do not get one. There might be something lurking there.

The other class I have is for the table definition, here in a class called GPSData:

package com.ucont;

import android.net.Uri;
import android.provider.BaseColumns;

public final class GPSData {
public static final String AUTHORITY = "com.ucont";

// This class cannot be instantiated
private GPSData() {}

/**
* GPS data table
*/
public static final class GPSPoint implements BaseColumns {
// This class cannot be instantiated
private GPSPoint() {}

/**
* The content:// style URL for this table
*/
public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/gpspoint");

/**
* The MIME type of {@link #CONTENT_URI} providing a track (list of points).
*/
public static final String CONTENT_TYPE = "mime/text";

/**
* The MIME type of a {@link #CONTENT_URI} sub-directory of a single point.
*/
public static final String CONTENT_ITEM_TYPE = "";

/**
* The default sort order for this table
*/
public static final String DEFAULT_SORT_ORDER = "modified DESC";

public static final String LONGITUDE = "longitude";
public static final String LATITUDE = "latitude";
public static final String TIME = "time";
}
}


This was mostly copied from the notepad application and as I am not yet taking any data out all the content type definitions are not right. The only stuff used pretty much are the column names, latitude etc.

Now how is all this called?

My old onLocationChange in my main activity now stores the new location like this:

public void onLocationChanged(Location loc) {
Log.e(TAG, loc.toString());

ContentValues values = new ContentValues();

Double lon = loc.getLongitude();
Long time = loc.getTime();
values.put(GPSData.GPSPoint.LONGITUDE, loc.getLongitude());
values.put(GPSData.GPSPoint.LATITUDE, loc.getLatitude());
values.put(GPSData.GPSPoint.TIME, loc.getTime());
getContentResolver().insert(GPSDataContentProvider.CONTENT_URI, values);
}

The next steps will be to get the data out again and to store them in a GPX file on the SD card. Only once that is done can I be sure that the data is actually in the DB.

A GPS with Android: the first steps

I am fed up with my Garmin GPS units. Not so much with their hardware, but with their closed software. Some people have cracked how to make maps for Garmin a while ago and turning open data into open maps was good fun for a while. But so many things remained difficult, impossible or just a pain in the back. The new units have plenty of space and reasonable processing power, so why not be able to have satellite images on the units for places other than the US? Why not better ways of managing tracklogs? The pains have become just too much.

So I am embarking on turning a Android phone into a GPS unit. I have done enough programming to know that my programs will just be as buggy as those of others, but for once I will have the option of fixing all my code. My GPS will not be as feature rich but they will be the features that I use and want. Plus I think Garmin's software developers have somehow stopped innovating. My Colorado has a better screen and a better chip than the old etrex, but that is hardware. The software is certainly no improvement.

How difficult will it be? That is the big question. A lot of stuff is already built into Android, but a lot of stuff is missing. This will be real-life experiment on how mature Android is, how difficult it is to use and how good it can become.

There are other platforms out there, winCE, openmoko, the iphone...so why Android? Well, Google has done pretty good software so far and this is something a lot of money rides on for them. So I hope they will put a lot of effort into it and they have made it open source. And it will be available (in due course) on many devices. So this is a good time to start. Let's get rolling.