Take your time – Widgets and AlarmManager

Fork me on GitHub

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:

  1. Users cannot select the update interval.
  2. 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:

  1. 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.
  2. 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.
  3. 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:

  1. Put the AlarmManager in a static variable (and initialize it only if is null).
  2. The AlarmManager should be initialized in onEnable() of the AppWidgetProvider. That method is called after the creation of the first widget instance.
  3. 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


About these ads

About Luong
http://www.linkedin.com/in/manhluong

12 Responses to Take your time – Widgets and AlarmManager

  1. Rallado says:

    Cool article !!!
    Why do you not leave source code package to download ?

  2. Luong says:

    Thank you Rollado!

    You can find the complete code in my github: https://github.com/manhluong

    In particular, for this tutorial, look here: https://github.com/manhluong/Various/tree/master/MaLuBuTestWidget

    It’s a fully functional and complete Eclipse project.

    Sorry for not being clear about where to find the code!

  3. Macos says:

    Thank you! I was looking for it!

  4. Delaji says:

    Nice man very nice! Kudos to you my friend.

  5. devi says:

    awesome tutorial…
    one question, what will happen if the phone reboot ? will the alarm manager restore to the configured interval ?
    if not, how can we solve that ?

  6. devi says:

    it fails to load after phone reboot.

  7. Pingback: Android Setting Update Interval On Appwidget With Listview | Laaptu

  8. laaptu says:

    Thanks a lot for this awesome tutorial

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Follow

Get every new post delivered to your Inbox.