Take your time – Widgets and AlarmManager

5 June 2012

In our previous example, our widget is updated through two events: user’s tap and the updatePeriodMillis of the provider info file.

Using the xml file has two main disadvantages:

Users cannot select the update interval. The minimum interval time is 30 minutes.

To overcome these problems, just use an AlarmManager to fire the updates.



An AlarmManager, in a few words, is a Service that schedules Intents. The schedules can be repeated.

There are three things to keep in mind when using an AlarmManager:

We don’t directly create one calling its constructor but we retrieve one calling Activity.getSystemService(ALARM_SERVICE) and casting the result to an AlarmManager. Cancel an AlarmManager when we don’t need it anymore. Use the methods of the AppWidgetProvider that are called at each steps of the widget’s lifecycle. Also, a good point to create an AlarmManger, is the configuration activity, if you have one of course. When we want to cancel an AlarmManager, we have to pass an Intent that match the one we created at the beginning. In this case two intents match if they share the same action and the same uri (we will need the exact instance of the Uri class). The extras are not considered.

Also, as a personal note, when dealing with widgets, I suggest to use an explicit intent and an action different than the one we used to update the widget the first time, in the configuration activity. This way we will have a more reliable AlarmManager.

So here are some code snippets to use an AlarmManager to update our widgets:

MaLuBuTestWidgetActivity.java

[...] public void mainOk(View source) { [...] Log.d("Ok Button", "First onUpdate broadcast sent"); //Create and launch the AlarmManager. //N.B.: //Use a different action than the first update to have more reliable results. //Use explicit intents to have more reliable results. Uri.Builder build = new Uri.Builder(); build.appendPath(""+widgetID); Uri uri = build.build(); Intent intentUpdate = new Intent(context, MaLuBuTestWidgetProvider.class); intentUpdate.setAction(MaLuBuTestWidgetProvider.UPDATE_ONE);//Set an action anyway to filter it in onReceive() intentUpdate.setData(uri);//One Alarm per instance. //We will need the exact instance to identify the intent. MaLuBuTestWidgetProvider.addUri(widgetID, uri); intentUpdate.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, widgetID); PendingIntent pendingIntentAlarm = PendingIntent.getBroadcast(MaLuBuTestWidgetActivity.this, 0, intentUpdate, PendingIntent.FLAG_UPDATE_CURRENT); //If you want one global AlarmManager for all instances, put this alarmManger as //static and create it only the first time. //Then pass in the Intent all the ids and do not put the Uri. AlarmManager alarmManager = (AlarmManager)getSystemService(ALARM_SERVICE); alarmManager.setRepeating(AlarmManager.RTC_WAKEUP, System.currentTimeMillis()+(seconds*1000), (seconds*1000), pendingIntentAlarm); [...] } [...]

MaLuBuTestWidgetProvider.java

[...] /** * We need the exact Uri instance to identify the Intent. */ private static HashMap<Integer, Uri> uris = new HashMap<Integer, Uri>(); [...] /** * Each time an instance is removed, we cancel the associated AlarmManager. */ @Override public void onDeleted(Context context, int[] appWidgetIds) { super.onDeleted(context, appWidgetIds); for (int appWidgetId : appWidgetIds) { cancelAlarmManager(context, appWidgetId); } } protected void cancelAlarmManager(Context context, int widgetID) { AlarmManager alarm = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); Intent intentUpdate = new Intent(context, MaLuBuTestWidgetProvider.class); //AlarmManager are identified with Intent's Action and Uri. intentUpdate.setAction(MaLuBuTestWidgetProvider.UPDATE_ONE); //For a global AlarmManager, don't put the uri to cancel //all the AlarmManager with action UPDATE_ONE. intentUpdate.setData(uris.get(widgetID)); intentUpdate.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, widgetID); PendingIntent pendingIntentAlarm = PendingIntent.getBroadcast(context, 0, intentUpdate, PendingIntent.FLAG_UPDATE_CURRENT); alarm.cancel(pendingIntentAlarm); Log.d("cancelAlarmManager", "Cancelled Alarm. Action = " + MaLuBuTestWidgetProvider.UPDATE_ONE + " URI = " + uris.get(widgetID)); uris.remove(widgetID); } public static void addUri(int id, Uri uri) { uris.put(new Integer(id), uri); } @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); Log.d("onReceive", "action: " + action); if(action.equals(AppWidgetManager.ACTION_APPWIDGET_UPDATE) || action.equals(UPDATE_ONE)) { //Check if there is a single widget ID. int widgetID = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID); //If there is no single ID, call the super implementation. if(widgetID == AppWidgetManager.INVALID_APPWIDGET_ID) super.onReceive(context, intent); //Otherwise call our onUpdate() passing a one element array, with the retrieved ID. else this.onUpdate(context, AppWidgetManager.getInstance(context), new int[]{widgetID}); } else super.onReceive(context, intent); } [...]

As you can see, we created one AlarmManager for each instance of the widget. Each AlarmManager has, potentially, a different update interval.

This is too much for most of the use cases and normally you just want one global AlarmManager for all of your instances.

In that case, just:

Put the AlarmManager in a static variable (and initialize it only if is null). The AlarmManager should be initialized in onEnable() of the AppWidgetProvider . That method is called after the creation of the first widget instance. In your AppWidgetProvider , cancel the AlarmManager in onDisable() (that is called after the last instance is deleted).

The code

Here is the sourcecode of the example, the multi-AlarmManager version.

I will post only the files that are changed from the first post.

widget_provider.xml

<?xml version="1.0" encoding="utf-8"?> <!-- 4 x 1 cells --> <appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android" android:minHeight="60dp" android:minWidth="290dp" android:configure="com.wordpress.malubu.MaLuBuTestWidgetActivity"> </appwidget-provider>

main.xml

<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent" android:orientation="vertical" > <LinearLayout android:id="@+id/conf_group" android:layout_width="fill_parent" android:layout_height="wrap_content" android:orientation="vertical" android:padding="3dp"> <!-- In seconds. From 1 sec to 60 sec = (from 0 sec to 59 sec) + 1 sec. --> <SeekBar android:id="@+id/conf_seek" android:layout_width="fill_parent" android:layout_height="wrap_content" android:max="59" android:progress="0"/> <TextView android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="@string/warning_time" android:padding="3dp"/> <LinearLayout android:id="@+id/color_button_group" android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="horizontal" android:layout_gravity="center" android:padding="3dp"> <Button android:id="@+id/btn_white" android:layout_width="fill_parent" android:layout_height="fill_parent" android:background="@android:color/white" android:onClick="mainColor"/> <Button android:id="@+id/btn_red" android:layout_width="fill_parent" android:layout_height="fill_parent" android:background="@android:color/holo_red_light" android:onClick="mainColor"/> <Button android:id="@+id/btn_blue" android:layout_width="fill_parent" android:layout_height="fill_parent" android:background="@android:color/holo_blue_light" android:onClick="mainColor"/> <Button android:id="@+id/btn_green" android:layout_width="fill_parent" android:layout_height="fill_parent" android:background="@android:color/holo_green_light" android:onClick="mainColor"/> </LinearLayout> <TextView android:id="@+id/main_result" android:text="@string/progress_start" android:layout_width="fill_parent" android:layout_height="wrap_content" android:textColor="@android:color/black" android:background="@android:color/white" android:textStyle="bold" android:gravity="center" style="@android:style/TextAppearance.Large" android:padding="3dp"/> </LinearLayout> <Button android:id="@+id/main_ok" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_below="@+id/conf_group" android:layout_alignRight="@+id/conf_group" android:layout_margin="0dp" android:padding="0dp" android:textStyle="bold" android:text="@android:string/ok" style="@android:style/TextAppearance.Large" android:onClick="mainOk"/> </RelativeLayout>

MaLuBuTestWidgetProvider.java

public class MaLuBuTestWidgetProvider extends AppWidgetProvider { public static final String EXTRA_COLOR_VALUE = "com.malubu.wordpress.EXTRA_COLOR_VALUE"; public static final String UPDATE_ONE = "com.malubu.wordpress.UPDATE_ONE_WIDGET"; /** * We need the exact Uri instance to identify the Intent. */ private static HashMap<Integer, Uri> uris = new HashMap<Integer, Uri>(); @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); Log.d("onReceive", "action: " + action); if(action.equals(AppWidgetManager.ACTION_APPWIDGET_UPDATE) || action.equals(UPDATE_ONE)) { //Check if there is a single widget ID. int widgetID = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID); //If there is no single ID, call the super implementation. if(widgetID == AppWidgetManager.INVALID_APPWIDGET_ID) super.onReceive(context, intent); //Otherwise call our onUpdate() passing a one element array, with the retrieved ID. else this.onUpdate(context, AppWidgetManager.getInstance(context), new int[]{widgetID}); } else super.onReceive(context, intent); } @Override public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { Log.d("onUpdate", "called, number of instances " + appWidgetIds.length); for (int widgetId : appWidgetIds) { updateAppWidget(context, appWidgetManager, widgetId); } } /** * Each time an instance is removed, we cancel the associated AlarmManager. */ @Override public void onDeleted(Context context, int[] appWidgetIds) { super.onDeleted(context, appWidgetIds); for (int appWidgetId : appWidgetIds) { cancelAlarmManager(context, appWidgetId); } } protected void cancelAlarmManager(Context context, int widgetID) { AlarmManager alarm = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); Intent intentUpdate = new Intent(context, MaLuBuTestWidgetProvider.class); //AlarmManager are identified with Intent's Action and Uri. intentUpdate.setAction(MaLuBuTestWidgetProvider.UPDATE_ONE); //Don't put the uri to cancel all the AlarmManager with action UPDATE_ONE. intentUpdate.setData(uris.get(widgetID)); intentUpdate.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, widgetID); PendingIntent pendingIntentAlarm = PendingIntent.getBroadcast(context, 0, intentUpdate, PendingIntent.FLAG_UPDATE_CURRENT); alarm.cancel(pendingIntentAlarm); Log.d("cancelAlarmManager", "Cancelled Alarm. Action = " + MaLuBuTestWidgetProvider.UPDATE_ONE + " URI = " + uris.get(widgetID)); uris.remove(widgetID); } public static void addUri(int id, Uri uri) { uris.put(new Integer(id), uri); } private void updateAppWidget(Context context, AppWidgetManager appWidgetManager, int appWidgetId) { //Inflate layout. RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.widget); //Update UI. remoteViews.setTextViewText(R.id.label, getTimeStamp()); //Retrieve color. SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); int actColor = prefs.getInt(EXTRA_COLOR_VALUE+"_"+appWidgetId, Color.WHITE); Log.d("updateAppWidget", "retrieve: " + EXTRA_COLOR_VALUE+"_"+appWidgetId + " color: " + actColor); //Apply color. remoteViews.setInt(R.id.label, "setBackgroundColor", actColor); //Create the intent. Intent labelIntent = new Intent(context, MaLuBuTestWidgetProvider.class); labelIntent.setAction("android.appwidget.action.APPWIDGET_UPDATE"); //Put the ID of our widget to identify it later. labelIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId); PendingIntent labelPendingIntent = PendingIntent.getBroadcast(context, appWidgetId, labelIntent, PendingIntent.FLAG_UPDATE_CURRENT); remoteViews.setOnClickPendingIntent(R.id.label, labelPendingIntent); Log.d("updateAppWidget", "Updated ID: " + appWidgetId); //Call the Manager to ensure the changes take effect. appWidgetManager.updateAppWidget(appWidgetId, remoteViews); } private String getTimeStamp() { String res=""; Calendar calendar = Calendar.getInstance(); calendar.setTimeInMillis(System.currentTimeMillis()); Date now = calendar.getTime(); res += now.getHours()+":"+now.getMinutes()+":"+now.getSeconds(); return res; } }

MaLuBuTestWidgetActivity.java

public class MaLuBuTestWidgetActivity extends Activity { /** * Widget ID that Android give us after showing the Configuration Activity. */ private int widgetID; private int seconds; private int color; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); //Try to retrieve the ID of the impending widget. widgetID = getIntent().getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID); //No valid ID, so bail out. if (widgetID == AppWidgetManager.INVALID_APPWIDGET_ID) finish(); //If the user press BACK, do not add any widget. setResult(RESULT_CANCELED); setContentView(R.layout.main); //Start with 1 second. seconds = 1; final TextView mainResult = (TextView)findViewById(R.id.main_result); color = getViewColor(mainResult, Color.WHITE); final SeekBar seekBar = (SeekBar)findViewById(R.id.conf_seek); seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { public void onStopTrackingTouch(SeekBar seekBar) {} public void onStartTrackingTouch(SeekBar seekBar) {} public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { //From 1 sec to 60 sec = (from 0 sec to 59 sec) + 1 sec. seconds = progress+1; mainResult.setText(seconds + " seconds"); } }); } /** * Quick and dirty solution to get the background color. * @param view * @param defColor * @return */ private int getViewColor(View view, int defColor) { Drawable back = view.getBackground(); if(back instanceof PaintDrawable) return ((PaintDrawable)back).getPaint().getColor(); else if(back instanceof ColorDrawable) return ((ColorDrawable)back).getColor(); else return defColor; } public void mainColor(View source) { color = getViewColor(source, Color.WHITE); final TextView mainResult = (TextView)findViewById(R.id.main_result); mainResult.setBackgroundColor(color); mainResult.invalidate(); } public void mainOk(View source) { Log.d("mainOk", "called"); //Configuration... //Store the color. SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); SharedPreferences.Editor prefsEdit = prefs.edit(); prefsEdit.putInt(MaLuBuTestWidgetProvider.EXTRA_COLOR_VALUE+"_"+widgetID, color); Log.d("mainOk", "tag: " + MaLuBuTestWidgetProvider.EXTRA_COLOR_VALUE+"_"+widgetID + " color: " + color); prefsEdit.commit(); //Call onUpdate for the first time. Log.d("Ok Button", "First onUpdate broadcast sending..."); final Context context = MaLuBuTestWidgetActivity.this; Intent firstUpdate = new Intent(context, MaLuBuTestWidgetProvider.class); firstUpdate.setAction(AppWidgetManager.ACTION_APPWIDGET_UPDATE); //Put the ID of our widget to identify it later. firstUpdate.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, widgetID); context.sendBroadcast(firstUpdate); Log.d("Ok Button", "First onUpdate broadcast sent"); //Create and launch the AlarmManager. //N.B.: //Use a different action than the first update to have more reliable results. //Use explicit intents to have more reliable results. Uri.Builder build = new Uri.Builder(); build.appendPath(""+widgetID); Uri uri = build.build(); Intent intentUpdate = new Intent(context, MaLuBuTestWidgetProvider.class); intentUpdate.setAction(MaLuBuTestWidgetProvider.UPDATE_ONE);//Set an action anyway to filter it in onReceive() intentUpdate.setData(uri);//One Alarm per instance. //We will need the exact instance to identify the intent. MaLuBuTestWidgetProvider.addUri(widgetID, uri); intentUpdate.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, widgetID); PendingIntent pendingIntentAlarm = PendingIntent.getBroadcast(MaLuBuTestWidgetActivity.this, 0, intentUpdate, PendingIntent.FLAG_UPDATE_CURRENT); //If you want one global AlarmManager for all instances, put this alarmManger as //static and create it only the first time. //Then pass in the Intent all the ids and do not put the Uri. AlarmManager alarmManager = (AlarmManager)getSystemService(ALARM_SERVICE); alarmManager.setRepeating(AlarmManager.RTC_WAKEUP, System.currentTimeMillis()+(seconds*1000), (seconds*1000), pendingIntentAlarm); Log.d("Ok Button", "Created Alarm. Action = " + MaLuBuTestWidgetProvider.UPDATE_ONE + " URI = " + build.build().toString() + " Seconds = " + seconds); //Return the original widget ID, found in onCreate(). Intent resultValue = new Intent(); resultValue.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, widgetID); setResult(RESULT_OK, resultValue); finish(); } }

The results



