App Widget / Lockscreen / Text Clock

Text Clock – Part 7

In the previous articles in this series we’ve been looking at creating an extremely simple app which has been published to Google Play. Since the update covered in the last article (V1.1.0) was published a bug has been discovered in the app. In this article we’ll look at the tools available to developers to enable us to detect bugs, and implement a fix.

Firstly an apology. In the last article I promised that we’d be looking at a feature next, but the discovery of a crash in the app has rather overridden that. Rather than add new features when many users were unable to use the app because of a crash, I felt it necessary to concentrate on fixing the crash first, so apologies to those of you disappointed that we’re not adding a new feature.

Those that have installed the Text Clock app on their device may have spotted an update which was pushed out rather rapidly last Saturday (23rd February 2013). When the user experiences a crash, they are given the opportunity to report the crash. When they do, the developer gets a report in the developer console, and this is the one that I received:

crash-report

This tells us that the crash was the result of a NoSuchMethodError exception caused by the method android.appwidget.AppWidgetManager.getAppWidgetOptions. If we look at the javadoc for this particular method, we can see that it was introduced in API level 16:

javadoc

The problem should be pretty clear from these two pieces of information: When the app was run on a device running an OS earlier than API 16 (Jelly Bean 4.1) the method in question was not available in the OS, and so the NoSuchMethodError exception was thrown resulting in the crash. The report also tells us where this particular method was being called: the updateTime method of TextClockService:

private void updateTime( Calendar date )
{
    Log.d( TAG, "Update: " + dateFormat.format( date.getTime() ) );
    AppWidgetManager manager = AppWidgetManager.getInstance( this );
    ComponentName name = new ComponentName( this, TextClockAppWidget.class );
    int[] appIds = manager.getAppWidgetIds( name );
    String[] words = TimeToWords.timeToWords( date );
    for ( int id : appIds )
    {
        Bundle options = manager.getAppWidgetOptions( id );
        int layoutId = R.layout.appwidget;
        if ( options != null )
        {
            int type = options.getInt( "appWidgetCategory", 1 );
            if ( type == 2 )
            {
                layoutId = R.layout.keyguard;
            }
        }
        RemoteViews v = new RemoteViews( getPackageName(), layoutId );
        updateTime( words, v );
        manager.updateAppWidget( id, v );
    }
}

Although we have some contingency for backwards compatibility built in here, as has been previously discussed, the inclusion of getAppWidgetOptions was an oversight which caused the problem.

Resolving this is relatively straightforward and is a technique that we’ve used before to ensure backwards compatibility: We need to wrap the method in an OS version check, and we can apply an annotation to acknowledge that the code will only be used on certain versions of Android:

private void updateTime( Calendar date )
{
    Log.d( TAG, "Update: " + dateFormat.format( date.getTime() ) );
    AppWidgetManager manager = AppWidgetManager.getInstance( this );
    ComponentName name = new ComponentName( this, TextClockAppWidget.class );
    int[] appIds = manager.getAppWidgetIds( name );
    String[] words = TimeToWords.timeToWords( date );
    for ( int id : appIds )
    {
        int layoutId = R.layout.appwidget;
        if ( Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN )
        {
            if ( getAppWidgetCategory( manager, id ) == WIDGET_CATEGORY_KEYGUARD )
            {
                layoutId = R.layout.keyguard;
            }
        }
        RemoteViews v = new RemoteViews( getPackageName(), layoutId );
        updateTime( words, v );
        manager.updateAppWidget( id, v );
    }
}

@TargetApi( Build.VERSION_CODES.JELLY_BEAN )
private int getAppWidgetCategory(AppWidgetManager manager, int id)
{
    int category = WIDGET_CATEGORY_HOME_SCREEN;
    Bundle options = manager.getAppWidgetOptions( id );
    if ( options != null )
    {
        category = options.getInt( "appWidgetCategory", 1 );
    }
    return category;
}

This will now ensure that the method in question only gets called upon Android versions which support it. We maintain our backward compatibility while resolving the crash.

There is one small change that we need to make: The minimum API level needs to be changed in the manifest from API 3 (Android Cupcake 1.5) to API 4 (Android Donut 1.6) because Build.VERSION.SDK_INT is only supported from Donut onwards. As Cupcake no longer appears on the Platform Versions list, and there a no current API 3 users of Text Clock according to the developer console, I think that it is fairly safe to make this change rather than introduce additional complexity to support a OS level that no-one is currently using.

This has turned out to be a rather unexpected detour on creation of a simple app, but demonstrates some of the tools which can help us to trace issues within our published apps. I would also point out that using the original code under Eclipse results in an error because of the rather excellent lint checking build in to ADT. However, I was developing the code under IntelliJ IDEA (which I’m somewhat new to) which was not running the lint checks. So the moral of the story is: Always run the lint checks before making a build release – it can potentially save you a lot of trouble!

Hopefully we’ll get back on track in the next article and add the feature that was promised this time around.

This bug fix release was published to Google Play as V1.1.1 and the source code can be found here.

© 2013 – 2014, Mark Allison. All rights reserved.

Copyright © 2013 Styling Android. All Rights Reserved.
Information about how to reuse or republish this work may be available at http://blog.stylingandroid.com/license-information.

2 Comments

    1. Erm, did you read the article? I explain that it does but wasn’t running in my IntelliJ IDEA IDE.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.