Home > android, ui > Per-widget options, stale widgets

Per-widget options, stale widgets

A feature often requested by users of BluetoothWidget and Wifi Manager is to be able to show widgets without text lables below. This is how I implemented this feature (not released as of yet).

Implementing the UI for configuring text label option is quite easy, all it takes is a configuration activity associated with a widget.

<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider
  xmlns:android="http://schemas.android.com/apk/res/android"
    android:minWidth="70dp"
    android:minHeight="72dp"
    android:updatePeriodMillis="3600000"
    android:initialLayout="@layout/appwidget"
    android:configure="org.kman.BluetoothWidget.BTWidgetConfigure" 
     >
</appwidget-provider>

Next, we need some place to store per-widget configuration. Putting data in AppWidgetProvider doesn’t work, since it can be destroyed and recreated by Android at any time. This happens not only in low memory situation, but also when memory is plentiful.

My solution was to store per-widget settings using SharedPreferences, keyed by the widget id. The widget id is available in the config activity, and is also passed to your AppWidgetProvier’s onUpdate.

	@Override
	public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
		Log.i(TAG, "onUpdate, appWidgetIds.length = " + String.valueOf(appWidgetIds.length));

		final int N = appWidgetIds.length;

		// For each App Widget that belongs to this provider
		for (int i = 0; i < N; i++) {
			int widgetId = appWidgetIds[i];

			// Load widget prefs
			WidgetPrefs prefs = new WidgetPrefs();
			prefs.load(context, widgetId);

			// Build view update
			RemoteViews updateViews = buildUpdate(context, widgetId, prefs);

			// Tell the AppWidgetManager to perform an update
			appWidgetManager.updateAppWidget(widgetId, updateViews);
		}
	}

WidgetPrefs is a simple class that loads per-widget settings based on the widgetId passed in:

public class WidgetPrefs {

	public final static String PREFS_KEY = "BluetoothWidget";
	
	public final static String SHOW_LABEL_KEY = "ShowLabel";
	public final static boolean SHOW_LABEL_DEFAULT = true;

	public final static String WIDGET_PRESENT_KEY = "WidgetPresent";
	public final static boolean WIDGET_PRESENT_DEFAULT = false;	

	public boolean mShowLabel;

	public WidgetPrefs() {
		mShowLabel = true;
	}

	public boolean load(Context context, int widgetId) {
		SharedPreferences prefs = context.getSharedPreferences(PREFS_KEY, Context.MODE_PRIVATE);
		if (prefs != null) {
			String showLabelKey = SHOW_LABEL_KEY + String.valueOf(widgetId);
			mShowLabel = prefs.getBoolean(showLabelKey, SHOW_LABEL_DEFAULT);
			
			String widgetPresentKey = WIDGET_PRESENT_KEY + String.valueOf(widgetId);
			return prefs.getBoolean(widgetPresentKey, WIDGET_PRESENT_DEFAULT);
		}
		return false;
	}
}

There is a corresponding store method, that’s called from the configuration activity to store settings for this widgetId.

So far so good, but what is WIDGET_PRESENT_KEY and why does load return a boolean?

In my widgets, I often need to make updates based on system events (such as Bluetooth turning on and off, or Wifi connection changing). This is where I ran into an Android bug.

My first attempt looked like this:

	public static void updateAllWidgets(Context context, int toggleResId) {
		AppWidgetManager manager = AppWidgetManager.getInstance(context);
		ComponentName thisWidget = new ComponentName(context, BTWidget.class);
		int[] widgetIds = manager.getAppWidgetIds(thisWidget);

		for (int widgetId : widgetIds) {
			WidgetPrefs prefs = new WidgetPrefs();
			prefs.load(context, widgetId);

			Log.i(TAG, "Updating widgetId " + String.valueOf(widgetId));
			RemoteViews updateViews = buildUpdate(context, widgetId, prefs, toggleResId);
			manager.updateAppWidget(widgetId, updateViews);
		}
	}

This worked, but I was quite surprised to find out that manager.getAppWidgetIds(thisWidget) returned a really large list. It included not only the current widget id, but also Ids of widgets that were added to the home screen previously, and are long gone! For one of my widgets, this means 8 stale widget ids, for the other – 38.

Stale widget ids only appear when calling manager.getAppWidgetIds(thisWidget). The widget’s onUpdate method always has the correct widget ids – just the active ones.

So obvisouly I needed to do something about these stale widgets, since they were making my code very ineffecient.

The WIDGET_PRESENT_KEY setting value is used to determine if a given widgetId is really “live”. This value is set for a widget id when WidgetPrefs object is stored by the configuration activity, just before creating a new widget.

	public void store(Context context, int widgetId) {
		SharedPreferences prefs = context.getSharedPreferences(PREFS_KEY, Context.MODE_PRIVATE);
		if (prefs != null) {
			Editor edit = prefs.edit();

			if (edit != null) {
				String showLabelKey = SHOW_LABEL_KEY + String.valueOf(widgetId);
				edit.putBoolean(showLabelKey, mShowLabel);
				
				String widgetPresentKey = WIDGET_PRESENT_KEY + String.valueOf(widgetId);
				edit.putBoolean(widgetPresentKey, true);
				
				edit.commit();
			}
		}
	}

Then updateAllWidgets uses the boolean return value from WidgetPrefs.load to determine if there really is a widget with this id:

	public static void updateAllWidgets(Context context, int toggleResId) {
		AppWidgetManager manager = AppWidgetManager.getInstance(context);
		ComponentName thisWidget = new ComponentName(context, BTWidget.class);
		int[] widgetIds = manager.getAppWidgetIds(thisWidget);

		for (int widgetId : widgetIds) {
			WidgetPrefs prefs = new WidgetPrefs();
			if (prefs.load(context, widgetId)) {
				Log.i(TAG, "Updating widgetId " + String.valueOf(widgetId));
				RemoteViews updateViews = buildUpdate(context, widgetId, prefs, toggleResId);
				manager.updateAppWidget(widgetId, updateViews);
			}
		}
	}

If the user starts adding a widget, and cancels the configuration activity, the preferences for this widget are never stored (including the WIDGET_PRESENT_KEY), but the widgetId is saved by Android and will be included by manager.getAppWidgetIds(thisWidget). However, in updateAllWidgets, prefs.load returns false and this widget id is skipped.

When the user removes a widget from the home screen, we need to remove its preferences, including the WIDGET_PRESENT_KEY flag.

In the AppWidgetProvider subclass:

	@Override
	public void onDeleted(Context context, int[] appWidgetIds) {
		Log.i(TAG, "onDeleted, appWidgetIds.length = " + String.valueOf(appWidgetIds.length));

		for (int widgetId : appWidgetIds) {
			WidgetPrefs.delete(context, widgetId);
		}
	}

WidgetPrefs:

	public static void delete(Context context, int widgetId) {
		SharedPreferences prefs = context.getSharedPreferences(PREFS_KEY, Context.MODE_PRIVATE);
		if (prefs != null) {
			Editor edit = prefs.edit();

			if (edit != null) {
				String showLabelKey = SHOW_LABEL_KEY + String.valueOf(widgetId);
				edit.remove(showLabelKey);
				
				String widgetPresentKey = WIDGET_PRESENT_KEY + String.valueOf(widgetId);
				edit.remove(widgetPresentKey);
				edit.commit();
			}
		}
	}

This is pretty much it.

About these ads
Categories: android, ui
  1. February 26, 2014 at 12:17 am

    Hi Kostya, late comment, hoping it helps anyway.

    “Stale widget ids only appear when calling manager.getAppWidgetIds(thisWidget). The widget’s onUpdate method always has the correct widget ids – just the active ones.”

    Just added to onUpdate() in my appwidget:

    Log.d(TAG, “onUpdate reports this appWidgetIds : ” + Arrays.toString(appWidgetIds));

    The answer: [34]. And there’s no widget on my home screen…

    I saw this in a Genymotion emulator running 4.2.2

    • February 26, 2014 at 1:09 am

      Thanks, Jose. Well, the way I structure code in my widgets is to check the “present” preference value on either code path. It’s quite amazing that this issue has not been fixed — I remember first seeing it on a 2.1 device (Motorola Milestone).

      • josegd
        February 26, 2014 at 7:56 pm

        I have tested again with that instance of Genymotion and with my phone (Paranoid Android 4.3) and I’ve learned something interesting.

        With only the phone conected, I modified the source code and run the app from Eclipse, and got a string of appwidget IDs I didn’t know I had. Then I remembered that for testing reasons, I have 3 launchers installed on this phone: AOSP launcher, Nova, Google Experience Launcher. I put more log.d lines in some places and what I’ve found is this: onUpdate() executes with an array of ALL widget instances, despite the launcher where they live on.

        So, if we have 10 differents launchers, and onUpdate executes for some reason in one of them, the widget is updated for the other 9… I tried with the Genymotion emulator again hoping the ID 34 I saw was from another launcher, but on that emulator I only have AOSP. This was a real stale ID, but I can’t remember if I was playing with launchers on that emulator. :(

        Another thing I’ve learned: if we add a new instance, onUpdate has only one ID in the array: the newly created ID.

        So my (probably wrong) conclusions are:

        * onUpdate and getAppWidgetIds have the same set of IDs, but only when onUpdate is executed at boot or when the update period expires

        * onUpdate has only the newly created appWidgetId when a new instance is created

        * You can have stale IDs because of the not-sure-when-and-if-was-solved bug OR as a consquence of installing uninstalling different launchers.

        (This went long sorry! It should have been a blog post of my own :)

  2. Tomek Z.
    February 7, 2011 at 9:20 pm

    I handle this sick situation another way. My widgets on the homescreen are numbered. If there is a gap in numeration (non-existing widget), user can blacklist it manually. Then I manually remove blacklisted widgets from getAppWidgetIds() output.

  3. December 15, 2010 at 3:35 am

    Hello Kostya,
    This is one and the only solution for such a big issue i could find. I wonder if there s any update, alternatives to this.

    Thanks, Orkun

    • December 15, 2010 at 4:17 am

      Some code paths leading to stale widgets were fixed in 2.1 or 2.2. But since I support Android versions starting with 1.5 in my applications, I haven’t really tried to find out for sure. Please let me know if you discover anything more up to date.

  4. Nolan
    July 19, 2010 at 7:42 pm

    Thanks for the blog post! It was a big help to me, since I couldn’t find any reference to the problem of dead app widget ids anywhere else. I’m disappointed, though, that there’s no more elegant solution than just setting your own flag in the shared preferences.

  1. April 4, 2011 at 9:21 pm

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

Follow

Get every new post delivered to your Inbox.

Join 108 other followers