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.