Android: How to test Local Web Apps on an Emulated Device (AVD)

Android AVD running local Web App

Editing your /etc/hosts file is great for running and testing web apps as they are under development. For a project I’m working on, I needed to be able to access one of my local Rails apps through an Android device, which meant editing the device’s /etc/hosts file. As I haven’t rooted my Nexus-S, so opted to use an emulated Android Virtual Device (AVD), as they are configured by default to allow root access.

I started by creating a simple hosts file on my desktop. It’s this file that I would push to the AVD

cd Desktop
vim hosts # or your favourite text editor....

Use your text editor to create a simple hosts file pointing to your local web app. Remember to also include a localhost entry:

  # hosts
  127.0.0.1       localhost 
  192.168.1.10    rails-app

Next, copy your new hosts file to the AVD:

adb push hosts /etc/hosts

Access Denied

Despite having root access, though, adb wouldn’t let me push the new hosts file because the system partition is mounted as read-only. You can remount the system partition in read-write mode using adb remount, and then try pushing the hosts file up to the device:

adb remount
abd push hosts /etc/hosts

I kept getting an Out of Memory error, which seems to be a common problem. The solution (gleaned from scattered forum posts) is to use the emulator’s -partition-size option.

Using this option means you won’t be able to launch the AVD directly from Eclipse, but instead need to use the command line. Close down any running AVDs, and then run, and then re-launch the AVD passing in the -partition-size option with a reasonable value:

emulator -avd Samsung_Galaxy-Tab -partition-size 128
 
# ...wait the avd to boot up...
 
adb remount
abd push hosts /etc/hosts

The hosts file should now successfully be copied to your AVD. If you launch the AVD’s browser and enter the local URL from your hosts file (e.g. http://rails-app:3000/), the AVD will connect to the local IP address you specified.

Note: You’ll need to keep hold of your hosts file as your settings will be wiped when you shut down the AVD. You can reinstall the file next time by performing the adb remount; adb push hosts /etc/hosts script each time you boot the AVD.

References

  1. Google Groups Thread
  2. Cute Android Tips: Failed to Copy File to System

Android: Spinner-Like UI for Selecting Multiple Options

Download the source code files AndroidMultipleChoice.tar.gz (tar.gz, eclipse project)

Android includes a Spinner control that works very much like a desktop drop-down (or combo-box) control. Tapping a Spinner presents a list of options, and allows the user to select one from the list.

However, in building a small research app, I needed to allow users to select multiple items from the Spinner’s list. From what I could see, though, the Spinner class is not capable of doing this, so I built a small throwaway app that implements this oft-requested functionality.

Getting Started

Spin up a new Android application in Eclipse, creating an activity named MultipleChoiceActivity. For reference, I’m targeting the Google Android 2.1 APIs (Level 7).

Once the project is created, open up and edit the main.xml file to display a simple label and button. I’ve used a Button instead of a Spinner as we can change the button’s label to indicate which options have been selected:

<!-- res/main.xml -->
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="fill_parent"
  android:layout_height="fill_parent"
  android:orientation="vertical"
  android:padding="5dip">
 
  <TextView 
    android:text="Select Colours"
    android:id="@+id/textView1"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:padding="5dip" />
 
  <Button 
    android:layout_height="wrap_content"
    android:layout_width="fill_parent"
    android:text="- None Selected -"
    android:id="@+id/select_colours"
    android:padding="5dip" />
</LinearLayout>

Building the Activity

For this example, I’m using a hard-coded list of colours here in this example (rather than, for example, hooking up to a database). Start by hooking up the interface to properties in MultipleChoiceActivity.java:

// src/com/example/multiple_choice/MultipleChoiceActivity.java
 
// ... package, imports ...
 
public class MultipleChoiceActivity extends Activity implements OnClickListener {
  protected Button selectColoursButton;
 
  protected CharSequence[] colours = { "Red", "Green", "Blue", "Yellow", "Orange", "Purple" };
  protected ArrayList<CharSequence> selectedColours = new ArrayList<CharSequence>();
 
  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);
 
    selectColoursButton = (Button) findViewById(R.id.select_colours);
    selectColoursButton.setOnClickListener(this);
  }
 
  @Override
  public void onClick(View view) {
    switch(view.getId()) {
      case R.id.select_colours:
        // TODO: Show the colours dialog
        break;
 
      default:
        break;
    }
  }
}

With that code, your button is now hooked up to listen for clicks. The colours array is a simple list of colours that will be displayed when we click the button. The selectedColours ArrayList is a dynamic array of the colours that have been selected. Using ArrayList lets us easily add and remove colours when the user changes the selection.

Try running the app in the Android emulator (or on a device) to check that everything works so far.

Displaying the Options

Next, we’ll use Android’s AlertDialog to let us display the list of colours, and allow the user to make selections. AlertDialog comes with built-in functionality to allow multiple selections, and automatically adds checkboxes to our list interface.

Add the following method to MultipleChoiceActivity.java:

// src/com/example/multiple_choice/MultipleChoiceActivity.java
protected void showSelectColoursDialog() {
  boolean[] checkedColours = new boolean[colours.length];
  int count = colours.length;
 
  for(int i = 0; i < count; i++)
    checkedColours[i] = selectedColours.contains(colours[i]);
 
  DialogInterface.OnMultiChoiceClickListener coloursDialogListener = new DialogInterface.OnMultiChoiceClickListener() {
   @Override
    public void onClick(DialogInterface dialog, int which, boolean isChecked) {
      if(isChecked)
        selectedColours.add(colours[which]);
      else
        selectedColours.remove(colours[which]);
 
      onChangeSelectedColours();
    }
   };
 
  AlertDialog.Builder builder = new AlertDialog.Builder(this);
  builder.setTitle("Select Colours");
  builder.setMultiChoiceItems(colours, checkedColours, coloursDialogListener);
 
  AlertDialog dialog = builder.create();
  dialog.show();
}

showSelectColoursDialog is the method that will be called when tapping the button. It starts by walking through the colours array and checking which colours are already in the selectedColours ArrayList. The results get stored in an array of booleans (checkedColours) that will be used by the AlertDialog to determine which items in the list are to be checked.

Next, we build a DialogInterface.OnMultiChoiceClickListener that will be invoked when the user checks or unchecks a colour in the list. Depending on the state of the check box (supplied via the isChecked parameter), we add or remove the colour from the selectedColours ArrayList.

Notice that when the listener is finished, we call an as-yet undefined method, onChangeSelectedColours. This will be a callback method that we’ll use to update the button’s label:

// src/com/example/multiple_choice/MultipleChoiceActivity.java
protected void onChangeSelectedColours() {
  StringBuilder stringBuilder = new StringBuilder();
 
  for(CharSequence colour : selectedColours)
    stringBuilder.append(colour + ",");
 
  selectColoursButton.setText(stringBuilder.toString());
}

This simple callback method just walks through the ArrayList of selected colours, and builds a string containing the name of each colour.

Finally, all that’s left to do is hook up our button to call showSelectColoursDialog in the onClick method we overrode earlier:

// src/com/example/multiple_choice/MultipleChoiceActivity.java
@Override
public void onClick(View view) {
  switch(view.getId()) {
    case R.id.select_colours:
      showSelectColoursDialog();
      break;
 
    default:
      break;
  }
}

With everything hooked up, it’s time to test out the app. Run the emulator (or install to a device), tap the button and you’ll be able to select multiple colours from the list. If you tap the back button, the list will close, and your button’s label will have updated to show the colours you tapped. Try tapping the button again, and your previously selected colours will still be checked.

Summary

Hopefully this simple example will be useful to anyone looking to implement multiple-choice options. It would be nice to use the Spinner gadget rather than a Button, perhaps by subclassing Spinner and overriding it’s click handler and label drawing methods. I haven’t tried this – let me know in the comments if you decide to give it a go.

Introducing Outlime: Rapid Sketching for Android

Update 26 August 2010: The Outlime product page is now live at plymouthsoftware.com/outlime.

I’m pleased to announce the publication of my first app for Android: Outlime.

Outlime is a very minimalist sketching app for Android that lets you quickly commit ideas and concepts from your head to your phone. Outlime costs just £1.29 and is available from the Android Market. You can find out more about Outlime at on the product page.

Outlime Screenshots

Example sketches in Outlime

Outlime is inspired by 37Signals’ Draft for iPad. A couple of weeks ago, lots of ideas were spinning round my head for new Amberleaf screens. I was on the verge of buying (another) whiteboard, but resisted cluttering and instead kept scribbling on endless piles of sticky notes.

Just at that time, I stumbled across Draft and hoped that such a minimalist app existed on Android. I couldn’t find one, so instead set about creating the app for myself.

The result is Outlime. Following the extremely minimal concepts that inspired it, Outlime offers two pens (white and green) and an eraser. Sketches are automatically saved when you load another sketch, or exit the app.

Built for Android

Outlime takes advantages of Android technologies, like Intents for sharing Outlime sketches with any registered image app on your phone. This makes it simple to send sketches via any appropriate app installed on your phone, such as Gmail, Facebook, Picasa, and so on. Outlime also leverages the Android MediaStore, so your sketches show up in your phone’s Gallery app.

Outlime benefits from larger screens, although I’ve found it pretty handy for sketching quick layouts on my phone. Although it was developed on a 3″ Pulse, some of the newer Android devices are sporting 4″, 5″ and larger screens.

Writing Outlime has been a great learning experience for me in dealing with the intricacies of the Android SDK (although I have suffered serious ruby-withdrawal!), and in preparing an app for the Market. I’ve got some ideas how I’d like to develop Outlime. If you try out, let me know what you think by tweeting @cblunt or @plymsoftware, or commenting below.

Get Outlime from Android Market

Outlime costs just £1.29 and is available now from Android Market. You can scan the QR code below with your Android Device to view Outlime in the Market (use Barcode Scanner or similar).


Outlime QR Code

Scan the QR code with your Android device to install Outlime via Android Market

Android: Double Taps on a MapView with Overlays

Download the source code for this post PushPin.tar.gz, PushPin.zip.

For the past few days, I’ve been struggling with the Android SDK to detect double-taps on a MapView with overlays. I’m working on Plymouth Software’s first Android app, which makes use of Google’s MapView class. The requirements are:

  • The map can have several pushpin markers overlaid onto it.
  • When the user double-taps an empty part of the map, a new pushpin is added at the tapped location.
  • When the user taps on an existing pushpin, they see a popup (or similar); no new marker is added.

Despite scouring the SDK and web for tutorials, I could only find examples of either detecting double taps (using primitive timers) or adding a list of markers which could be tapped. After several hours, I finally managed to get somewhere. Check out the call for help at the end of the post though!

Creating the Maps Activity

After creating a new Android project in the SDK, switch it to extend MapActivity and add a MapView to the layout. I’ve also setup the MapView with things like built-in zoom controls in the initialiseMapView() method:

<!-- /AndroidManifest.xml -->
 
  <!-- Required to use the Google Maps library -->
  <application>
    <!-- ... -->  
    <uses-library android:name="com.google.android.maps" />
  </application>
 
  <!-- Request permissions to access location and the internet -->
  <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
  <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
  <uses-permission android:name="android.permission.INTERNET" />
/* /src/com/example/PushPinActivity.java */
package com.example;
 
import android.graphics.drawable.Drawable;
import android.os.Bundle;
 
import com.google.android.maps.GeoPoint;
import com.google.android.maps.MapActivity;
import com.google.android.maps.MapController;
import com.google.android.maps.MapView;
import com.google.android.maps.OverlayItem;
 
class PushPinActivity extends MapActivity {
  private MapView mapView;
  private MapController mapController;
 
  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);
 
    initialiseMapView();
  }
 
  @Override
  public boolean isRouteDisplayed() {
    return false;
  }
 
  private void initialiseMapView() {
    mapView = (MapView) findViewById(R.id.mapView);
    mapController = mapView.getController();
 
    mapView.setBuiltInZoomControls(true);
    mapView.setSatellite(false);
 
    GeoPoint startPoint = new GeoPoint((int)(40.7575 * 1E6), (int)(-73.9785 * 1E6));
    mapController.setCenter(startPoint);
 
    mapController.setZoom(8);
  }
}
<!-- /res/layout/main.xml -->
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:orientation="vertical"
  android:layout_width="fill_parent"
  android:layout_height="fill_parent">
  <com.google.android.maps.MapView
    android:id="@+id/mapView"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:clickable="true"
    android:apiKey="YOUR_MAPS_API_KEY" />
</LinearLayout>

Adding Overlays

Next, I’ll add an array of Overlay markers to the map. I couldn’t find much high-level documentation on overlays, but from what I could figure out, the ItemizedOverlay class allows you to store a List of OverlayItems. Each OverlayItem is a marker on the map.

For this example, I’ve just used the default Android application icon as a marker. Let’s add a few Overlays in the initialiseOverlays() method, which is called from onStart() (not onCreate()). Note that I’ve also declared a property, placesItemizedOverlay which is a sub-class of ItemizedOverlay. The GeoPoint locations are from another tutorial I found during my research!

/* /src/com/example/PushPinActivity.java */
class PushPinActivity extends MapActivity {
  private PlacesItemizedOverlay placesItemizedOverlay;
 
  // ...
 
  @Override
  public void onStart() {
    super.onStart();
    initialiseOverlays();
  }
 
  private void initialiseOverlays() {
    // Create an ItemizedOverlay to display a list of markers
    Drawable defaultMarker = getResources().getDrawable(R.drawable.icon);
    placesItemizedOverlay = new PlacesItemizedOverlay(this, defaultMarker);
 
    placesItemizedOverlay.addOverlayItem(new OverlayItem(new GeoPoint((int) (40.748963847316034 * 1E6),
            (int) (-73.96807193756104 * 1E6)), "UN", "United Nations"));
    placesItemizedOverlay.addOverlayItem(new OverlayItem(new GeoPoint(
        (int) (40.76866299974387 * 1E6), (int) (-73.98268461227417 * 1E6)), "Lincoln Center",
        "Home of Jazz at Lincoln Center"));
    placesItemizedOverlay.addOverlayItem(new OverlayItem(new GeoPoint(
        (int) (40.765136435316755 * 1E6), (int) (-73.97989511489868 * 1E6)), "Carnegie Hall",
        "Where you go with practice, practice, practice"));
    placesItemizedOverlay.addOverlayItem(new OverlayItem(new GeoPoint(
        (int) (40.70686417491799 * 1E6), (int) (-74.01572942733765 * 1E6)), "The Downtown Club",
        "Original home of the Heisman Trophy"));
 
    // Add the overlays to the map
    mapView.getOverlays().add(placesItemizedOverlay);
  }
}
/* /src/com/example/PlacesItemizedOverlay.java */
package com.example;
 
import java.util.ArrayList;
 
import android.app.AlertDialog;
import android.content.Context;
import android.graphics.drawable.Drawable;
 
import com.google.android.maps.ItemizedOverlay;
import com.google.android.maps.OverlayItem;
 
public class PlacesItemizedOverlay extends ItemizedOverlay {
  private Context context;
  private ArrayList items = new ArrayList();
 
  public PlacesItemizedOverlay(Context aContext, Drawable marker) {
    super(boundCenterBottom(marker));
    context = aContext;
  }
 
    public void addOverlayItem(OverlayItem item) {
        items.add(item);
        populate();
    }
 
    @Override
    protected OverlayItem createItem(int i) {
        return items.get(i);
    }
 
    @Override
    public int size() {
        return items.size();
    }
 
    @Override
    protected boolean onTap(int index) {
      OverlayItem item = items.get(index);
      AlertDialog.Builder dialog = new AlertDialog.Builder(context);
      dialog.setTitle(item.getTitle());
      dialog.setMessage(item.getSnippet());
      dialog.show();
 
      return true;
    }
}

If you save and run the code now, you’ll see a map centred on Manhattan with several Android-esque markers scattered around. Tapping on one of the markers will popup an AlertDialog with the marker’s title and description. This is nothing more advanced than the standard Google reference documentation:

Android Markers

Detecting Double Taps

For my app, I needed to detect a double-tap anywhere else on the MapView, except where a Marker was displayed. To do this, I started to look at the GestureDetector class, and set about extending MapView to detect double-taps.

/* /src/com/example/PushPinMapView.java */
package com.example;
 
import android.content.Context;
import android.util.AttributeSet;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.GestureDetector.OnGestureListener;
 
import com.google.android.maps.MapView;
 
public class PushPinMapView extends MapView {
  private Context context;
  private GestureDetector gestureDetector;
 
  public PushPinMapView(Context aContext, AttributeSet attrs) {
    super(aContext, attrs);
    context = aContext;
 
    gestureDetector = new GestureDetector((OnGestureListener)context);
    gestureDetector.setOnDoubleTapListener((OnDoubleTapListener) context);
  }
 
  // Override the onTouchEvent() method to intercept events and pass them
  // to the GestureDetector. If the GestureDetector doesn't handle the event,
  // propagate it up to the MapView.
  public boolean onTouchEvent(MotionEvent ev) {
    if(this.gestureDetector.onTouchEvent(ev))
       return true;
    else
      return super.onTouchEvent(ev);
  }
}
/* /src/com/example/PushPinActivity.java */
 
// ...
import android.view.GestureDetector.OnGestureListener;
import android.view.GestureDetector.OnDoubleTapListener;
 
class PushPinActivity extends MapActivity implements OnGestureListener, OnDoubleTapListener {
  // ...
 
  /**
   * Methods required by OnDoubleTapListener
   **/
  @Override
  public boolean onDoubleTap(MotionEvent e) {
    GeoPoint p = mapView.getProjection().fromPixels((int)e.getX(), (int)e.getY());
 
    AlertDialog.Builder dialog = new AlertDialog.Builder(this);
    dialog.setTitle("Double Tap");
    dialog.setMessage("Location: " + p.getLatitudeE6() + ", " + p.getLongitudeE6());
    dialog.show();
 
    return true;
  }
 
  @Override
  public boolean onDoubleTapEvent(MotionEvent e) {
    return false;
  }
 
  @Override
  public boolean onSingleTapConfirmed(MotionEvent e) {
    return false;
  }
 
  /**
   * Methods required by OnGestureListener
   **/
  @Override
  public boolean onDown(MotionEvent e) {
    return false;
  }
 
  @Override
  public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
    return false;
  }
 
  @Override
  public void onLongPress(MotionEvent e) {
  }
 
  @Override
  public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
    return false;
  }
 
  @Override
  public void onShowPress(MotionEvent e) {
  }
 
  @Override
  public boolean onSingleTapUp(MotionEvent e) {
    return false;
  }
}

Finally, make sure you change your main.xml layout file to use the com.example.PushPinMapView instead of the original Google version. This one caught me out whilst writing this post!

<!-- /res/layout/main.xml -->
  <!-- ....Change com.google.android.maps.MapView to use your custom MapView-->
  <com.example.PushPinMapView
    android:id="@+id/mapView"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:clickable="true"
    android:apiKey="YOUR_MAPS_API_KEY" />
  <!-- .... -->

What’s going on?

The custom PushPinMapView creates an instance of GestureDetector and dispatches any touch events (onTouchEvent()) to the designated OnGestureListener. In this code, that is the context instance of PushPinActivity.

If the listener handles the gesture (it’s a double-tap), it shouldn’t not propagate any further (see below). For any gestures that aren’t handled, the GestureDetector propagates the gesture up to other listeners. In this case, it would be handled by the parent MapView gesture handling, which means we don’t have to override things like dragging the map.

Also be sure that your OnGestureListener class imports from the android.view.GestureDetector package.

Call for help…

Whilst functional, the code is still not quite perfect. According to the documentation I’ve found on OnGestureListener, if a method returns true, then the event should not be propagated to any other listeners. However, despite onDoubleTap() returning true in the code above, you’ll find that if you double-tap on one of the OverlayItem markers, both the double-tap dialog and the marker’s dialog are displayed. It seems the MapView is detecting both a single and double-tap.

If you figure out how to stop double-taps on an OverlayItem from triggering a single tap event, please leave a comment and I’ll update the code in the post…Thanks!

Business Startup Week 3: Android

After launching Amberleaf live last week, this week has been eventful as I continued work on the first Android app to be released through Plymouth Software.

The Android platform uses Java, and after indulging in the purity of Ruby for the past couple of years, switching back to Java was a fairly painful process. Nevertheless, after countless brews, I eventually figured out how to get the framework doing what I needed, and began putting together the first parts of my app.

In an upcoming post, I’ll describe how I achieved double-tap detection on a MapView with multiple overlay markers – something I struggled to find much information about online.

Cup of tea and biscuit

Surviving Java

Amberleaf Developments

In the meantime, Amberleaf has been enjoying its first full release week. A number of subscribers have joined from the beta, and there are people trying out the app for the first time thanks to the 30 day free trial.

Visitors to the Amberleaf site have risen quite sharply since launch, increasing by over 300% according to Google Analytics. One problem that this has highlighted is people landing on the registration page (the 2nd most popular entry after the homepage) and leaving after 30 seconds or so.

A friend checking out the site noticed that nowhere on the registration page is the subscription plan (and, importantly, price) mentioned – certainly a barrier to entering registration information! Sometimes it takes a fresh pair of eyes to see what should be obvious! I’ll be revising the registration page accordingly in the coming week.

Explay 2010

On Friday, I headed to Extended Play 2010, Plymouth’s first games event organised by Designed in Devon. Interesting speakers and a panel interview, a great recently redeveloped venue, and seeing a few familiar faces made for a great event. I was also pleasantly surprised to learn of several new resources for startup businesses in the area, including Formation Zone, office spaces designated at the University.

Lecturn at Explay 2010

Waiting for Explay 2010 to start

More inspiring, though, was seeing how much energy there was to see Plymouth recognised as a hub for digital creative excellence. After several years away from that community, I was excited and inspired by what’s happening within local businesses and the University to build a vibrant creative community in the city and its surrounding area.

Overall, what started as a frustrating week reminding myself about Java ended very well. Next week will be exciting as I get my business card designs off to Solopress, and I’ll be working with a couple of customers on their own new projects.