Sunday, July 8, 2012

Tutorial on Android Homescreen Widget with AlarmManager.

This is a follow up tutorial on Android widget. If you haven't read it till now then please go through    it before starting this tutorial. You may be aware that AppWidgetProvider's lowest interval is 30 mins. In this tutorial we will learn  to create widget with update interval less than 30 mins using AlarmManager.

New update:

In Android 4.1, a new feature has been introduced for Homescreen widget which enables widget to reorganize its view when resized. To support this feature a new method onAppWidgetOptionsChanged() has been introduced in AppWidgetProvider class. This method gets called in response to the ACTION_APPWIDGET_OPTIONS_CHANGED broadcast when this widget has been layed out at a new size. 

Project Information:  Meta-information about the project.

Platform Version : Android API Level 16.
IDE : Eclipse Helios Service Release 2
Emulator: Android 4.1

Prerequisite: Preliminary knowledge of Android application framework, Intent Broadcast receiver and AlarmManager.

Example with fixed update interval less than 30 mins.

In this tutorial we will create time widget which shows current time. This widget will get updated every second and we will be using AlarmManager for it. Here, repeating alarm is set for one second interval. But in real world scenario, it is not recommended to use one second repeating alarm because it drains the battery fast. You have to follow the similar steps mentioned in previous widget tutorial  to write widget layout file. But this time we are introducing a TextView field in the layout which will display the time. The content of the "time_widget_layout.xml" is given below.
<linearlayout android:background="@drawable/widget_background" 
   android:layout_height="match_parent" android:layout_width="match_parent" 
   android:orientation="vertical" 
   xmlns:android="http://schemas.android.com/apk/res/android">
     <textview android:gravity="center_horizontal|center_vertical" 
          android:id="@+id/tvTime" android:layout_gravity="center" 
          android:layout_height="match_parent" android:layout_margin="4dip" 
          android:layout_width="match_parent" android:textcolor="#000000" 
          style="android: style/TextAppearance.Medium;"/>
</linearlayout>

Follow the same procedure to create the AppWidgetProvider metadata file. The content of metadata file "widget_metadata.xml" is given below.
  <appwidget-provider android:initiallayout="@layout/time_widget_layout" 
        android:minheight="40dp" android:minwidth="130dp"  
        android:updateperiodmillis="1800000" 
        xmlns:android="http://schemas.android.com/apk/res/android">
</appwidget-provider>

In this tutorial, onEnabled(), onDsiabled(), onUpdate() and onAppWidgetOptionsChanged() have been defined unlike the previous widget tutorial where only onUpdate() was defined. 

  • onEnabled(): An instance of AlarmManager is created here to start the repeating timer and register the intent with the AlarmManager.  As this method gets called at the very first instance of widget installation, it helps to set repeating alarm  only once.
  • onDisabled(): In this method, alarm is canceled because this method gets called as soon as the very last instance of widget is removed/uninstalled and we don't want to leave the registered alarm even when it's not being used.
  • onUpdate(): This method updates the time on remote TextView.
  • onAppWidgetOptionsChanged(): This method gets called when the widget is resized.

package com.rakesh.widgetalarmmanagerexample;

import android.app.AlarmManager;
import android.app.PendingIntent;
import android.appwidget.AppWidgetManager;
import android.appwidget.AppWidgetProvider;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.widget.RemoteViews;
import android.widget.Toast;

public class TimeWidgetProvider extends AppWidgetProvider {

 @Override
 public void onDeleted(Context context, int[] appWidgetIds) {
  Toast.makeText(context, "TimeWidgetRemoved id(s):"+appWidgetIds, Toast.LENGTH_SHORT).show();
  super.onDeleted(context, appWidgetIds);
 }

 @Override
 public void onDisabled(Context context) {
  Toast.makeText(context, "onDisabled():last widget instance removed", Toast.LENGTH_SHORT).show(); 
  Intent intent = new Intent(context, AlarmManagerBroadcastReceiver.class);
  PendingIntent sender = PendingIntent.getBroadcast(context, 0, intent, 0);
  AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
  alarmManager.cancel(sender);
  super.onDisabled(context);
 }

 @Override
 public void onEnabled(Context context) {
  super.onEnabled(context);
  AlarmManager am=(AlarmManager)context.getSystemService(Context.ALARM_SERVICE);
  Intent intent = new Intent(context, AlarmManagerBroadcastReceiver.class);
  PendingIntent pi = PendingIntent.getBroadcast(context, 0, intent, 0);
  //After after 3 seconds
  am.setRepeating(AlarmManager.RTC_WAKEUP, System.currentTimeMillis()+ 100 * 3, 1000 , pi);
 }

 @Override
 public void onUpdate(Context context, AppWidgetManager appWidgetManager,
   int[] appWidgetIds) {
  ComponentName thisWidget = new ComponentName(context,
    TimeWidgetProvider.class);

  for (int widgetId : appWidgetManager.getAppWidgetIds(thisWidget)) {

   //Get the remote views
   RemoteViews remoteViews = new RemoteViews(context.getPackageName(),
     R.layout.time_widget_layout);
   // Set the text with the current time.
   remoteViews.setTextViewText(R.id.tvTime, Utility.getCurrentTime("hh:mm:ss a"));
   appWidgetManager.updateAppWidget(widgetId, remoteViews);
  }
 }

 @Override
 public void onAppWidgetOptionsChanged(Context context,
   AppWidgetManager appWidgetManager, int appWidgetId,
   Bundle newOptions) {
  //Do some operation here, once you see that the widget has change its size or position.
  Toast.makeText(context, "onAppWidgetOptionsChanged() called", Toast.LENGTH_SHORT).show();
 }
}


Broadcast receiver is defined to handle the intent registered with alarm. This broadcast receiver gets called every second because repeating alarm has been set in the AppWidgetProvider classs for 1 second. Here, onReceive() method has been defined which updates the widget with the current time and getCurrentTime() has been used to get the current time.
package com.rakesh.widgetalarmmanagerexample;

import android.app.AlarmManager;
import android.app.PendingIntent;
import android.appwidget.AppWidgetManager;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.os.PowerManager;
import android.widget.RemoteViews;
import android.widget.Toast;

public class AlarmManagerBroadcastReceiver extends BroadcastReceiver {

 @Override
 public void onReceive(Context context, Intent intent) {
  PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
  PowerManager.WakeLock wl = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "YOUR TAG");
  //Acquire the lock
  wl.acquire();

  //You can do the processing here update the widget/remote views.
  RemoteViews remoteViews = new RemoteViews(context.getPackageName(),
    R.layout.time_widget_layout);
  remoteViews.setTextViewText(R.id.tvTime,  Utility.getCurrentTime("hh:mm:ss a"));
  ComponentName thiswidget = new ComponentName(context, TimeWidgetProvider.class);
  AppWidgetManager manager = AppWidgetManager.getInstance(context);
  manager.updateAppWidget(thiswidget, remoteViews);
  //Release the lock
  wl.release();
 }
}

It's always a good idea to keep utility methods in some utility class which can be accessed from other packages. getCurrentTime() has been defined in the Uitility class. This method is used in AppWidgetProvider and BroadcastReciever classes. 
package com.rakesh.widgetalarmmanagerexample;

import java.text.Format;
import java.text.SimpleDateFormat;
import java.util.Date;

public class Utility {
  public static String getCurrentTime(String timeformat){
      Format formatter = new SimpleDateFormat(timeformat);
         return formatter.format(new Date());
     }
}

In Android manifest file, we need to include WAKE_LOCK permission because wake lock is used in broadcast receiver. AlarmManagerBroadcastReceiver has been registered as broadcast receiver. Remaining part is simple to understand.
  <manifest android:versioncode="1" android:versionname="1.0" 
      package="com.rakesh.widgetalarmmanagerexample" 
      xmlns:android="http://schemas.android.com/apk/res/android">
    <uses-sdk android:minsdkversion="16" android:targetsdkversion="16"/>

<uses-permission android:name="android.permission.WAKE_LOCK"/>
    <application android:icon="@drawable/ic_launcher" 
          android:label="@string/app_name">
        <activity android:label="@string/title_activity_widget_alarm_manager" 
                android:name=".WidgetAlarmManagerActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>
                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>
        <receiver android:icon="@drawable/ic_launcher" 
             android:label="@string/app_name"
             android:name=".TimeWidgetProvider">
            <intent-filter>
                <action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
            </intent-filter>
            <meta-data android:name="android.appwidget.provider"  
                android:resource="@xml/widget_metadata"/>
        </receiver>
        <receiver android:name=".AlarmManagerBroadcastReceiver"/>
    </application>
</manifest>

Once the code is executed, the widget gets registered. When you install widget on homescreen, it appears as shown below.
Android Homescreen time widget

you can download source code from here.

Related tutorial:



Your valuable comments are always welcomed. It will help to improve my post and understanding.

"By three methods we may learn wisdom: First, by reflection, which is noblest; Second, by imitation, which is easiest; and third by experience, which is the bitterest."
By : Confucius