Actions and App Widgets
App widgets can do more than render a bit of UI on the home screen. They can also respond to click events, either to update the contents of the app widget or to take some other action, such as displaying an activity.
However, the app widget UI is not part of the app’s process. We do not have access to the actual UI objects, so we cannot call setOnClickListener() on them. Instead, we have go through a bit of indirection, in the form of PendingIntent objects.
Introducing PendingIntent
We have seen Intent objects before. We use them to identify activities to start. As it turns out, we can also use Intent objects with two other types of components:
- We can start a service, where the
Intentidentifies the service to be started - We can send a broadcast, where the
Intentis the broadcast, identifying who should receive the message
A PendingIntent is a wrapper around an Intent that, among other things, indicates what action should be used with that Intent, such as starting an activity or sending a broadcast. There is a lot more to a PendingIntent than that, particularly with respect to security. But, for the purposes of this chapter, considering it to be a combination of an Intent and a statement of what to do with the Intent will suffice.
A PendingIntent is a way of passing an action to be performed to some other app or to the OS. In the case of app widgets, we can register click “listeners” that, instead of invoking some listener object or lambda expression in our app, invokes a PendingIntent. So, we can tie clicks in our app widget to PendingIntent objects, which in turn tell Android to start an activity, send a broadcast, etc.
Refreshing the Color
With that in mind, let’s look at two actions that we want to perform from our app widget.
First, there is the refresh button on the one side of the app widget. When the user taps that button, we want to generate a new random color and update the UI of the app widget to show that color.
Since we want to update the app widget, one approach is to route control back to ColorWidget, since we already have the logic there to update the app widget UI.
Creating the PendingIntent
In updateWidget(), as part of configuring the RemoteViews, we create an Intent, identifying our ColorWidget:
Intent refreshIntent = new Intent(context, ColorWidget.class)
.setAction(ACTION_REFRESH)
.putExtra(EXTRA_APP_WIDGET_ID, appWidgetId);
val refreshIntent = Intent(context, ColorWidget::class.java)
.setAction(ACTION_REFRESH)
.putExtra(EXTRA_APP_WIDGET_ID, appWidgetId)
We created Intent objects for our own components before, when starting activities. We use the same Intent constructor here as then, to supply our ColorWidget class and indicate that is what should be started. We also do two other things as part of configuring the Intent:
- We call
setAction(), to associate a custom action string with ourIntent. We can use this to distinguish cases where theColorWidgetis invoked based on our ownIntentfrom cases where it is invoked for other reasons. - We call
putExtra()to attach the app widget ID to theIntent. This way, we know which app widget is the one that the user tapped the refresh button on.
Then, we wrap that Intent in a PendingIntent:
PendingIntent refreshPI = PendingIntent.getBroadcast(
context,
appWidgetId,
refreshIntent,
PendingIntent.FLAG_UPDATE_CURRENT
);
val refreshPI = PendingIntent.getBroadcast(
context,
appWidgetId,
refreshIntent,
PendingIntent.FLAG_UPDATE_CURRENT
)
The function on PendingIntent that we call is getBroadcast(), saying that we want the system to send a broadcast using our supplied Intent. We also provide:
- a
Context - a unique ID for this
PendingIntent(in this case, we use the app widget ID) - the
Intentto be wrapped in thePendingIntent - a flag, the details of which are well beyond the scope of this book
Connecting the PendingIntent
Given the PendingIntent, attaching it to a click listener is a matter of calling setOnClickPendingIntent() on the RemoteViews:
remoteViews.setOnClickPendingIntent(R.id.refresh, refreshPI);
remoteViews.setOnClickPendingIntent(R.id.refresh, refreshPI)
setOnClickPendingIntent() takes the ID of the widget in the layout and the PendingIntent. If the user clicks that widget, Android will invoke the PendingIntent and will send our broadcast.
Reacting to the PendingIntent
Our broadcast is going to our ColorWidget as a broadcast. The entry point for a BroadcastReceiver is an onReceive() function. So, ColorWidget has its own onReceive() function to process our broadcast:
@Override
public void onReceive(Context context, Intent intent) {
if (intent.getAction().equals(ACTION_REFRESH) &&
intent.hasExtra(EXTRA_APP_WIDGET_ID)) {
AppWidgetManager appWidgetManager =
context.getSystemService(AppWidgetManager.class);
updateWidget(
context,
appWidgetManager,
intent.getIntExtra(EXTRA_APP_WIDGET_ID, -1)
);
}
else {
super.onReceive(context, intent);
}
}
override fun onReceive(context: Context, intent: Intent) {
if (intent.action == ACTION_REFRESH && intent.hasExtra(EXTRA_APP_WIDGET_ID)) {
val appWidgetManager =
context.getSystemService(AppWidgetManager::class.java)
updateWidget(
context,
appWidgetManager,
intent.getIntExtra(EXTRA_APP_WIDGET_ID, -1)
)
} else {
super.onReceive(context, intent)
}
}
onReceive() gets a copy of the Intent that was broadcast. The first thing that we do is to see if this is our refresh broadcast, by:
- Confirming that the action string is our custom refresh action, and
- Confirming that we have our app widget ID extra
If we do, we call updateWidget() from here, to update that app widget based on its ID. If we do not, we chain to the superclass implementation of onReceive(). In this case, the superclass is AppWidgetProvider, and it handles other broadcasts… such as the broadcast that eventually triggers the call to onUpdate().
Displaying the BigSwatchFragment
The other action that we want to perform will be triggered by a click anywhere else on the app widget besides our refresh button. That should bring up our screen to show our color, the same as if the user tapped on a row in our list of colors.
In this case, we want to navigate to BigSwatchFragment. We are using Jetpack’s Navigation framework for handling the navigation between fragments. One side benefit of that is we can use NavDeepLinkBuilder to create a PendingIntent for our use:
Bundle args = new Bundle();
args.putInt("color", color);
PendingIntent deepLinkPI = new NavDeepLinkBuilder(context)
.setGraph(R.navigation.nav_graph)
.setDestination(R.id.bigSwatchFragment)
.setArguments(args)
.createPendingIntent();
remoteViews.setOnClickPendingIntent(R.id.root, deepLinkPI);
val deepLinkPI = NavDeepLinkBuilder(context)
.setGraph(R.navigation.nav_graph)
.setDestination(R.id.bigSwatchFragment)
.setArguments(bundleOf("color" to color))
.createPendingIntent()
remoteViews.setOnClickPendingIntent(R.id.root, deepLinkPI)
NavDeepLinkBuilder takes:
- The ID of the navigation graph resource that we are using
- The ID of the destination that we are trying to navigate to
- A
Bundleof arguments to pass to that destination (in this case, packaging up our color)
Then, a simple call to createPendingIntent() will create a PendingIntent that will start our activity and go to our desired destination. We can attach that PendingIntent to the RemoteViews via setOnClickPendingIntent(), and the rest is taken care of for us by the Navigation framework.
Prev Table of Contents Next
This book is licensed under the Creative Commons Attribution-ShareAlike 4.0 International license.