Android Development Gotchas

Android Development Gotchas

This isn’t designed to be a definitive list of unexpected occurrences while developing for Android, but it is a list that I scratched down while I was picking up some skill for myself. It won’t save you from every issue but there may be cases where you can stop and think “Yeah, Dave told me that would happen”.

My development environment included Eclipse 3.5 with the Google ADT plugin and all testing was against a rooted HTC Desire and unroot Samsung Galaxy SII both running Android 2.2, development code targeted Android 2.1. Problems may be an issue with ADT, HTC, Samsung, Android 2.1 or 2.2, rooting or the specific hand sets 🙂

Java 1.6 on a Dell XPS 1530 running Ubuntu 10.10, if that matters.

Virtual Devices

I found these too slow and unusable.

Early on in development I gave up trying to use the the ADT virtual devices and tested directly against the target hardware devices. Luckily for me the application was for internal use only and would be released to specific clients with specific handsets, but I was disappointed at how slow the VMs were start and how slow they ran. The develop-deploy-test cycle was just too slow using the ADT.

Virtual Devices Cannot Support Bluetooth

ADT Virtual Devices cannot support Bluetooth, and as our application includes Bluetooth communication this was another reason ADT development was excluded.

Inconsistent Bluetooth Support Between Vendors

This was a strange one to us, and not one that we have (to this point) been able to resolve although my guess is that it is a problem with the HTC code.

Our application involved having a file sent to the phone using OBEX over Bluetooth. No problems so far. Pair the devices, enter the code, initiate the file transfer.

The Samsung Galaxy SII displayed the preferred behaviour, where the incoming file prompt the phone user to accept the file, but also provides a checkbox to always accept files from this device. If you don’t select the checkbox then you are prompted each time a file arrives, but you are still able to check the box later on.

When the first file arrives, HTC Desire asks if you want to allow the remote device to synchronize contacts, and whether you always want to allow this. If you don’t allow this, you aren’t able to get to the next step where you accept the file. If you don’t accept contact swap all the time you’ll be prompted each time. Regardless of whether you swap contacts or not, you must accept each file as it comes in. Accepting every file manually is not our preferred behaviour.

Android Apps Run on a Single Thread

Some of this cuts into Android’s Handlers, task scheduling and communication across threads, but it may come as a surprise during development that all standard processing occurs on a single Thread. This is something that you’ll want to play with and become familiar with as it may be surprising that UI performance is impacted by Runnables and Services.

I do recommend you have a play.

Creating new Threads is one option, although Android’s AsyncTask is the preferred mechanism for truly asynchronous processing that doesn’t freeze the GUI.

… and that Thread is a UI Thread

Following on from the previous point, the main thread is a UI thread and that that thread is only active when the UI is active. Sort of. Check the Android fundamentals processes and threads page, but just be aware that if you want something to process while (for example) the screen is locked, you need to be aware of the processing thread.

Beware Empty Elements in XML Layouts

If you’re like me, you like tidy XML and prefer an empty element (where applicable) to an element which is not ’empty’ but has no child elements.

	<!-- this element has no children -->
	<ListView android:layout_width="fill_parent" android:layout_height="wrap_content" android:id="@android:id/list"></ListView>
	<!-- this is an empty element -->
	<TextView android:id="@android:id/empty" style="@style/EmptyContent" android:text="@string/emptyResponseEntries" />

In this example, the TextView is empty and causes no problems, but the ListView will. If you make the ListView an empty XML element, you can write items to the List, but they won’t show up on the screen. There won’t be any error reported, it just won’t show up.

Can’t Read Property Defaults

Property defaults were also something I found perplexing. If you specify a properties.xml file, each property is able to define a default value. Likewise when reading properties via the SharedPreferences you can specify a default value if the property doesn’t exist, but the two default values aren’t related. You can’t read the default value as defined in the preferences.xml file for use elsewhere, the API doesn’t provide access.

Note that recommended properties management is via the PreferenceFragment

Notifications Can Be Empty

I found this one amusing. It is possible to create an empty Notification using new Notification(), specify a notification sound, and annoy users without providing any indication of where the annoying sounds are coming from.

Improving the custom Android Gallery

I was looking for a custom gallery for displaying and selecting images on an Android phone, and while I found the sample code here to be a good start it needed some work before it was useful.

Before starting, it is worth pointing out that the Android API supplies a perfectly useful image gallery, and the StackOverflow thread how to pick a image from gallery (SD Card) for my app in android? has everything you need to get plug it into your own application.

If you require customised behaviour it isn’t too hard to get something rough working, so go to the original thread and create the ImageThumbnailsActivity and ViewImage classes, then configure the AndroidManifest.xml, res/layout/main.xml and missing res/values/strings.xml files.

The layout specifies a single GridView to hold the images, and the main ImageThumbnailsActivity activity performs two main jobs. First it loads all existing thumbnails and provides the code necessary to interact with the thumbnail if it is selected, and it has a nested adapter to manage the display of the thumbnails using a Cursor position into the GridView.

There are quite a few comments in the original posting indicating that the code is not quite right, and if you run it on a phone with a decent history of photos you’ll get some unusual behaviour. The two main issues are that firstly there are far more thumbnails displayed than there are images stored, and secondly that the images displayed are repeated as you scroll through the gallery.

The first issue can be resolved using the StackOverflow thread here and involves initially loading the full images rather than the thumbnail, but associating with the thumbnail when displaying on the grid. This means that we don’t try to show a thumbnail for an image that no longer exists or is incorrect for some other reason.

The second issue is a little more involved, but related to the fact that the nested ImageAdapter class will attempt to reuse View instances if possible (in the getView method), and the code provided assumes that if a View is returned to be reused that it is correct, which it is not. The GridView only creates as many View instances as required to fill the screen. Once you scroll to a new section it asks if you want to reuse the instances that have scrolled off the screen. Obviously this is an acceptable reuse of resources, but the reused instances will be showing the thumbnail for a different picture and will need to be updated.

As a matter of style, I also made the following changes:

  • Remove all of the System.gc() calls, they aren’t required.
  • close cursors
  • make variables local where applicable
  • as stated we load images from the MediaStore.Images.Media rather than the MediaStore.Images.Thumbnails but return the thumbnail for display.
  • specify the MediaStore.Images.Thumbnails.MICRO_KIND for display. The benefit here is that the images are the size we intend to show and eliminited scaling. Feel free to play with settings but I found this the most pleasing layout and matches the existing Android gallery

Finally, if you look up the thumbnail when the View is visible on the screen, the scrolling is slow and limited to a row at a time rather than being able to race past images they do not want. Since we specify the thumbnails as the smallest size, I chose to load and cache the android.graphics.Bitmap for the thumbnails so they are ready to display when required.

Just to Review

We’re doing this to provide a customised image gallery. The Android gallery is fully functional and should suit most needs, but if for example you also needed to display the file name or size, date or other information, this solution allows a custom View on the GridView.

Everything else remains the same, and the updated ImageThumbnailsActivity looks like this:

public class ImageThumbnailsActivity extends Activity
{
    private int count;
    /**
     * Caching this is a trade off in memory versus performance.
     * We deliberated use the smallest thumbnails so the size of each should be small (92x92x1 byte = about 10kb each)
     * but can be significant if there are thousands of images on the device.
     */
    private Bitmap[] windows;

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

    private void init_phone_image_grid()
    {
        final String[] columns = { MediaStore.Images.Media._ID };
        final String orderBy = MediaStore.Images.Media._ID;
        Cursor imagecursor = managedQuery(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, columns, null, null, orderBy);
        int image_column_index = imagecursor.getColumnIndex(MediaStore.Images.Media._ID);
        this.count = imagecursor.getCount();
        this.windows = new Bitmap[this.count];
        for(int i=0;i<this.count;i++)
        {
            imagecursor.moveToPosition(i);
            int id = imagecursor.getInt(image_column_index);
            windows[i] = MediaStore.Images.Thumbnails.getThumbnail(getApplicationContext().getContentResolver(),
                    id, MediaStore.Images.Thumbnails.MICRO_KIND, null);
        }
        GridView imagegrid = (GridView) findViewById(R.id.PhoneImageGrid);
        imagegrid.setAdapter(new ImageAdapter(getApplicationContext()));
        imagegrid.setOnItemClickListener(new OnItemClickListener() {
            public void onItemClick(@SuppressWarnings("rawtypes") AdapterView parent, View v, int position, long id)
            {
                String[] columns = { MediaStore.Images.Media.DATA, MediaStore.Images.Media._ID };
                Cursor actualimagecursor = managedQuery(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, columns, null, null, null);
                final int dataColumnIndex = actualimagecursor.getColumnIndex(MediaStore.Images.Media.DATA);
                final int idColumnIndex = actualimagecursor.getColumnIndex(MediaStore.Images.Media._ID);
                actualimagecursor.moveToPosition(position);
                final String filename = actualimagecursor.getString(dataColumnIndex);
                final long imageId = actualimagecursor.getLong(idColumnIndex);
                final Intent intent = new Intent(ImageThumbnailsActivity.this, ViewImage.class);
                intent.putExtra("filename", filename);
                intent.putExtra("dataUid", imageId);
                actualimagecursor.close();
                startActivity(intent);
            }
        });
        imagecursor.close();
    }

    public class ImageAdapter extends BaseAdapter
    {
        private Context mContext;

        public ImageAdapter(Context c)
        {
            mContext = c;
        }

        @Override
        public int getCount()
        {
            return count;
        }

        @Override
        public Object getItem(int position)
        {
            return position;
        }

        @Override
        public long getItemId(int position)
        {
            return position;
        }

        @Override
        public View getView(int position, View convertView, ViewGroup parent)
        {
            ImageView i = (ImageView)convertView;
            if(i!=null)
            {
               i.setImageBitmap(windows[position]);
            }
            else
            {
                i = new ImageView(mContext.getApplicationContext());
                i.setImageBitmap(windows[position]);
                i.setLayoutParams(new GridView.LayoutParams(92, 92));
            }
            return i;
        }
    }
}

WTF from the Past

The missing image

I was remembering a strange problem from a few years ago and thought I would share the issue.

We had a simple web page around the year 2002 which contained either a plus or a minus image, not unlike these: plus minus

The problem we had was that regardless of the code we used, we could only ever see the minus image. Checking the page source and viewing the image on its own told us that we should be seeing the plus image where appropriate, and yet on the page itself we only ever saw the minus image.

How does your browser scale images?

It turns out that actual images were 11 pixels wide and 11 pixels high, but the image tag was telling the page to display as 10 pixels wide. I’ll show you what that looks like in today’s browsers, but your mileage may vary: plus minus

Hopefully you saw something like this: plus minus

If you’re unlucky you’ll see something like this: plus minus

Depending on the scaling algorithm used by your browser, scaling from width 11 to width 10 may just remove the middle line, making a plus look like a minus. Problem solved. I didn’t pick it and I’m sad to say that it took too long to pick up the issue.