2011-04-26

A Simple, Complete App Widget - Part 1

Note: This blog post is incomplete, and I discontinued posting part 2, as the Widget API and abilities were changing significantly with new releases of Android, at the time of writing. I left this post online, as it has many views, and some useful content.

While information on developing App Widgets for the Android home screen is plentiful, much of what's available - including the contents of many books on Android software development - is incomplete, often broken, and leads to improperly functioning App Widgets. One all too common outcome is App Widgets that unnecessarily run users' batteries down. Another frequent fail includes funky App Widget behavior if more than one instance exists on a user's home screen. This post is intended to provide a complete blueprint for folks to follow, to develop their own App Widgets that display dynamic content while consuming minimal device resources, provide standard user input controls, and otherwise work correctly as expected.
Link To This ArticleQR Code Link To This Articlehttp://goo.gl/RN1ga

Overview Of The App To Be Created


Following are two cropped screenshots of the application to be created: one of the app widget, and the other of the activity called when the user clicks the "Save Code" button on the app widget.





The App Widget displays a random passcode that changes when the user clicks "New Code" and at the start of the second hour after the last update. The "Save Code" button on the app widget launches the Save Activity, passing it the current passcode.

Code Components


(Download the complete source code for this example.)

The application code consists of the following major parts.

1. The Random Passcode App Widget Provider and Service, with configuration and layout xml - Responsible for the App Widget display, updates, event handling, and lifecycle.

2. The Random Passcode Save Activity, with layout xml - Receives a passcode from an App Widget, and displays it on screen.

3. The Android Manifest - Ties all the application pieces together.

(Note: The example code lines are broken at 64 characters for display in Blogger. So, some of the code formatting is funky.)

The App Widget Provider (BroadcastReceiver) and Service


RandomPasscodeAppWidgetProvider
/**
* Handles intents for Random Passcode App Widget updates and
* other lifecycle events. Delegates app widget updates to a
* RandomPasscodeAppWidgetService.
*/
public class RandomPasscodeAppWidgetProvider
extends AppWidgetProvider
{
/**
* {@inheritDoc}
*/
@Override
public void onUpdate(Context context, AppWidgetManager
appWidgetManager, int[] appWidgetIds)
{
int appWidgetId = INVALID_APPWIDGET_ID;
// Jeff Sharkey's Sky app portends a null check is
// necessary, though the API documentation provides no
// indication that null is possible.
if (appWidgetIds != null)
{
int N = appWidgetIds.length;
if (N == 1)
{
appWidgetId = appWidgetIds[0];
}
}

Intent intent = new Intent(context,
RandomPasscodeAppWidgetService.class);
intent.putExtra(EXTRA_APPWIDGET_ID, appWidgetId);
context.startService(intent);
}

...
This calls a Service to handle the initial layout and subsequent updates of App Widgets, instead of directly performing that work in onUpdate. This allows the App Widget Provider to finish quickly, as it should, while any potentially long-running tasks are performed in the background service.

Putting the appWidgetId in the Intent extra is a simple way for the RandomPasscodeAppWidgetService to determine whether just one App Widget instance is to be updated, or all of the App Widget instances are to be updated. It's a common mistake of other App Widget example applications to not consider that an update request may be for just one App Widget instance, and instead they simply update all existing App Widgets.

RandomPasscodeAppWidgetProvider Continued
  ...

/**
* {@inheritDoc}
*/
@Override
public void onReceive(Context context, Intent intent)
{
// Special app widget delete handling added for bug in
// Android 1.5, as described at
// http://groups.google.com/group/android-developers/
// browse_thread/thread/365d1ed3aac30916?pli=1
String sdk = android.os.Build.VERSION.SDK;
String release = android.os.Build.VERSION.RELEASE;
String action = intent.getAction();
if ((sdk.equals("3") || release.equals("1.5")) &&
ACTION_APPWIDGET_DELETED.equals(action))
{
int appWidgetId = intent.getIntExtra(EXTRA_APPWIDGET_ID,
INVALID_APPWIDGET_ID);
if (appWidgetId != INVALID_APPWIDGET_ID)
{
this.onDeleted(context, new int[] { appWidgetId });
}
}
else
{
super.onReceive(context, intent);
}
}
}
Be warned, Android has bugs. This code includes a workaround for a failure of Android 1.5 to properly remove deleted App Widget instances. Without this workaround, subsequent App Widget updates will continue to include the deleted App Widget ID in the list of App Widgets to update. Review the post at blog.elsdoerfer.name on App Widget problems for other things to be wary of.

RandomPasscodeAppWidgetService
/**
* A work queue processor that handles asynchronous Random
* Passcode App Widget update requests.
*
* As a "good citizen", using minimal battery to complete
* tasks, this service is started as a needed, and stops
* itself when it runs out of work.
*/
public class RandomPasscodeAppWidgetService
extends IntentService
{
...

/**
* {@inheritDoc}
*/
@Override
protected void onHandleIntent(Intent intent)
{
AppWidgetManager appWidgetManager =
AppWidgetManager.getInstance(this);

int incomingAppWidgetId = intent.getIntExtra(
EXTRA_APPWIDGET_ID, INVALID_APPWIDGET_ID);
if (incomingAppWidgetId != INVALID_APPWIDGET_ID)
{
updateOneAppWidget(appWidgetManager,
incomingAppWidgetId);
}
else
{
updateAllAppWidgets(appWidgetManager);
}

scheduleNextUpdate();
}

/**
* Schedules the next App Widget update to occur at the
* start of the fourth hour after the current time. Any
* previously scheduled App Widget update is effectively
* canceled and replaced by the newly scheduled update.
*
* The scheduled update does not wake the device up. If
* the update is scheduled to start while the device is
* asleep, it will not run until the next time the device
* is awake.
*/
private void scheduleNextUpdate()
{
Intent changePasscodeIntent =
new Intent(this, this.getClass());
// A content URI for this Intent may be unnecessary.
changePasscodeIntent.setData(Uri.parse("content://" +
PACKAGE_NAME + "/change_passcode"));
PendingIntent changePasscodePendingIntent =
PendingIntent.getService(this, 0, changePasscodeIntent,
PendingIntent.FLAG_UPDATE_CURRENT);

// The update frequency should be user configurable.
Time time = new Time();
time.set(System.currentTimeMillis() + 4 *
DateUtils.HOUR_IN_MILLIS);
time.minute = 0;
time.second = 0;
long nextUpdate = time.toMillis(false);

AlarmManager alarmManager =
(AlarmManager) getSystemService(Context.ALARM_SERVICE);
alarmManager.set(AlarmManager.RTC, nextUpdate,
changePasscodePendingIntent);
}

...

  ...

/**
* For each random passcode app widget on the user's home
* screen, updates its display with a new passcode, and
* registers click handling for its buttons.
*/
private void updateAllAppWidgets(AppWidgetManager
appWidgetManager)
{
ComponentName appWidgetProvider = new ComponentName(this,
RandomPasscodeAppWidgetProvider.class);
int[] appWidgetIds =
appWidgetManager.getAppWidgetIds(appWidgetProvider);
int N = appWidgetIds.length;
for (int i = 0; i < N; i++)
{
int appWidgetId = appWidgetIds[i];
updateOneAppWidget(appWidgetManager, appWidgetId);
}
}

/**
* For the random passcode app widget with the provided ID,
* updates its display with a new passcode, and registers
* click handling for its buttons.
*/
private void updateOneAppWidget(AppWidgetManager
appWidgetManager, int appWidgetId)
{
String newRandomPasscode = generateRandomPasscode();
RemoteViews views = new RemoteViews(PACKAGE_NAME,
R.layout.app_widget_layout);
views.setTextViewText(R.id.passcode_view,
newRandomPasscode);

setSavePasscodeIntent(views, appWidgetId,
newRandomPasscode);
setChangePasscodeIntent(views, appWidgetId);

appWidgetManager.updateAppWidget(appWidgetId, views);
}

...

  ...

/**
* Configures "Save Code" button clicks to pass the current
* passcode of the parent app widget to the save passcode
* Activity.
*/
private void setSavePasscodeIntent(RemoteViews views,
int appWidgetId, String newRandomPasscode)
{
Intent savePasscodeIntent =
new Intent(this, SaveRandomPasscodeActivity.class);
savePasscodeIntent.setData(Uri.parse("content://" +
PACKAGE_NAME + "/save_passcode/widget_id/" +
appWidgetId));
savePasscodeIntent.putExtra("PASSCODE", newRandomPasscode);
PendingIntent savePasscodePendingIntent =
PendingIntent.getActivity(this, 0, savePasscodeIntent,
PendingIntent.FLAG_UPDATE_CURRENT);
views.setOnClickPendingIntent(R.id.save_passcode_button,
savePasscodePendingIntent);
}

/**
* Configures "New Code" button clicks to generate and set a
* new passcode on the parent app widget.
*/
private void setChangePasscodeIntent(RemoteViews views,
int appWidgetId)
{
Intent changePasscodeIntent =
new Intent(this, this.getClass());
changePasscodeIntent.setData(Uri.parse("content://" +
PACKAGE_NAME + "/change_passcode/widget_id/" +
appWidgetId));
changePasscodeIntent.putExtra(EXTRA_APPWIDGET_ID,
appWidgetId);
PendingIntent changePasscodePendingIntent =
PendingIntent.getService(this, 0, changePasscodeIntent,
PendingIntent.FLAG_UPDATE_CURRENT);
views.setOnClickPendingIntent(R.id.new_passcode_button,
changePasscodePendingIntent);
}

...
Note the calls to Intent.setData(Uri) with a content URI String unique for each App Widget instance. Explanations for exactly what happens if this value were not set on the Intent differ, and I haven't yet dug through the source code to see which one is correct. At any rate, when one of the buttons on the App Widget were clicked, without these unique Uris, the same Intent would be passed to the Activity or Service, without proper consideration for which App Widget originated the action. To clarify, it would be possible that when a button from App Widget instance "B" were clicked, a value from App Widget instance "A" would be included in the Intent passed to the Activity or Process. If this problem still isn't clear, just remove the lines of code that set the data Uris, create multiple instances of the App Widget on the home screen, click on them, and see what happens.

RandomPasscodeAppWidgetService Continued
  ...

/**
* Generates a string, eight characters in length, comprised
* of ASCII characters randomly selected from the range of
* character 33, '!', to character 126, '~'.
*
* Security Note: The random passcode generator of this
* service is a toy implementation that uses java.util.Random
* for random values. A random passcode generator for real
* world use should use a cryptographically secure
* pseudorandom number generator, or a hardware random number
* generator, both of which java.util.Random does not do. For
* more information, see
* http://en.wikipedia.org/wiki/
* Cryptographically_secure_pseudorandom_number_generator
*/
private static String generateRandomPasscode()
{
Random random = new Random();
int targetLength = 8;
char[] passcode = new char[targetLength];
for (int i = 0; i < targetLength; i++)
{
passcode[i] = (char) (random.nextInt(94) + 33);
}
return new String(passcode);
}
}


layout/app_widget_layout.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:padding="10dip"
android:gravity="center"
android:background="#88888888"
android:weightSum="3">
<TextView
android:id="@+id/passcode_view"
android:layout_width="0dip"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Loading..."
android:gravity="center"
android:background="@android:color/black"
android:textColor="@android:color/white" />
<Button
android:id="@+id/new_passcode_button"
android:layout_width="0dip"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="New Code" />
<Button
android:id="@+id/save_passcode_button"
android:layout_width="0dip"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Save Code" />
</LinearLayout>


xml/initial_app_widget_provider_config.xml
<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider
xmlns:android="http://schemas.android.com/apk/res/android"
android:minWidth="290dip"
android:minHeight="72dip"
android:updatePeriodMillis="0"
android:initialLayout="@layout/app_widget_layout" />



The Random Passcode Save Activity

/**
* Receives passcode save requests from Random Passcode App
* Widgets, and displays the passcode to the user.
*
* Note: This example activity doesn't actually save anything.
* It just displays an incoming passcode.
*/
public class SaveRandomPasscodeActivity extends Activity
{
/**
* {@inheritDoc}
*/
@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.main);

Intent intent = getIntent();
String passcode = intent.getStringExtra("PASSCODE");
String message =
passcode == null ?
"Select a passcode to save from an app widget " +
"on the home screen."
: "Passcode " + passcode + " saved.";
TextView tv =
(TextView) findViewById(R.id.passcode_saved_view);
tv.setText(message);
}
}


layout/main.mxl
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<TextView
android:id="@+id/passcode_saved_view"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="Loading..."
android:textSize="24dip" />
</LinearLayout>



The Android Manifest

<?xml version="1.0" encoding="utf-8"?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package=
"com.androidprogrammingexamples.simplecompleteappwidget"
android:versionCode="1"
android:versionName="1.0">
<uses-sdk
android:minSdkVersion="3" />

<application
android:icon="@drawable/icon"
android:label="@string/app_name">
<activity
android:name=".SaveRandomPasscodeActivity"
android:label="@string/app_name">
<intent-filter>
<action
android:name="android.intent.action.MAIN" />
<category
android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<receiver
android:name=".RandomPasscodeAppWidgetProvider"
android:label="Random Passcode">
<intent-filter>
<action
android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/initial_app_widget_provider_config" />
</receiver>
<service
android:name=".RandomPasscodeAppWidgetService" />
</application>
</manifest>
The configurations in the Android Manifest are typical for an Activity and an App Widget. Note, the service of the application is also registered here.

Other Implementation Notes


In this implementation, button clicks launch PendingIntents that directly start an Activity or directly start a Service. A different approach many Android applications take is to instead configure PendingIntents that broadcast an Intent, for any interested and properly configured BroadcastReceivers to receive and handle. This broadcasting approach provides decoupling between the broadcaster and receiver, since the broadcaster doesn't need any knowledge of the receiver, allowing for a "plug-in architecture", where other receivers can register to handle the broadcasts. For example, someone could write a receiver to respond to every passcode save broadcast, to email the passcode to our GMail account. (This is not a suggestion for a good idea - it's just an example.)

The example implementation of this blog post was designed to demonstrate that it's not necessary to use the broadcasting approach with App Widget event handling (though the App Widget "Provider" is a BroadcastReceiver responding to APPWIDGET_UPDATE broadcasts).

For a decent example of an App Widget with event broadcasting and receiving, see the "Home screen tips" widget, which is part of stock Android builds. (Note, the "Home screen tips" widget does not handle events per specific App Widget instance, instead applying events and display state to all instances the same, such that no two instances will display different messages at the same time.)

References And How I Arrived At This Implementation


The first time I wanted to create an App Widget, I figured I'd be able to just follow a few of the many examples available on the Internet, in the official Android developer guide, in the various Android programming books, and in the Android platform code base. That didn't work out very well. I think every example I tested included at least one bug (like failing to accommodate multiple instances, and leaving services running, though they weren't in use), and/or otherwise didn't provide for the user interactions I wanted. Luckily, the Android platform is open source, and one is able to see exactly what happens behind the scenes.

Recognizing that much of the App Widget information in the following resources is incomplete for high quality, real world applications, they still provide alternative examples worth reviewing.

Books

Other Examples

Additional Resources
FINI

2 comments:

  1. Thanks for this example. You were right about the other ones out there being less than perfect. Yours let me fix in 20 minutes what I'd been banging my head against the wall for three days.

    ReplyDelete
  2. Thank you very much! This is really a complete widget tutorial without to hide critical points, saved me lot of time!

    ReplyDelete