My Tracks Patched

The guys at My Tracks have integrated the changes I sent to enable more data to be passed with the GPX file. There are some slight changes needed to the currently released version of Tracks2Miles but I will push a new version in the next couple of days to be ready for the next My Tracks drop.

I don’t know when the next version of My Tracks will ship yet, but we will be ready,

In the meantime here are the details of the change so others can use it to build similar add-ons for any other site (why they would want to use anything but dailymile?).

As mentioned back in the first post on this subject your going to need an Intent filter that will trap when My Tracks is looking to export a GPX file (the changes to My Tracks apply to all file types, but I’m only going to talk about GPX here). The following is what I’ve been using for Tracks2Miles:

 <activity android:name=".Export"
      android:label="@string/app_name" android:exported="true">
   <intent-filter>
     <action android:name="android.intent.action.SEND" />
     <category android:name="android.intent.category.DEFAULT" />
     <data android:mimeType="application/gpx+xml" />
   </intent-filter>
 </activity>

This hooks the “android.intent.action.SEND” action and filters it for the mimeType “application/gpx+xml”. So in the Activity that gets called when an intent matching this is broadcast you will need a onCreate method that looks something like this.

public void onCreate(Bundle savedInstanceState) {
  Intent intent = getIntent();

  if (intent.getAction().equals(Intent.ACTION_SEND)) {
    if (intent.hasExtra("com.google.android.apps.mytracks.TRACK")) {
      Track track = (Track) intent.getParcelableExtra("com.google.android.apps.mytracks.TRACK");
      ...
    }

    Uri gpxURI = (Uri)intent.getExtras().get(Intent.EXTRA_STREAM);
    ...
  }
}

Where Track is an instance of com.google.android.apps.mytracks.content.Track. You will need to grab this and the 2 classes in the package com.google.android.apps.mytracks.stats so they can be on the classpath for getParcelableExtra to find them. From the Track object you can get hold of name, time and distance of the workout and also there is a Statistics field that has info on average/max speed, elevation information.

I’m planning on writing a My Tracks wiki page about this some time soon where I’ll go in to a little bit more detail but this should get most people started.

Tracks2Miles 1.0.4

A quick update about the 1.0.4 * update release of Tracks2Miles.

  • Added some better error path handling to make it a bit more graceful should it fall over.
  • Added a local DB to cache route information, this is for 2 reasons. Firstly to make it quicker to just pick a route without having to wait for the list to be downloaded and secondly as a work around for the missing distance info in routes uploaded via the API. One of the side effects of this is that previously uploaded routes with no distance will not be shown in the list. Look at the end of the post for a way to fix these routes.
  • Added a bit more validation before posting, this should make it harder to post routes without titles or time and distances. A dialogue will ask if you really want to do this before posting.

I’ve also found a way to fix the uploaded routes missing distances. If you view the routes on the dailymile website, then hit the edit link at the top it will open the route editor. At this point it will recalculate the distance and you can just hit the Update button in the bottom right hand corner. The route should now have a valid distance.

Based on Dan’s comments on the announcement entry, I’m planning on looking to see if I can identify an existing route before posting a new one from My Tracks, this will reduce upload time and allow for dailymile to track your best time. To keep things easy it will probably just be based on the name of the track and distance.

I have added the following short video to the Android Market entry.

* 1.0.2 & 1.0.3 wouldn’t work on 2.1 due to using a method in API v8, I’ll remember to check 2.1 in the emulator before uploading next time

Announcing Tracks2Miles

OK, so the last 3* posts have been about the development of this so I guess you all knew it was coming, but now after some kind folk (Thanks @davidgeorgeuk, @bodonovan, @TheMightyAl & @ragtag) beta tested it for me it’s time to release it to the world.

A Friday afternoon chat between myself, Karl Roche, Brian O’Donovan and Dale Lane about tracking exercise habits with the website dailymile led to wondering why there was no Android app for it yet. A quick look at the API suggested it shouldn’t be too hard run something up. A short weekend of hacking later the majority of Tracks2Miles was done.
Tracks2Miles Icon
Tracks2Miles is an Android application to take workouts recorded with My Tracks and uploads them dailymile.

Rather than reinvent the wheel and write a full GPS tracking app I decided to see how easy it would be to extend one of the existing apps. My Tracks was one of the first training apps released for Android so it seamed like a good place to start. My Tracks have released a library to allow embedding the GPS tracking into new apps, but even this looked like a lot of work, but My Tracks also has a feature to “Share with friends…” which uses the android.intent.action.SEND intent action to allow other apps to forward on the data in a number of formats, one of which is GPX. This made writing the Tracks2Miles very easy.

Current Features:

  • Export routes from My Tracks – use the “Share with friends…” menu option to share the current track
  • Post workouts without using My Tracks – Tracks2Miles can be launched directly to upload a workout without uploading a new track e.g. treadmill seasons
  • Existing route reuse – start Tracks2Miles from the launcher and hit the “Use existing route” button and you can use routes already uploaded to dailymile

Possible Upcoming Features:

  • Support other apps – add support for exporting from apps like Endomondo or Runkeeper if they can export GPX files
  • Export any GPX – search the file system and upload any GPX found
  • dailymile timeline – a way to see what everybody else has been up to and add comments and motivation
  • Offline caching – save on bandwidth and API calls
  • User preferences – store defaults for Rating and workout type type
  • Contact syncing – add your running buddies to your phones contact list
  • Retry on network loss – uploading long routes can take a while over slow networks so a bit more fault tolerance would be good
  • Widget – because every app needs a widget

Known issues:

  • Facebook login not working – When authorising Tracks2Miles using an account created using your Facebook account it says password is wrong. This looks like either a problem on the dailymile or Facebook side of things, I have reported it
  • Time and distance not filled in when exporting from My Tracks – a patch to make this happen have been submitted to My Tracks, hopefully it will make it into the next release.
  • Reused routes have zero length – Some routes returned by dailymile are shown to have zero length so the distance is not filled in when using them. I have reported this to dailymile.

I’ve tested this on a HTC Desire, a ZTE Blade (Orange San Francisco) (updated to 2.2), a Samsung Galaxy and a Motorola Defy devices along with in the emulator running 2.1, 2.2 and 2.3. It should run on Android 2.1 or newer.

The Android Market URL is https://market.android.com/details?id=uk.me.hardill.dailymile or us the following QR code:

*

  1. My Tracks to dailymile GPX route exporter – part 1
  2. My Tracks to dailymile GPX route exporter – part 2
  3. My Tracks to dailymile GPX route exporter – part 3

My Tracks to dailymile GPX route exporter – part 3

So I’ve got the oAuth 2.0 all sorted it’s time to write the code to do the actual publish to dailymile.

First up I need a UI to capture all the other info about the workout other than the route. Not all of the options on the website are available via the API, but there are enough make a useful post.

I wanted to fill in as many of the fields as possible, so the route name comes from the GPX file name. When My Tracks creates the intent to share the GPX file it adds 2 extras which map to the equivalent of an email subject and email some body text to go with the file, but none of this contains data like the distance covered or the time. This information isn’t explicitly in the default GPX files either.

I could parse the GPX file and generate this info, but it’s going to be a heavy weight process and the data is something My Tracks should already have. I had a quick look at some of the other formats that My Tracks can export and found a the track description field in the KML version which holds the info I’m looking for.


<desc><![CDATA[Created by <a href='http://mytracks.appspot.com'>My Tracks</a< on Android.<p>Total Distance: 223.32 km (138.8 mi)<br>Total Time: 1:57<br>Moving Time: 0:20<br>Average Speed: 6866.26 km/h (4266.5 mi/h)<br> Average Moving Speed: 40197.14 km/h (24977.3 mi/h)<br> Max Speed: 0.00 km/h (0.0 mi/h)<br>Min Elevation: 0 m (0 ft)<br>Max Elevation: 0 m (0 ft)<br>Elevation Gain: 0 m (0 ft)<br>Max Grade: 0 %<br>Min Grade: 0 %<br>Recorded: Tue Apr 12 18:53:08 GMT+00:00 2011<br>Activity type: -<br><img border="0" src="http://chart.apis.google.com/chart?&chs=600x350&cht=lxy&chtt=Elevation&chxt=x,y&chxr=0,0,223,37|1,0.0,100.0,25&chco=009A00&chm=B,00AA00,0,0,0&chg=100000,25.0,1,0&chd=e:,"/>]]></desc>

There is a similar field in the GPX file that is currently empty. A quick look at the code showed that a 5 line patch should populate that field with the same data. I’ve sent the patch off to the My Tracks guys, but in the mean time if the field is empty the user will have to fill in the time and distance by hand.

With the UI sorted, the publishing is done in a service as it’s a multi-stage process including uploading the GPX file which could take a while over mobile networks. It’s basically 3 dailymile API calls

  1. Create a new route – POST https://api.dailymile.com/routes.json
  2. Upload GPS data to form the route – PUT https://api.dailymile.com/entries/id/track.json
  3. Create new workout entry – POST https://api.dailymile.com/entries.json

Interspersing these with Notification updates so the user can see how far through the process it is.

 

private void upload() {
HttpResponse resp;
final HttpClient httpClient = new DefaultHttpClient();</code>

String ns = Context.NOTIFICATION_SERVICE;
NotificationManager notificationManager = (NotificationManager) getSystemService(ns);

Notification notification = new Notification(R.drawable.icon, "Uploading Workout", System.currentTimeMillis());
Context context = getApplicationContext();

Intent notificationIntent = new Intent();
PendingIntent contentIntent = PendingIntent.getActivity(this, 0, notificationIntent, 0);

notification.setLatestEventInfo(context, "Uploading Workout", "Creating new Route", contentIntent);
notification.flags |= Notification.FLAG_ONGOING_EVENT;
notification.flags |= Notification.FLAG_NO_CLEAR;
notificationManager.notify(1, notification);
Log.i("DailyMile",Constants.NEW_ROUTE_URL + authToken);
final HttpPost post = new HttpPost(Constants.NEW_ROUTE_URL + authToken);
List<NameValuePair> nameValuePairs = new ArrayList<NameValuePair>(2);
nameValuePairs.add(new BasicNameValuePair("name", routeName));
nameValuePairs.add(new BasicNameValuePair("activity_type", routeType));
try {
post.setEntity(new UrlEncodedFormEntity(nameValuePairs));
} catch (UnsupportedEncodingException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
String routeID = "";
try {
resp = httpClient.execute(post);
if (resp.getStatusLine().getStatusCode() == 201) {
InputStream in = resp.getEntity().getContent();
StringBuffer buffer = new StringBuffer();
for (int i = in.read(); i != -1; i = in.read()) {
buffer.append((char)i);
}
JSONObject jsObject = new JSONObject(new JSONTokener(buffer.toString()));
routeID = jsObject.getString("id");
Log.i("DailyMile", routeID);
} else {
Log.i("DailyMile","" + resp.getStatusLine().getStatusCode());
notification.setLatestEventInfo(context, "Uploading Workout", "Failed to create a new route", contentIntent);
notification.flags |= Notification.FLAG_AUTO_CANCEL;
notificationManager.notify(1, notification);
//barf
return;
}
} catch (ClientProtocolException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
catch (JSONException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
notification.setLatestEventInfo(context, "Uploading Workout", "Uploading Route", contentIntent);
notificationManager.notify(1, notification);
//{"id":622197,"activity_type":"Fitness","name":"test"}

String putURL = Constants.UPDATE_ROUTE_URL.replace("{id}", routeID) + authToken;
Log.i("DailyMile", putURL);
final HttpPut put = new HttpPut(putURL);
try {
ParcelFileDescriptor pfd = this.getContentResolver().openFileDescriptor(gpxURI, "r");
InputStream in = this.getContentResolver().openInputStream(gpxURI);
InputStreamEntity entity = new InputStreamEntity(in, pfd.getStatSize());
entity.setContentType("application/gpx+xml");
put.setEntity(entity);
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}

try {
resp = httpClient.execute(put);
if (resp.getStatusLine().getStatusCode() != 201) {
Log.i("DailyMile","" + resp.getStatusLine().getStatusCode());
notification.setLatestEventInfo(context, "Uploading Workout", "Failed to upload new route", contentIntent);
notification.flags |= Notification.FLAG_AUTO_CANCEL;
notificationManager.notify(1, notification);
//barf
return;
}
} catch (ClientProtocolException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}

notification.setLatestEventInfo(context, "Uploading Workout", "Adding new workout", contentIntent);
notificationManager.notify(1, notification);

final HttpPost postWorkOut = new HttpPost(Constants.NEW_ENTRY_URL+authToken);
StringEntity entity = null;
JSONObject json = new JSONObject();
JSONObject workout = new JSONObject();
JSONObject distance = new JSONObject();
try {
json.put("message",workoutMessage);
workout.put("duration",workoutDuration);
workout.put("activity_type", routeType);
workout.put("felt", workoutRating.toLowerCase());
workout.put("route_id",Integer.parseInt(routeID));
distance.put("units", workoutDistanceUnits);
distance.put("value",workoutDistance);
workout.put("distance", distance);
json.put("workout", workout);
Log.i("DailyMile",json.toString());
entity = new StringEntity(json.toString());
entity.setContentType("application/json");
} catch (JSONException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (UnsupportedEncodingException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
postWorkOut.setEntity(entity);

try {
resp = httpClient.execute(postWorkOut);
if (resp.getStatusLine().getStatusCode() != 201) {
Log.i("DailyMile","" + resp.getStatusLine().getStatusCode());
notification.flags |= Notification.FLAG_AUTO_CANCEL;
notification.setLatestEventInfo(context, "Uploading Workout", "Failed to upload new workout", contentIntent);
notificationManager.notify(1, notification);
//barf
return;
}
} catch (ClientProtocolException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}

notification.setLatestEventInfo(context, "Uploading Workout", "All Done", contentIntent);
notification.flags |= Notification.FLAG_AUTO_CANCEL;
notificationManager.notify(1, notification);

}

 

So it’s all done now, just got to jump trough a few hopes to get it sign off to publish it in the Android Market and hopefully the My Tracks guys will include my patch in the next drop.

My Tracks to dailymile GPX route exporter – part 2

In my last post I talked about a project I’m working on to export GPS tracks from My Tracks directly to dailymile.

I was having problems getting the mobile client flow for oAuth 2.0 to work with dailymile. As a work around I decided to see if I could get the User-Agent flow to work for this application.

At the end of the previous post I suggested using an Intent filter to catch a redirect to a custom URI schema, while I got this to work it was messy as it left the browser app on the activity stack during the authentication. While looking for a way round this I found the documentation for the WebView widget and came up a much better solution. The WebView widget allows you to embed HTML content in your application so it should be possible to embed the web based authorisation set into a Activity.


<pre>
getWindow().requestFeature(Window.FEATURE_PROGRESS);
WebView webView = new WebView(this);
setContentView(webView);
final Activity activity = this;
webView.loadUrl(Constants.AUTH_URL);
</pre>

In the onCreate method of the Activity I have done away with a normal layout and just used a full screen WebView and asked it to load the authentication URL from the User-Agent Flow

https://api.dailymile.com/oauth/authorize?response_type=token&client_id=<CLIENT_ID>&redirect_uri=dm://locahost

Login Authorise

The callback redirect URI (dm://localhost) still has a custom schema similar to the one suggested in the previous post. Assuming the user allows access to their dailymile profile we can intercept the redirect by overriding the WebViewClient on the Webview which will look a bit like this:

dm://localhost#access_token=[ACCESS_TOKEN]

webView.setWebViewClient(new WebViewClient() {
  @Override
  public boolean shouldOverrideUrlLoading(WebView view, String url) {
    if (url.startsWith("dm://")) {
      String summary = "<html><body>Grabbing Account info</body></html>";
      view.loadData(summary, "text/html", "utf-8");
      String token = url.substring(url.lastIndexOf("=")+1);
      User user = Utilities.getAccountDetails(token, activity, handler);
      if (user != null) {
        user.setToken(token);
        finishLogin(user);
      }
      return false;
    }
    view.loadUrl(url);
    return true;
}});

Now I’ve got the AuthToken I can tuck it away in the Android AccountManager so I can grab it later when I want to upload a training session.

My Tracks to dailymile GPX route exporter – part 1

On Friday afternoon a few people I know were talking about a web site for people who like to record their running and cycling and share it with their friends, the site is called dailymile. Dailymile allows you to upload GPS recordings of your routes so you can work out how far you’ve gone and also share routes with others.

Those of us that have Android phones tend to use an app called My Tracks to do the GPS recording and time our excursions. At the moment you either have to email the GPX files to yourself or if you have the Dropbox application installed, save them to the cloud storage before you can use dailymile’s web interface to upload them to the site.

Dailymile have recently published a REST API which includes an end point to upload new routes to, so it looked like it shouldn’t be too hard to put something together allow direct exporting of training sessions.

Getting something into the My Track’s “Share with Friends…” list is just a case of setting the right Intent Filter

 <activity android:name=".Export"
      android:label="@string/app_name" android:exported="true">
   <intent-filter>
     <action android:name="android.intent.action.SEND" />
     <category android:name="android.intent.category.DEFAULT" />
     <data android:mimeType="application/gpx+xml" />
   </intent-filter>
 </activity>

Share with Friends...
Share with Friends...

I had already been playing with adding Account mangers to the Accounts & Sync section under the Android settings for a work project and this seamed like the best way to manage the OAuth requirements for the dailymile API. So I set about writing a SyncAdapter based on the previous code I already had. The problem is that dailymile have chosen to use OAuth 2.0 which is still a draft, so while there are a load of Java libraries out there that have support for 1.0 there are only a couple that have varying levels of support for different levels of draft for 2.0. The other problem is that there is no example on the dailymile api page of how to do OAuth authentication from a mobile app, which I’m starting to think means they’ve not implemented it yet.

A site with a very similar name (DailyMotion) also uses OAuth 2.0 and has an example of how to do mobile app authentication, following this would lead to the following curl test.


curl -F grant_type=password -F client_id=[client_id] -F client_secret=[client_secret] -F username=[username] -F password=[password] https://api.dailymile.com/oauth/token

This gives the following output


{"error":"unauthorized_client"}

So I’m wondering if this means that dailymile have to enable specific clients for this direct access to a token.

I’ve send a mail to the dailymile guys asking about this and if they can update their examples/docs for this case. In the mean time I’m going to have a look at using the web based authorization approach with a custom schema for the callback URL which I should be able to register with a suitable Intent filter.

 <activity android:name=".AuthCallBackAct" android:label="@string/app_name"
      android:exported="true">
 <intent-filter>
  <action android:name="android.intent.action.VIEW"/>
  <category android:name="android.intent.category.BROWSABLE"/>
  <category android:name="android.intent.category.DEFAULT"/>
  <data android:scheme="dm" android:host="localhost"/>
 </intent-filter>
</activity>

First Android App

After mentioning my new toy a couple of posts ago I mentioned I was hoping to do a bit of Android development.

I’ve been doing a bit of Android work as part of my new job (I recently joined a very cool team at work) making some existing Java code run, but this is my first real application.

I had left the TV on and gone to the office one day last week and while it did not permanent damage, there was a slight bit of ghosting from the MythTV menu for a day or 2 afterwards. I already have my TV hooked up a couple of topic on my home MQTT broker, so I though that running up a simple Android Widget to show the state would be a good starting point.

MQTT Topic Tree
MQTT Topic Tree

The TV/Status topic updates every 30 seconds and currently shows either off or a string like on:RGB:6 which is made up of state, input and volume.

The first task is to create a Android Service that will subscribe to this topic and watch for changes. The standard IA92 MQTT drivers can be used with the Android SDK to get access to the broker, so the connect code is very similar to the code used in my Twitter2MQTT bridge except the id for the client is generated from the phones unique id.

private String android_id =Secure.getString(this.getContentResolver(),Secure.ANDROID_ID);

private boolean connect() {
  try {
    client = (MqttClient) MqttClient.createMqttClient("tcp://tiefighter.loc:1883", null);
    client.registerSimpleHandler(new MessageHandler());
    client.connect("droid-"+android_id, true, (short) 240);
    String topics[] = {"TV/Status"};
    int qos[] = {1};
    client.subscribe(topics, qos);
    return true;
  } catch (MqttException e) {
    e.printStackTrace();
    return false;
  }
}

The MessageHandler is an inner class and handles both incoming messages and reconnection using the following 2 methods.

public void publishArrived(String topic, byte[] payload, int qos, boolean retained) throws Exception {
	String message = new String(payload);
	// This is to allow access to the notification service from a none 	
	if (Looper.myLooper() == null) {
		Looper.prepare();
	}
			
	RemoteViews updateViews = new RemoteViews(TVLongService.this.getPackageName(), R.layout.main);
			
	String toastMsg = "";
	String toastHead = "";
	int icon = 0;
	boolean alert = false;

	if (message.equals("off")) {
		if (TVWidget.on) {
			toastMsg = "TV now OFF";
			toastHead = "TV OFF";
			icon = R.drawable.tv_off;
			alert = true;
			TVWidget.on = false;
		}
	} else {
		if (!TVWidget.on) {
			toastMsg = "TV now ON";
			toastHead = "TV ON";
			icon = R.drawable.tv_on;
			alert = true;
			TVWidget.on = true;
		}
	}

	if (alert) {
		Intent intent = new Intent();
		PendingIntent contentIntent = PendingIntent.getActivity(TVLongService.this, 0, intent, 0);
		NotificationManager notificationManager = (NotificationManager) TVLongService.this.getSystemService(Context.NOTIFICATION_SERVICE);
		Notification notification = new Notification(icon, toastHead, System.currentTimeMillis());
		notification.setLatestEventInfo(TVLongService.this, "TV Monitor", toastMsg, contentIntent);
		notificationManager.notify(1, notification);
		updateViews.setImageViewResource(R.id.img, icon);
	}

	ComponentName componentName = new ComponentName(TVLongService.this, TVWidget.class);
	AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(TVLongService.this);
	appWidgetManager.updateAppWidget(componentName, updateViews);
}

TVWidget
The MessageHandler onMessage method fires when a message is posted to the TV/Status topic and then updates the widget Icon from tv_on to tv_off and then triggers a notification pop-up if the state has changed since the previous state.

The connectionLost method fires up a background thread to try and reconnect to the broker every 10 seconds.
TVWidget_on

TVWidget_toast

public void connectionLost() throws Exception {
	client = null;
	Log.v("TVMonitor","connection dropped");
	Thread t = new Thread(new Runnable() {
		
		@Override
		public void run() {
			do {//pause for 5 seconds and try again;
				Log.v("TVMonitor","sleeping for 10 seconds before trying to reconnect");
				try {
					Thread.sleep(10 * 1000);
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			} while (!connect());
			System.err.println("reconnected");
		}
	});			
}

Next Steps

  • Survive network changes

    At the moment the code does not handle moving from WIFI to 3G networking all that well, there are collection of BroadCast future PendingIntents that should signal changes in network state.

  • Geo-Fencing

    I don’t really need to know that the TV is on while I’m at home, so I’m planning on adding a filter so notifications are only fired when I’m outside say 200m of home

  • MythTV

    May be add some indication as to what channel is currently being watched and what the upcoming recordings may be

New Toy

I’ve finally got round to getting a smart phone. I have never been sold on the whole iPhone clut craze and I’ve already got a work blackberry that I despise so the choices where really between an Android or Windows Mobile device.

Given the hackability of Android pretty much sealed the deal, but I was torn between a Sony Ericsson Xperia X10 and a HTC Desire.

Sony Erricson – Xperia X10

Sony Xperia X10 front
Image CC licensed by louisvolant on Flickr

My last few phones have all been from Sony Ericsson, I’ve had the following:

  • T68i
  • K700
  • K750
  • w850
  • W880i
  • C905

I’ve been very happy with these phones over the years (with the exception of the C905’s lack of functional Google Maps, but that was down to O2’s “custom” firmware) so getting another SE was definitely a possibility but after getting hold one and having a bit of a play with the Timescape layer that SE have added on top of Android I decided it wasn’t for me.

HTC – Desire

HTC Desire 4
Image CC licensed by louisvolant on Flickr

HTC built the first Android phone the G1 and also the recent Nexus One for Google so they have a decent amount of experience with Android. The Desire shares most of the components of the Nexus One with major differences being cosmetic, the 4 buttons on the front are physical rather than an extension of the touch screen and the track ball in an optical sensor on the Desire.

I am quite taken with the Sense interface that HTC have layered on top of 2.1 Android “Eclaire”, but I’m hoping to have a play with a colleges Desire which is running a plain build of Android 2.2 “Froyo” shortly to see if it’s worth waiting for the Sense skinned version of Android 2.2 which is rumoured to arriving in 3Q, or to root the phone and install a custom image.

Apps

I’ve been taking it easy with apps so far and I’ve not found one I want to pay for just yet but the following free ones have been getting a reasonable amount of use.

  • Simple Last.fm Scrobbler – A scrobbler for the Android built in music player
  • UK Traffic Checker – A very cool app written by Dale Lane that check the route between 2 places for traffic problems
  • Bump – A neat way to send photos and vcards between 2 phones
  • Layar Reality Browser- A AR viewer

Development

So now I have my cool new phone I suppose it would be rude to not make use of it’s open development platform. I’ve downloaded the latest version of the SDK and the Eclipse plugin. I’m working my way through a couple of the tutorials before coming up with a project.