Thursday, 23 January 2014

Creating distinct PendingIntents

With my recent work on the BiPolar widget I came upon an interesting bug in my code.  One that made me scratch my head while walking through the code.

The widget is rather simple, it displays the current temperature in Celsius and Fahrenheit simultaneously and allows you to click on it if you want to share the current temperature with your favourite social media site. Everything seemed to be working fine until I tried installing multiple instances of the widget.  At the point, clicking on any widget would always share the weather of the last installed widget.  I figured that I had a logic problem in my code and poured over it repeatedly.  Nothing.

Here is the original erroneous code :

// Create the Intent that will be sent when the widget is clicked on
Intent intent = new Intent(this, BiPolar.class);
intent.putExtra(CLICK_WIDGET_ID, appWidgetId);

// Create the PendingIntent for the RemoteViews
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, intent, 0);

//attach an on-click listener to the widget
views.setOnClickPendingIntent(, pendingIntent);

That code is inside of a loop that goes through all of the appWidgetIds and appears correct.  The problem lies in the fact that PendingIntent are reused by the system.  If the PendingIntent that would be returned by getBroadcast() or getActivity() is considered equal to one that already exists then a new one is not created, rather a copy of an existing one is returned. The problem lies in that Extras are not considered in the equality comparison. So in this case every call for a new PendingIntent returned the same PendingIntent over and over again.

From the documentation : 

A PendingIntent itself is simply a reference to a token maintained by the system describing the original data used to retrieve it. This means that, even if its owning application's process is killed, the PendingIntent itself will remain usable from other processes that have been given it. If the creating application later re-retrieves the same kind of PendingIntent (same operation, same Intent action, data, categories, and components, and same flags), it will receive a PendingIntent representing the same token if that is still valid, and can thus call cancel() to remove it.
Because of this behavior, it is important to know when two Intents are considered to be the same for purposes of retrieving a PendingIntent. A common mistake people make is to create multiple PendingIntent objects with Intents that only vary in their "extra" contents, expecting to get a different PendingIntent each time. This does not happen. The parts of the Intent that are used for matching are the same ones defined by Intent.filterEquals. If you use two Intent objects that are equivalent as per Intent.filterEquals, then you will get the same PendingIntent for both of them.

The Solution

So how to we make sure that the requested PendingIntent is different from the existing ones?  The solution is rather easy.  Again from the documentation : 

you will need to ensure there is something that is different about them to associate them with different PendingIntents. This may be any of the Intent attributes considered by Intent.filterEquals, or different request code integers supplied to getActivity(Context, int, Intent, int)getActivities(Context, int, Intent[], int)getBroadcast(Context, int, Intent, int), or getService(Context, int, Intent, int).
In my case the fix was very easy, just add the appWidgetId as the request code to the parameters of getBroadcast.
// Create the PendingIntent for the RemoteViews
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, appWidgetId, intent, FLAG_UPDATE_CURRENT);

Although that field is not yet used, and may never be, it is enough for the system to distinguish the PendingIntent as being different from the other and to create a new one.