App Widget / IntentService / RemoteViews / Text Clock

Text Clock – Part 3

In the previous article we got an app widget background displaying on the home screen, but it does not actually display any real data, which is somewhat useless! In this article we’ll look at displaying the time on our app widget.

To update of the time on our widget we’ll use an IntentService. IntentService is a specialised form of Android Service which gets started in the usual way by calling Context.startService(Intent intent), it performs an action but then shuts itself down once that action is complete. It is very well suited to small, periodic operations and, unlike a background service, is highly resilient to task killers for the simple reason that, provided it is well behaved, it simply will not exist for long enough to be discovered and killed!

To implement our IntentService implementation we must override the constructor and the onHandleIntent() method, and we’ll also declare a DateFormat that we’ll use later :

public class TextClockService extends IntentService
{
    private static final DateFormat dateFormat = 
        new SimpleDateFormat( "yyyy-MM-dd HH:mm:ss.SSS" );
    private static final String TAG = "TextClockService";

    public static final String ACTION_UPDATE = 
        "com.stylingandroid.textclock.ACTION_UPDATE";

    public TextClockService()
    {
        super( TAG );
    }

    @Override
    protected void onHandleIntent( Intent intent )
    {
        if(intent.getAction().equals( ACTION_UPDATE ))
        {
            // TODO: handle the Intent
        }
    }
}

The onHandleIntent() method will be called each time the IntentService is started, and the service will shut down once we exit this method. We’ve also defined a custom action which we’ll use to trigger a widget update.

If we are likely to take any significant time in this method it would be advisable to acquire a WakeLock to prevent the device from sleeping (and thus stopping our service from running), or use Mark Murphy’s WakefulIntentService. However, we should only be processing for a very short time, so we don’t need to do this.

Of course we now need to declare this in our Manifest along with the appropriate intent filter so that it responds to our custom action:


    
        
        
    

So that’s our IntentService defined, but how can we update our app widget from here? An app widget is rather different from a standard Activity where you can do pretty much whatever you like. An app widget is running on the home screen and there may be other app widgets running that we cannot interfere with. For example, within a normal Activity individual controls can draw outside of their own bounds, and even those of their parent layouts if they allow it via their clipChildren attribute. However doing this on the home screen could interfere with other widgets, and to prevent this kind of bad behaviour from app widgets, they do not get direct access to the app widget layout and it’s child views. Instead they must use a RemoteViews object to update these views. RemoteViews is a proxy which provides us with limited access to these views, and consequently we have a limtied subset of widgets that we can use in our app widget layouts. These are:

  • AnalogClock
  • Button
  • Chronometer
  • ImageButton
  • ImageView
  • ProgressBar
  • TextView
  • ViewFlipper
  • ListView
  • GridView
  • StackView
  • AdapterViewFlipper

We are also limited to the following layout types:

  • FrameLayout
  • LinearLayout
  • RelativeLayout
  • GridLayout

Although this seems somewhat restrictive, we can still do some pretty cool stuff.

So let’s update our service to update the time whenever the service is started:

@Override
protected void onHandleIntent( Intent intent )
{
    if(intent.getAction().equals( ACTION_UPDATE ))
    {
        Calendar now = Calendar.getInstance();
        updateTime( now );
    }
}

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 )
    {
        RemoteViews v = new RemoteViews( getPackageName(), R.layout.appwidget );
        updateTime( words, v );
        manager.updateAppWidget( id, v );
    }

}

private void updateTime( String[] words, RemoteViews views )
{
    views.setTextViewText( R.id.hours, words[0] );
    if ( words.length == 1 )
    {
        views.setViewVisibility( R.id.minutes, View.INVISIBLE );
        views.setViewVisibility( R.id.tens, View.INVISIBLE );
    }
    else if ( words.length == 2 )
    {
        views.setViewVisibility( R.id.minutes, View.INVISIBLE );
        views.setViewVisibility( R.id.tens, View.VISIBLE );
        views.setTextViewText( R.id.tens, words[1] );
    }
    else
    {
        views.setViewVisibility( R.id.minutes, View.VISIBLE );
        views.setViewVisibility( R.id.tens, View.VISIBLE );
        views.setTextViewText( R.id.tens, words[1] );
        views.setTextViewText( R.id.minutes, words[2] );
    }
}

The updateTime() method obtains an AppWidgetManager instance which allows us to update widgets on the system. We then lookup the component name of our AppWidgetProvider instance, and use this to obtain a list of app widget IDs for this app widget. There may be multiple instances of the widget as the user may have added it in more than one place, so this iterating all of the active app widget ids is necessary to ensure that we update them all. We then generate the words which make up the current time from the business logic code that we defined in the first article in this series. We then iterate through the ids, creating a RemoteView object based upon the app widget layout that we defined in the previous article, and then call updateTime() to update those views, before actually applying the update via the AppWidgetManager.

The updateTime() method changes the visibility of the TextViews in the layout based on the number of words that we need to display, and also sets the text of them. The RemoteViews object permits this control, albeit in a limited way.

To get this to work, we’ll need to actually start the IntentService. We can do this from the onUpdate method of our AppWidgetProvider which will be called when the widget gets added to the home screen:

public class TextClockAppWidget extends AppWidgetProvider
{
    private static final String TAG = "TextClockWidget";
    private static final Intent update = new Intent( TextClockService.ACTION_UPDATE );
    private Context context = null;

    @Override
    public void onUpdate( Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds )
    {
        Log.d( TAG, "onUpdate" );
        this.context = context;
        this.context.startService( update );
    }
}

If we run this, we can see that the app widget text is now set, but if we compare the app widget time to the clock in the status bar we can see that the time does not get updated:

widget-no-update

In the next article in this series we’ll get the time of the widget updating.

The source code for this article can be found here, and TextClock is available from Google Play.

© 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.

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.