Pending Intents, widget and notification

2010-03-15
Igor
java android

Recently, I developed Android application for easier parking in my hometown ('Parkiraj!'). Among other features, application has few notification alerts (when park-time is about to pass), as well as widget on main screen that shows parking elapsed time. Moreover, when user taps on widget, main activity should opens. And here I start to have problems.

Before I proceed, let me emphasize that maybe I am doing something in a wrong way, since I am relatively new to Android. If so, please forgive me for being noob.

Notifications

First, here is how I create notifications, its pretty straight:

public class Notificator {

	public static final int NOTIFICATION_ID = 173;

	private final Context context;
	private final NotificationManager notificationManager;
	private final String appName;

	public Notificator(Context context) {
		this.context = context;
		appName = context.getResources().getString(R.string.app_name);
		this.notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
	}


	public void notifyAlert(String text, boolean loud) {
		PendingIntent intent =  PendingIntent.getActivity(context, 0, new Intent(context, ParkActivity.class), FLAG_UPDATE_CURRENT | FLAG_ACTIVITY_NEW_TASK);
		notify(intent, R.drawable.notify_alert, appName, text, loud);
	}
	public void notifyWarning(String text) {
		PendingIntent intent =  PendingIntent.getActivity(context, 0, new Intent(context, ParkActivity.class), FLAG_UPDATE_CURRENT | FLAG_ACTIVITY_NEW_TASK);
		notify(intent, R.drawable.notify_warn, appName, text, false);
	}


	private void notify(PendingIntent intent, int iconId, String title, String text, boolean loud) {
		Notification notification = new Notification(iconId, title, System.currentTimeMillis());
		notification.setLatestEventInfo(context, title, text, intent);

		// sound
		notification.defaults |= Notification.DEFAULT_SOUND;

		// vibrator: pause-vibrating sequence
		if (loud == false) {
			notification.vibrate = new long[] {100, 300, 100, 300, 100, 600};
		}

		// lights
		notification.flags |= Notification.FLAG_SHOW_LIGHTS;
		notification.ledARGB = Color.GREEN;
		notification.ledOnMS = 200;
		notification.ledOffMS = 50;

		if (loud) {
			notification.flags |= Notification.FLAG_INSISTENT;	// repeat & repeat sound and vibration
		}

		notificationManager.notify(NOTIFICATION_ID, notification);
	}

	public void cancel() {
		notificationManager.cancel(NOTIFICATION_ID);
	}
}

From above we can see that pending event is created for main activity: ParkActivity. Therefore, when user clicks on notification, main activity will appear. So far, so good.

Widget

Now, here is the widget provider:

public class Widget extends AppWidgetProvider {

	@Override
	public void onEnabled(Context context) {
		App.t();
		Intent intent = createClickIntent();
		RemoteViews remoteViews = bindOnClick(context, intent);

		ComponentName comp = new ComponentName(context.getPackageName(), Widget.class.getName());
		AppWidgetManager.getInstance(context).updateAppWidget(comp, remoteViews);
	}

	@Override
	public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
		App.t();
		Intent intent = createClickIntent();
		RemoteViews remoteViews = bindOnClick(context, intent);
		appWidgetManager.updateAppWidget(appWidgetIds, remoteViews);
	}

	private static Intent createClickIntent() {
		Intent i = new Intent();
		i.setClassName(App.PACKAGE_NAME, ParkActivity.class.getName());
		i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
		return i;
	}

	private RemoteViews bindOnClick(Context context, Intent intent) {
		RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.widget);
		PendingIntent pendingIntent =  PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT | Intent.FLAG_ACTIVITY_NEW_TASK);
		remoteViews.setOnClickPendingIntent(R.id.widgetView, pendingIntent);
		return remoteViews;
	}
}

Here last two methods are the major ones. I've tried many different scenarios for createClickIntent(); at the end I've stick with the above. Here I create pending intent that will invoke the same main activity, ParkActivity.

The problem

At first sight, everything works fine: when user click on the widget, main activity appears. But when notification times pass and notification is fired, click on widget stops working! It looks like notification somehow 'cancels' the pending intents that invokes the same main activity, no matter if they are created after the notification occurs! So, once when notification appears, when user clicks on widget the following exception occurs:

ERROR/AndroidRuntime(245): android.widget.RemoteViews$ActionException: android.app.PendingIntent$CanceledException
at android.widget.RemoteViews$SetOnClickPendingIntent$1.onClick(RemoteViews.java:153)

I was not able to solve this by modifying pending intent flags, or using different approaches. I must say it again; I am probably missing something; but no matter how much I've tried, I was not able to find a solution.

Moreover, what is more weird is that I have two notification messages (warn and alert, see the first code) posted on different times; and the click on second notification actually works! I would expected it doesn't work, since click on widget stops working.

The workaround

The only thing that helped is forwarding: instead of invoking the main activity (when widget is tapped), I invoked a dummy activity: ParkFwdActivity, that onCreate() simply starts the main activity:

public class ParkFwdActivity extends Activity {
	@Override
	public void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.fwd);
		App.t();
		Intent intent = new Intent(this, ParkActivity.class);
		startActivity(intent);
		finish();
	}
}

At first, this was activity with no content; however I've noticed that sometimes an empty layout appears before main activity is shown. Therefore, I am using fwd layout that is just a simple empty dialog with an image - up to now, it never showed.

Obviously, the Widget#createClickIntent() has to be changed to use new activity:

i.setClassName(App.PACKAGE_NAME, ParkFwdActivity.class.getName());

Contents

Read about...

...loading...