Android App Widgets and their Configuration

Fork me on GitHub

Recently, I tried to implement an Android App Widget with a Configuration Activity.

It didn’t went smoothly but I did learn something on the way.
I will expose what I could learn this time in three four posts: this first one with the bare minimum to make a working widget + configuration, a second one where I’ll describe my attempt to direct a click event to only one instance among several instances of the same widget, a second third one with an AlarmManager that trigs the updates and a third fourth and last one in which I will expose a known bug and my attempt to solve it.

The theory

An Android App Widget may have a Configuration Activity that launches when the user add the widget on the Home Screen. It is used to let the user customize some settings of the impending widget, like the color or what data to show.
It is not mandatory but it may comes in handy in many use cases.

The basic steps to implement an App Widget with its Configuration Activity are the follow:

  1. Define an appwidget provider info file.
    This xml file contains the metadata of the widget. Among all the properties you can define for a widget, the most important ones are:

    • The size of the widget, width and height. Android will take these numbers and convert them in number of cells accordingly.
    • An update interval, in milliseconds. The widget will use this value as some sort of periodic timetable and refresh itself at each occurrences.
    • The class that will act as the Configuration Activity, described with its full scope.
  2. Insert a receiver tag in the application of the Android Manifest file.
    At its base, the receiver section tells to Android three things:

    • Where to find the class that implement the Provider of our widget.
    • That our Provider will intercepts the system intent that tells that the widget must update itself. This system intent is android.appwidget.action.APPWIDGET_UPDATE.
    • Where is the appwidget provider info file, through a meta-data tag.

    This step is considered as the “registration step” of our widget.

  3. Create a layout file for our widget.
  4. Define a Provider class, that extends an AppWidgetProvider. This class is called at each steps of the lifecycle of our widget and is responsible of the actual creation of our widget.
    The most important method of AppWidgetProvider that our class has to override, is onUpdate(). As the name suggests, this method is called whenever the widget must update. The frequency of the updates is defined in the appwidget provider info file.
    The Provider creates the UI of the widget through instances of RemoteViews, inflating the layout file.
    When Android fire an android.appwidget.action.APPWIDGET_UPDATE intent, the onReceive() method of AppWidgetProvider dispatch that call to onUpdate(). In other words, onReceive() is the “gate” through which the update trigger passes.
  5. Define a Configuration Activity.
    Remember that this Activity is not mandatory.
    To transform an activity to a configuration one, just let it takes android.appwidget.action.APPWIDGET_CONFIGURE intents, after registering it in the appwidget provider info file.
    In onCreate() we have to ensure that a valid widget ID is passed to us and we have to not add any widgets if the user press BACK or cancel the Activity.
    In case of a successful creation of the widget, the Activity has to return the ID in the result intent.
    Also, according to the documentation, when a Configuration Activity is present, Android won’t call onUpdate() for the first time automatically, letting us call it after we have completed the actual configuration.
    To call the AppWidgetProvider, simply launch a broadcast Intent accordingly:

               final Context context = MaLuBuTestWidgetActivity.this;
               AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
               ComponentName thisAppWidget = new ComponentName(context.getPackageName(),
                                              MaLuBuTestWidgetActivity.class.getName());
               //Launch to the AppWidgetProvider.
               Intent firstUpdate = new Intent(context, MaLuBuTestWidgetProvider.class);
               int[] appWidgetIds = appWidgetManager.getAppWidgetIds(thisAppWidget);
               firstUpdate.setAction("android.appwidget.action.APPWIDGET_UPDATE");
               firstUpdate.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, appWidgetIds);
               context.sendBroadcast(firstUpdate);
               

The code

This is a very simple example: a widget that displays the time of the update, and it refreshes every 180000 milliseconds or when the user tap on the widget.
Please, read the comments I have left in the code for more details.
Here I just paste some of the files, following the steps I described earlier.

App widget provider file. Supposed to be the file xml/widget_provider.xml

<?xml version="1.0" encoding="utf-8"?>
<!-- 4 x 1 cells -->
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
    android:minHeight="60dp"
    android:minWidth="290dp"
    android:configure="com.wordpress.malubu.MaLuBuTestWidgetActivity"
    android:updatePeriodMillis="180000">
</appwidget-provider>

AndroidManifest.xml file

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.wordpress.malubu"
    android:versionCode="1"
    android:versionName="1.0.0" >

    <uses-sdk android:minSdkVersion="7" android:targetSdkVersion="14"/>

    <application
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name" >
        <activity
            android:name=".MaLuBuTestWidgetActivity"
            android:label="@string/app_name" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
                <action android:name="android.appwidget.action.APPWIDGET_CONFIGURE"/>
            </intent-filter>
        </activity>
        <receiver android:name="com.wordpress.malubu.MaLuBuTestWidgetProvider" >
            <intent-filter >
                <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
            </intent-filter>
            <meta-data
                android:name="android.appwidget.provider"
                android:resource="@xml/widget_provider" />
       </receiver>
    </application>
</manifest>

The widget layout.

<?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:background="@android:color/white">
   
   <TextView android:id="@+id/label"
      android:text="@string/app_name"
      android:layout_width="fill_parent"
      android:layout_height="fill_parent"
      android:textColor="@android:color/black"
      android:textStyle="bold"
      android:layout_gravity="center"
      style="@android:style/TextAppearance.Large">
   </TextView>
</LinearLayout>

The configuration activity layout.

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical" >

    <Button android:id="@+id/main_ok"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_vertical|center_horizontal"
        android:text="@android:string/ok"
        android:onClick="mainOk"/>

</FrameLayout>

The widget provider.

public class MaLuBuTestWidgetProvider extends AppWidgetProvider
   {
   
   @Override
   public void onUpdate(Context context,
                        AppWidgetManager appWidgetManager,
                        int[] appWidgetIds)
      {
      Log.d("onUpdate", "called");
      for (int widgetId : appWidgetIds)
         {
         updateAppWidget(context,
                         appWidgetManager,
                         widgetId);
         }
      }
   
   private void updateAppWidget(Context context,
                                AppWidgetManager appWidgetManager,
                                int appWidgetId)
      {
      //Inflate layout.
      RemoteViews remoteViews = new RemoteViews(context.getPackageName(),
                                                R.layout.widget);
      //Update UI.
      remoteViews.setTextViewText(R.id.label, getTimeStamp());
      //When user click on the label, update ALL the instances of the widget.
      Intent labelIntent = get_ACTION_APPWIDGET_UPDATE_Intent(context);
      PendingIntent labelPendingIntent = PendingIntent.getBroadcast(context,
                                                                    appWidgetId,
                                                                    labelIntent,
                                             PendingIntent.FLAG_UPDATE_CURRENT);
      remoteViews.setOnClickPendingIntent(R.id.label, labelPendingIntent);
      Log.d("updateAppWidget", "Updated ID: " + appWidgetId);
      //Call the Manager to ensure the changes take effect.
      appWidgetManager.updateAppWidget(appWidgetId, remoteViews);
      }
   
   /**
    * Utility method to ensure that when we want an Intent that fire ACTION_APPWIDGET_UPDATE, the extras are correct.<br>
    * The default implementation of onReceive() will discard it if we don't add the ids of all the instances.
    * @param context
    * @return
    */
   protected Intent get_ACTION_APPWIDGET_UPDATE_Intent(Context context)
      {
      AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
      ComponentName thisAppWidget = new ComponentName(context.getPackageName(),
                                     MaLuBuTestWidgetProvider.class.getName());
      int[] appWidgetIds = appWidgetManager.getAppWidgetIds(thisAppWidget);
      Intent intent = new Intent(context, MaLuBuTestWidgetProvider.class);
      intent.setAction(AppWidgetManager.ACTION_APPWIDGET_UPDATE);
      intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, appWidgetIds);
      return intent;
      }
   
   private String getTimeStamp()
      {
      String res="";
      Calendar calendar = Calendar.getInstance();
      calendar.setTimeInMillis(System.currentTimeMillis());
      Date now = calendar.getTime();
      res += now.getHours()+":"+now.getMinutes()+":"+now.getSeconds();
      return res;
      }
   }

The widget configuration.

public class MaLuBuTestWidgetActivity extends Activity
   {
   /**
    * Widget ID that Android give us after showing the Configuration Activity.
    */
   private int widgetID;
   
   @Override
   public void onCreate(Bundle savedInstanceState)
      {
      super.onCreate(savedInstanceState);
      widgetID = AppWidgetManager.INVALID_APPWIDGET_ID;
      Intent intent = getIntent();
      Bundle extras = intent.getExtras();
      if (extras != null)
         {
         widgetID = extras.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID, 
                                      AppWidgetManager.INVALID_APPWIDGET_ID);
         }
      //No valid ID, so bail out.
      if (widgetID == AppWidgetManager.INVALID_APPWIDGET_ID)
          finish();
      //If the user press BACK, do not add any widget.
      setResult(RESULT_CANCELED);
      setContentView(R.layout.main);
      }
   
   public void mainOk(View source)
      {
      Log.d("mainOk", "called");
      //Configuration...
      //Call onUpdate for the first time.
      Log.d("Ok Button", "First onUpdate broadcast sending...");
      final Context context = MaLuBuTestWidgetActivity.this;
      AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
      ComponentName thisAppWidget = new ComponentName(context.getPackageName(),
                                     MaLuBuTestWidgetActivity.class.getName());
      //N.B.: we want to launch this intent to our AppWidgetProvider!
      Intent firstUpdate = new Intent(context, MaLuBuTestWidgetProvider.class);
      int[] appWidgetIds = appWidgetManager.getAppWidgetIds(thisAppWidget);
      firstUpdate.setAction("android.appwidget.action.APPWIDGET_UPDATE");
      firstUpdate.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, appWidgetIds);
      context.sendBroadcast(firstUpdate);
      Log.d("Ok Button", "First onUpdate broadcast sent");
      //Return the original widget ID, found in onCreate().
      Intent resultValue = new Intent();
      resultValue.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, widgetID);
      setResult(RESULT_OK, resultValue);
      finish();
      }
   }

The note

A note on MaLuBuTestWidgetProvider.get_ACTION_APPWIDGET_UPDATE_Intent().
As I wrote in the comment, when we fire an android.appwidget.action.APPWIDGET_UPDATE we have to put the ids of all the instances of the widget in the intent’s extras.
Remember that Android is open source? Just search for the code of onReceive() and we’ll find what we need.
Here it is, for convenience:

public void onReceive(Context context, Intent intent) {
        // Protect against rogue update broadcasts (not really a security issue,
        // just filter bad broacasts out so subclasses are less likely to crash).
        String action = intent.getAction();
        if (AppWidgetManager.ACTION_APPWIDGET_UPDATE.equals(action)) {
            Bundle extras = intent.getExtras();
            if (extras != null) {
                int[] appWidgetIds = extras.getIntArray(AppWidgetManager.EXTRA_APPWIDGET_IDS);
                if (appWidgetIds != null && appWidgetIds.length > 0) {
                    this.onUpdate(context, AppWidgetManager.getInstance(context), appWidgetIds);
                }
            }
        }
        else if (AppWidgetManager.ACTION_APPWIDGET_DELETED.equals(action)) {
            Bundle extras = intent.getExtras();
            if (extras != null && extras.containsKey(AppWidgetManager.EXTRA_APPWIDGET_ID)) {
                final int appWidgetId = extras.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID);
                this.onDeleted(context, new int[] { appWidgetId });
            }
        }
        else if (AppWidgetManager.ACTION_APPWIDGET_ENABLED.equals(action)) {
            this.onEnabled(context);
        }
        else if (AppWidgetManager.ACTION_APPWIDGET_DISABLED.equals(action)) {
            this.onDisabled(context);
        }
    }

As we can see in the first if, if there are no extras and if those extras are not an array, the intent is discarded and no onUpdate() take place.

The results

Just run the widget in the emulator:

Advertisements

About Luong
http://www.linkedin.com/in/manhluong

9 Responses to Android App Widgets and their Configuration

  1. paldrive says:

    Ciao, I was looking for an android widget example to use to show local weather on my Galaxy Note homescreen. Yours is probably the most complete example on the net but I would have the need to show a web image (a png from an url I type when I add the widget to homescreen) instead of text (the clock of your example). Consider that I am a total newbie with android so I would be very glad if you can code it on a fifth post in your blog πŸ˜‰
    Thankyou very much!

    • Luong says:

      Ciao paldrive! πŸ™‚

      For your situation, there are three things to check:
      1) Use a TextView in the configuration activity. This is easy to implement, just Google it around to find how to add a TextView to an Activity and retrieve its value (in the example, in the mainOk() method).
      2) Download a file/image from the web. Here you can find an example, but you can Google it for more.
      3) Finally, to show the image, check out ImageView.

      As for me, I don’t know if I ever continue these posts, as I am moving on to other things with Android, that I will write here, of course.

      Anyway never say never! πŸ™‚

  2. Vanessa says:

    This was a great tutorial, i was having problems trying to launch my configuration activity. Thanks!

  3. Pingback: Part -1: Creating a Simple App Widget with Configuration Actvitiy « Prativa's Blog

  4. It is not my first time to visit this website, i am visiting this
    site dailly and get good facts from here everyday.

  5. I truly enjoy looking at on this site, it holds wonderful posts . “The secret of eternal youth is arrested development.” by Alice Roosevelt Longworth.

  6. But how can I save variables in extras in Configuration File and read them later in widgetProvider? I spend about… 6hours trying to do this by changing your code – no luck. I lost with intents…

  7. Hi there! I’m at work surfing around your blog from my new iphone 4!
    Just wanted to say I love reading through your blog and look forward
    to all your posts! Keep up the great work!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s