2Lines Software
21Jan/0912

Programming with Android Part 4 – Finishing WikiWhere

Continuing from Part 3 we are going to spend the 30 minutes or so to finish the WikiWhere application. So far we have a mapping and location application on the device. The final step is integrating the Wiki sites and displaying them on the map.

1:45 Foosball, Reddit, Digg and Google Reader Break

1:55  Getting the XML feed for the Wiki sites

Now that you've had a long break we can get to the more complicated sections of the code. The Wiki information is collected and provided as a web service by Geonames.org. We will be using the wikipediaBoundingBox XML API to get the information from the service. By making a call to http://ws.geonames.org/wikipediaBoundingBox?north=51.1&south=50.1&east=-113&west=-115 you can get the XML feed of the Wiki Entries in the bounding box in the following format:

  1. <geonames>
  2.   <entry>
  3.   <lang>en</lang>
  4.   <title>Max Bell Centre</title>
  5.   <summary>The Max Bell Centre (often referred to as the Max Bell
  6.   Arena) is an ice hockey arena in Calgary, Alberta, Canada in the
  7.   community of Radisson Heights. It seats 2,121 for hockey with a
  8.   standing room capacity of over 3,000. It is named after George
  9.   Maxwell Bell, a philanthropist who helped fund the arena's
  10.   construction (...)</summary>
  11.   <feature>landmark</feature>
  12.   <countryCode>CA</countryCode>
  13.   <population>0</population>
  14.   <elevation>0</elevation>
  15.   <lat>51.0422</lat>
  16.   <lng>-114.0036</lng>
  17.   <wikipediaUrl>http://en.wikipedia.org/wiki/Max_Bell_Centre
  18.   </wikipediaUrl>
  19.   <thumbnailImg />
  20.  </entry>
  21. ...
  22. </geonames>

So to get our wiki items displayed on the map we need to:

1. Call the Geonames XML API

2. Parse the XML

3. Display the icons on a MapOverlay

4. Implement the "Search For Wiki Entries" Button

Step 1. Call the Geonames API

Android comes built in with the HttpClient code from Apache Foundation, which gives it a very flexible tool in accessing web services. For WikiWhere we will create two classes to handle the API calls. GeonameManager will be responsible for making the HTTP request and parsing the XML, and much like the LocationListener, GeonameListener will push the messages back to the WikiWhere Class. GeonameManager:

public class GeonameManager {
  1.         private GeonameListener listener;
  2.  private static final String WS_URL = "http://ws5.geonames.org/wikipediaBoundingBox?";
  3.  
  4.  public GeonameManager(GeonameListener _listener){
  5.   listener = _listener;
  6.  }
  7.  
  8.  public void readFromUrl(double minLat, double maxLat, double minLon, double maxLon) {
  9.   StringBuffer sb = new StringBuffer();
  10.   sb.append(WS_URL);
  11.   sb.append("north=");
  12.   sb.append(maxLat);
  13.   sb.append("&amp;south=");
  14.   sb.append(minLat);
  15.   sb.append("&amp;west=");
  16.   sb.append(minLon);
  17.   sb.append("&amp;east=");
  18.   sb.append(maxLon);
  19.   String httpUrl = sb.toString();
  20.  
  21.   URL url;
  22.   URLConnection connection = null;
  23.   try{
  24.  
  25.    HttpParams params = new BasicHttpParams();
  26.    HttpConnectionParams.setConnectionTimeout(params, 5000);
  27.    HttpClient httpclient = new DefaultHttpClient(params);
  28.  
  29.          HttpGet httpGet = new HttpGet(httpUrl);
  30.          HttpResponse response = httpclient.execute(httpGet);
  31.    WikiItems wi = parseXML(response.getEntity().getContent());
  32.    listener.newWikiItems(wi);
  33.   }catch(Exception e){
  34.    listener.newError("Network error:"+e.getMessage()+". Please try again later");
  35.   }
  36.  
  37.  }
  38.     private WikiItems parseXML(InputStream in){
  39.   XStream xstream = new XStream(new DomDriver());
  40.   xstream.alias("geonames", WikiItems.class);
  41.   xstream.alias("entry",WikiEntry.class);
  42.   xstream.addImplicitCollection(WikiItems.class, "entries");
  43.  
  44.   WikiItems wi = (WikiItems) xstream.fromXML(in);
  45.   return wi;
  46.  }
  47.  
  48. }

and GeonameListener..

public interface GeonameListener {
  1.  public void newWikiItems(WikiItems wi);
  2.  public void newError(String message);
  3. }

The method to call the API (readFromUrl) makes a simple HTTP connection in the running thread. Once the connection is made, the InputStream is pushed to the parseXml() method.

Step 2. Parse the XML

This is where I cheat to save time. I have imported the open source XStream library to parse the XML. XStream is a very powerful tool used to serialize XML to Java Objects. By taking the source code and importing it into our project we can leverage the XStream tools in the Android environment. I have modified the code to work with the Android parsers and you can download it from here:

XStream for Android Source

To use the XStream parser we first have to create two classes, WikiItems and  WikiEntry, to hold the XML information as a Java Object.

public class WikiEntry {
  1.         private String lang;
  2.  private String title;
  3.  private String summary;
  4.  private String feature;
  5.  private String countryCode;
  6.  private int population;
  7.  private double elevation;
  8.  private double lat;
  9.  private double lng;
  10.  private String wikipediaUrl;
  11.  private String thumbnailImg;
  12.  public String getLang() {
  13.   return lang;
  14.  }
  15.  public void setLang(String lang) {
  16.   this.lang = lang;
  17.  }
  18.  public String getTitle() {
  19.   return title;
  20.  }
  21.  public void setTitle(String title) {
  22.   this.title = title;
  23.  }
  24.  public String getSummary() {
  25.   return summary;
  26.  }
  27.  public void setSummary(String summary) {
  28.   this.summary = summary;
  29.  }
  30.  public String getFeature() {
  31.   return feature;
  32.  }
  33.  public void setFeature(String feature) {
  34.   this.feature = feature;
  35.  }
  36.  public String getCountryCode() {
  37.   return countryCode;
  38.  }
  39.  public void setCountryCode(String countryCode) {
  40.   this.countryCode = countryCode;
  41.  }
  42.  public int getPopulation() {
  43.   return population;
  44.  }
  45.  public void setPopulation(int population) {
  46.   this.population = population;
  47.  }
  48.  public double getElevation() {
  49.   return elevation;
  50.  }
  51.  public void setElevation(double elevation) {
  52.   this.elevation = elevation;
  53.  }
  54.  public double getLat() {
  55.   return lat;
  56.  }
  57.  public void setLat(double lat) {
  58.   this.lat = lat;
  59.  }
  60.  public double getLng() {
  61.   return lng;
  62.  }
  63.  public void setLng(double lng) {
  64.   this.lng = lng;
  65.  }
  66.  public String getWikipediaUrl() {
  67.   return wikipediaUrl;
  68.  }
  69.  public void setWikipediaUrl(String wikipediaUrl) {
  70.   this.wikipediaUrl = wikipediaUrl;
  71.  }
  72.  public String getThumbnailImg() {
  73.   return thumbnailImg;
  74.  }
  75.  public void setThumbnailImg(String thumbnailImg) {
  76.   this.thumbnailImg = thumbnailImg;
  77.  }
  78.  
  79. }

..and,

public class WikiItems {
  1.  private ArrayList entries;
  2.  public ArrayList getContent() {
  3.   return entries;
  4.  }
  5.  
  6.  public void add(WikiEntry item){
  7.   entries.add(item);
  8.  }
  9. }

You can see they are both fairly simple java classes using the tag names from the XML as field names.

Next you tell the XStream parser which classes map to which XML tags.

XStream xstream = new XStream(new DomDriver());
  1. xstream.alias("geonames", WikiItems.class);
  2. xstream.alias("entry",WikiEntry.class);
  3. xstream.addImplicitCollection(WikiItems.class, "entries");

and call the fromXML method.
WikiItems wi = (WikiItems) xstream.fromXML(in);

That's it. If you've ever had the hairpulling experience of parsing XML files on a mobile device you will be pleasantly surprised at how easy XStream is to use.

Step 3. Display the Icons on the MapOverlay

Going back to our WikiWhere activity we want to add the GeonameListener to receive the updates from the HTTP web service of new WikiItems loaded.

  1.     public class WikiWhere extends MapActivity implements LocationListener, GeonameListener {

and implement the onNewWikiItems method;

  1. public void newWikiItems(WikiItems wi) {
  2.  List overlays = mv.getOverlays();
  3.  
  4.  Drawable marker = getResources().getDrawable(R.drawable.mapicon);
  5.      marker.setBounds(0, 0, marker.getIntrinsicWidth(), marker.getIntrinsicHeight());
  6.      wikiOverlay = new WikiOverlay(marker,wi,this);
  7.      overlays.add(wikiOverlay);
  8.  
  9.      handler.sendEmptyMessage(0);
  10. }

Don't worry about the handler.sendEmptyMessage(0) call just yet. We'll get to that one soon.

The next part we will create a custom map overlay to handle placing the icons on the map when new wiki's are sent from the GeonameListener. Androids ItemizedOverlay provides a convenient way to handle multiple icons on a map. We implement this by extending it in a creating a class call WikiOverlay.

  1. public class WikiOverlay extends ItemizedOverlay {
  2.        @Override
  3.  protected boolean onTap(int index) {
  4.   listener.onTap(wi.getContent().get(index));
  5.   return super.onTap(index);
  6.  }
  7. private WikiItems wi;
  8. private ArrayList items;
  9. private ActionListener listener;
  10.  
  11. public WikiOverlay(Drawable defaultMarker, WikiItems  _wi, ActionListener _listener) {
  12. super(defaultMarker);
  13. items = new ArrayList();
  14. listener = _listener;
  15. wi = _wi;
  16. for(WikiEntry we:wi.getContent()){
  17.    GeoPoint g = new GeoPoint((int)(we.getLat()*1E6),(int)(we.getLng()*1E6));
  18.    OverlayItem oi = new OverlayItem(g,we.getTitle(),we.getSummary());
  19.    items.add(oi);
  20. }
  21. populate();
  22. }
  23. @Override
  24. protected OverlayItem createItem(int i) {
  25. return items.get(i);
  26. }
  27. @Override
  28. public int size() {
  29. return items.size();
  30. }
  31. }

The WikiOverlay converts the WikiEntry items into the standard OverlayItem that MapView is expecting. Once the populate() method is called the MapView will render the map icons onto the map. Pay attention to the onTap method and ActionListener we will come back to them shortly.

Special update ! Resources !

Android has a unique way of managing the resources in an application by creating the R.java file. This file provides a link between the physical resources on the disk and a way to access them from inside the code. This means no more complicated pulling from resource files to access images or properties files. Localization is handled automatically by populating different directory names(i.e. res/values-fr/string.xml for French).

Try copying this png Wiki Map Icon(Wiki Map Icon) into the directory res/drawable. Now refresh/open the R.java file. You should see a line that looks something like this:

  1. public static final int mapicon=0x7f020001;

That link is automatically generated and will give you access to the resource from any class and you can change it at anytime. Here is the relevant code snippet that access the mapicon png image and loads it into a Marker classfor display on the map.

  1. Drawable marker = getResources().getDrawable(R.drawable.mapicon);

This method of handling resources is used for strings, raw binary files (mp3s or mp4s), images and any other static file that is referenced by the code. It is a great way to handle configuration changes and localization in different devices. Again it shows a solid separation of graphic design from the programmers!.

Step 4. Implement the "Search For Wiki Entries" Button

The last step is to link the users request with the HTTP API call from the GeonameManager. We've implemented a button at the very beginning called "Search For Wiki Entries" that can be seen on the main page. We will use this button to send the HTTP request out.

In order to prevent the blocking operation on the main activity and effectively freeze the UI (and draw your users into a mad frenzy button mashing panic) we will create a worker thread running in the background to handle this. The worker thread will also turn on and off the waiting animation for the user.

Here is an important note: you cannot update a UI from any thread outside the main class where it was instantiated. That is; our background communications thread cannot make changes to the UI. In order to get around this Android includes a Handler class that queues requests in the main thread. Here is the code implemented at the start of WikiWhere class.

private Handler handler = new Handler(){
  1.   public void handleMessage(Message msg){
  2.    dialog.dismiss();
  3.    if(msg.getData().containsKey("error")){
  4.     showError(msg.getData().getString("error"));
  5.    }
  6.    mv.invalidate();
  7.   }
  8.  };

To update our UI from the communications worker thread we post a Message to the handler. Since the handler runs in the main thread it can update the UI

The worker thread is implemented as an inner class in the WikiWhere main class. Here is the code for the worker thread:

private class LoadingThread extends Thread{
  1.   private GeonameManager gm;
  2.   private double minLat;
  3.   private double maxLat;
  4.   private double minLng;
  5.   private double maxLng;
  6.  
  7.   public LoadingThread(double _minLat,double _maxLat,double _minLng,double _maxLng,GeonameManager _gm){
  8.    gm = _gm;
  9.    minLat = _minLat;
  10.    maxLat = _maxLat;
  11.    minLng = _minLng;
  12.    maxLng = _maxLng;
  13.   }
  14.  
  15.   public void run() {
  16.    gm.readFromUrl(minLat, maxLat, minLng, maxLng);
  17.      }
  18.  }

Finally we can call the thread by implenting the onClickListener for the button. So in the initMap() method we added the following code

button = (Button) findViewById(R.id.search);
  1.         button.setOnClickListener(new OnClickListener() {
  2.             public void onClick(View v) {
  3.              loadWiki();
  4.             }
  5.         });

and also added a new method to instantiate the loading;

private void loadWiki(){
  1.      dialog = new ProgressDialog(this);
  2.         dialog.setTitle("Wiki, Where");
  3.         dialog.setMessage("Getting Wiki Data...");
  4.         dialog.setIndeterminate(true);
  5.         dialog.setCancelable(true);
  6.         dialog.show();
  7.   GeoPoint center = mv.getMapCenter();
  8.      double minLat = (double)(center.getLatitudeE6()-(mv.getLatitudeSpan()/2)) / 1E6;
  9.   double maxLat = (double)(center.getLatitudeE6()+(mv.getLatitudeSpan()/2)) / 1E6;
  10.   double minLng = (double)(center.getLongitudeE6()- (mv.getLongitudeSpan()/2) ) / 1E6;
  11.   double maxLng = (double)(center.getLongitudeE6()+ (mv.getLongitudeSpan()/2)) / 1E6;
  12.  
  13.      LoadingThread process = new LoadingThread(minLat,maxLat,minLng,maxLng,gm);
  14.   process.start();
  15.     }

Run the app again and click on the button. It should launch a dialog and load the wiki items near that map location.If everything goes right you should see the blue wiki icon on the map like this;

Wiki Map View

Good app but let's add the last part to pull up information on that wiki locationin a separate web page.

2:15 Final Steps

The last step will involve listening for the user to click on a wiki item and display that wiki page to the user.

Remember back when we skipped over the ActionListener? Well, let's go back and add that interface into the WikiWhere class and extend the onTap() function;

public class WikiWhere extends MapActivity implements LocationListener, GeonameListener, ActionListener {
  1. Plus,
  2. <pre lang="java5">        public void onTap(WikiEntry we) {
  3.   mc.animateTo(new GeoPoint((int) (we.getLat() * 1E6),
  4.     (int) (we.getLng() * 1E6)));
  5.   final Dialog dialog = new Dialog(this);
  6.   dialog.setContentView(R.layout.web);
  7.   dialog.setTitle(we.getTitle());
  8.   dialog.setCancelable(true);
  9.  
  10.                WebView web = (WebView) dialog.findViewById(R.id.web);
  11.   web.loadUrl(we.getWikipediaUrl());
  12.   web.setInitialScale(50);
  13.   dialog.show();
  14.  }

The onTap() method listens from the WikiOverlay and waits for a user to tap one of the OverlayItems. We overload this method with ActionListener and pass back the WikiEntry reference that was selected.
This new method does a couple of things. First, it pans the map to center on the wiki location. Second, it launches a new WebView within a dialog. WebViews are fully functioning WebKit browser pages and essentially the same browser that is on the device. Simply by calling web.loadUrl() you can call up any web page and display it to the user.  This by itself is a fantastic tool for development.
However, we will have to first create the layout resource XML file. Create a file called web.xml in the res/layout directory and add the following to it;

  1. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  2.   android:orientation="vertical" android:layout_width="fill_parent"
  3.   android:layout_height="fill_parent">
  4.  <WebView android:id="@+id/web" android:layout_width="wrap_content"
  5.   android:layout_height="fill_parent" android:padding="10px" />
  6. </LinearLayout>

2:30pm We're Finished!
Congratulations you've just created your first app from scratch in less time than it takes to install a windows update.
Download the full source code here:

WikiWhere Source Code

Bonus Points!
To truly see how powerful the application environment and tools are you will need a device.  Sign up to http://market.android.com to become a registered developer. This gives you access to publish your applications and buy a development phone directly from Google. The phone is only $399USD but that doesn't include shipping and duty, which in some cases (Canada) can be over $250! Wait, what happend to NAFTA?
Once you get your device simply install the USB drivers and plug in your phone. The drivers will immediately recognize it. Now when you click Run in Eclipse you can execute the program on the phone including all the debugging and monitoring tools. Completely seemless emulator and device testing environment. It's like having your cake (not a lie) and eating it too!

[Slashdot] [Digg] [Reddit] [del.icio.us] [Facebook] [Technorati] [Google] [StumbleUpon]
Comments (12) Trackbacks (1)
  1. Thanks for a great series delivered in perfect bite-sized chunks. I am getting very interested in mobile device development and this complete lifecycle series was exactly what I was looking for.

    Notwithstanding the high device price, Android development certainly looks less intimidating than the iPhone process, especially in light of Apple’s vetting process. Wouldn’t it be a cheaper route to just buy a phone from T-Mobile instead?

    Anyway, thanks for a great read!

    -bill

  2. Thanks Bill,

    I’d love to get a phone from T-Mobile but we can’t get one in Canada just yet. The lack of 3G coverage for the G1 mobile band probably means we won’t be getting it anytime soon.

    The Android process is much easier to develop for, but Apple has created a more enticing model to developers (i.e. getting paid). Hopefully Android can find a way to monetize the applications as well as iTunes/Apple has done.

  3. Great article.
    I downloaded the source and applied to it the adroid 1.1 r2 jar.
    The webservice works as expected, but not the map and not a location change.
    I use the DDMS perspective under eclipse to submit a new GPS location, and th onLocationChange() gets fired , but the map never gets displayed.

    One contributing factor may be the @Override annotated to that method. When compiling against the latest android jar, the @Override is an error.
    I guess that the android.location.LocationListener is different in the new jar but I can’t figure out why the @Override was needed in the first place because that is an abstract method on the interface.

    The map never displays in the emulator (I’m not passing any special parameters to the debug session for the emulator – just the defaults).

    Any ideas?
    Thanks,
    Frank

  4. I followed the source code as given in – WikiWhere Source Code.

    Getting error in my emulator as

    Network error: Socket is not connected. Please try again later.

    Could any one help to resovle the issues.

  5. Hi Frank,

    Have you added your Map API Key into the code? You will have to edit the res/layout/main.xml file and add your key to android:apiKey=” “ . Keys are free but are tied to your developer certificate. You can get more details here/ http://code.google.com/android/maps-api-signup.html

    The @Overrides are added into the code automatically by Eclipse under default. Mostly lazy on my part. I was under the impression they are benign but I guess 1.1 doesn’t like them. You can remove them without any problems.

    Although you bring up a good point. I’ve never tested this on 1.1. I’ll run through that today to see if I run into any problems.

    Thanks,
    John

  6. Does that error show up in the Log tab of the DDMS perspective?

    You don’t run through a proxy out to the internet do you? You will have to set up the emulator to follow that proxy if that is the case.

  7. The location change using the DDMS doesn’t work for me either.
    Did anybody find out why? Or maybe somebody knows a workaround?

  8. Hi all,

    I have some troubles regarding android emulator. I tried to access the Internet, every thing works fine when I use a direct connection (without proxy). However, when I am behind a proxy I didn’t manage to access the Internet. And the weird thing is that I can not access even internal web pages (which are accessible without a proxy). Does anyone have an idea on how to access these internal pages???

    Best regards,
    Nassim Laga

  9. The location change using the DDMS doesn’t work for me either.

  10. Great articles bro —

    So are you straight importing the XStream source code into your application source tree or adding the jar? If importing, what package(s) were required? Also, is there not native support for XML parsing in Android?

    Peace,
    Scott

  11. Thanks for the feedback,

    The XStream is being brought in as source code. I found it was much more compatible with the Dalvik JVM and also it allowed me to control what modules to bring in. Android does have a native XML parser but what the XStream adds is a serialization back and forth from Java Objects to XML. I find I spend way too much of my life in airports and writing java serializers.

  12. Hi, I am not able to get the blue dot, or stated differently, not able to send location details through DDMS emulator.. I have tried googleAPI(google inc) platform 1.5, 1.6 and 2.0.1. but nothing seems to work. Please suggest a way to go around this issue. its really very urgent.


Leave a comment