26-Apr-11 (Created: 26-Apr-11) | More in 'Android Widgets'

An approach and source code for Android Widget Models

As you start working with widgets, you will quickly realize that they are stateless. Every call back to paint the widget is invoked and the process dismantled (unless you take reasonable steps to keep this process alive, if that were to be needed). There is no explicit or direct mechanism to ask the widget: "Hey, what did I paint you with the last time around?". This is essentially a valid question if the widget were to have a state. Such a state is typically (indirectly) maintained using a persistence mechansim such as files, shared preferences, SQLLite, or the internet itself.

Remembering each element needed by a widget could be tedious to maintain one by one. The following code proposes the idea of a widget model behind every widget instance and provides a quicky save/retrieve model based on preferences. One can extend this idea to SQLLite and other persistence mechanisms.

However this code could save you sometime if you are looking for something quick and dirty to try out your widget ideas. This is not intended as a properly worked out framework for widget models as many things could be done to perfect the idea.

The way you would use this is

1. For every widget you are planning on create a java class that will work as a widget model, keeping the data for that model

2. For every visual item on that widget instance, there is a corresponding member in the widget model

3. You can ask the widget model to be saved and retrieved, there by saving/retrieving all the values for a widget model instance

4. The widget is painted using the widget model

5. The widget models are cleared out from persistence store during the right call backs.

IWidgetModelSaveContract


public interface IWidgetModelSaveContract 
{
   public void setValueForPref(String key, String value);
   public String getPrefname();
   public Map<String,String> getPrefsToSave();
   public void init();
}

Here is an elaborated interface of the same

public interface IWidgetModelSaveContract 
{
   //Given a key and value
   //set the equivalent member of the object to that value
   public void setValueForPref(String key, String value);
    
   //In a derived class tell
   //what should be the name of the 
   //shared preferences file
   //Typically points to the class name of the
   //widget provider
    public String getPrefname();
   
   //As a derived class return 
   //the data members of the object to be saved
   //in a map. You can choose to save only a subset
   //of your members. Some members or attributes
   //could be hardcoded, or transient and need not be saved.
   public Map<String,String> getPrefsToSave();

   //You will implement to initialize
   //your invariants or transients
    public void init();
}

APrefWidgetModel


//This class will implement the save and retrieve 
public abstract class APrefWidgetModel implements IWidgetModelSaveContract
{
   //**************************************************
   //* Abstract methods   
   //**************************************************   
   public abstract String getPrefname();
    public abstract void init();
   
   
   //**************************************************
   //* Local variables, constants etc   
   //**************************************************   
   private static String tag = "AWidgetModel";
   public static int STATUS_ACTIVE = 1;
   public static int STATUS_DELETED = 2;
   
   public int iid;
   public int status = STATUS_ACTIVE; 

   //**************************************************
   //* Construction, basic gets and sets   
   //**************************************************   
   public APrefWidgetModel(int instanceId)
   {
      iid = instanceId;
   }
   public void setStatus(int inStatus)
   {
      status = inStatus; 
   }
   public int getStatus()
   {
      return status;
   }
   public void setDeleted()
   {
      status = STATUS_DELETED;
   }
   public boolean isDeleted()
   {
      return (status == STATUS_DELETED) ? true : false;
   }
   //**************************************************
   //* Some basic defaults   
   //**************************************************   
   public Map<String,String> getPrefsToSave()
   {
      return null;
   }
    public void setValueForPref(String key, String value)
    {
       return;
    }
   
   //**************************************************
   //* Core implementation
   //* save()
   //* retrieve()   
   //* removePref() or delete
   //* cleareAllPreferences
   //**************************************************   
    public void save(Context context) 
    {
       Map<String,String> keyValuePairs = getPrefsToSave();
       if (keyValuePairs == null)
       {
          return;
       }
       //going to save some values
       
        SharedPreferences.Editor prefs = 
           context.getSharedPreferences(getPrefname(), 0).edit();

        for(String key: keyValuePairs.keySet())
        {
           String value = keyValuePairs.get(key);
           savePref(prefs,key,value);
        }
        //finally commit the values
        prefs.commit();
    }
    
    private void savePref(SharedPreferences.Editor prefs, String key, String value)
    {
       String newkey = getStoredKeyForFieldName(key);
       Log.d(tag,"saving:" + newkey + ":" + value);
        prefs.putString(newkey, value);
    }
    private void removePref(SharedPreferences.Editor prefs, String key)
    {
       String newkey = getStoredKeyForFieldName(key);
       Log.d(tag,"Removing:" + newkey );
        prefs.remove(newkey);
    }
    protected String getStoredKeyForFieldName(String fieldName)
    {
       return fieldName + "_" + iid;
    }
    public static void clearAllPreferences(Context context, String prefname) 
    {
       Log.d(tag,"Clearing all preferences for:" + prefname);
       SharedPreferences prefs=context.getSharedPreferences(prefname, 0);
       Log.d(tag,"Number of preferences:" + prefs.getAll().size());
        SharedPreferences.Editor prefsEdit = prefs.edit(); 
        prefsEdit.clear();
        //finally commit the values
        prefsEdit.commit();
    }
    
    public boolean retrieve(Context ctx)
    {
       Log.d(tag,"Rerieving preferences for widget id:" + iid);
       SharedPreferences prefs = ctx.getSharedPreferences(getPrefname(), 0);
       Map<String,?> keyValuePairs = prefs.getAll();
       Log.d(tag,"Number of keys for all widget ids of this type:" + keyValuePairs.size());
       boolean prefFound = false;
       for (String key: keyValuePairs.keySet())
       {
          if (isItMyPref(key) == true)
          {
             String value = (String)keyValuePairs.get(key);
             Log.d(tag,"setting value for:" + key + ":" + value);
             setValueForPref(key,value);
             prefFound = true;
          }
       }
       return prefFound;
    }
    public void removePrefs(Context context)
    {
      Log.d(tag,"Removing preferences for widget id:" + iid);
       Map<String,String> keyValuePairs = getPrefsToSave();
       if (keyValuePairs == null)
       {
          return;
       }
       //going to save some values
       
        SharedPreferences.Editor prefs = 
           context.getSharedPreferences(getPrefname(), 0).edit();

        for(String key: keyValuePairs.keySet())
        {
           removePref(prefs,key);
        }
        //finally commit the values
        prefs.commit();
    }
    private boolean isItMyPref(String keyname)
    {
       Log.d(tag,"Examinging keyname:" + keyname);
       if (keyname.indexOf("_" + iid) > 0)
       {
          return true;
       }
       return false;
    }
}

BDayWidgetModel


public class BDayWidgetModel extends APrefWidgetModel 
{
   private static String tag="BDayWidgetModel";
   
   //**************************************************
   //* Give a name to uniquely identify shared preferences   
   //**************************************************   
   private static String BDAY_WIDGET_PROVIDER_NAME=
      "com.ai.android.BDayWidget.BDayWidgetProvider";
   

   //**************************************************
   //* Real model where attributes we are after are   
   //**************************************************   
   private String name = "anon";
   private String bday = "1/1/2001";

   //remember these to set the values back
   private static String F_NAME = "name";
   private static String F_BDAY = "bday";

   //the invariant to buy gifts from   
   private String url="http://www.google.com";
   
   //**************************************************
   //* Construction, basic gets and sets   
   //**************************************************   
   public BDayWidgetModel(int instanceId)
   {
      super(instanceId);
   }
   
   public BDayWidgetModel(int instanceId, String inName, String inBday)
   {
      super(instanceId);
      name=inName;
      bday=inBday;
   }
    public void init()
    {
    }

    public void setName(String inname)
    {
       name=inname;
    }
    public void setBday(String inbday)
    {
       bday=inbday;
    }
    public String getName()
    {
       return name;
    }
    public String getBday()
    {
       return bday;
    }
    
    public long howManyDays()
    {
       try
       {
          return Utils.howfarInDays(Utils.getDate(this.bday));
       }
       catch(ParseException x)
       {
          return 20000;
       }
    }
   //****************************
   //Implement save contract
   //****************************
    public void setValueForPref(String key, String value)
    {
       if (key.equals(getStoredKeyForFieldName(BDayWidgetModel.F_NAME)))
       {
          Log.d(tag,"Setting name to:" + value);
          this.name = value;
          return;
       }
       if (key.equals(getStoredKeyForFieldName(BDayWidgetModel.F_BDAY)))
       {
          Log.d(tag,"Setting bday to:" + value);
          this.bday = value;
          return;
       }
       Log.d(tag,"Sorry the key does not match:" + key);
    }
    
    //you need to do this
   public String getPrefname()
   {
      return BDayWidgetModel.BDAY_WIDGET_PROVIDER_NAME;
   }
   
   //return key value pairs you want to be saved
   public Map<String,String> getPrefsToSave()
   {
      Map<String, String> map
      = new HashMap<String,String>();
      map.put(BDayWidgetModel.F_NAME, this.name);
      map.put(BDayWidgetModel.F_BDAY, this.bday);
      return map;
   }
   public String toString()
   {
      StringBuffer sbuf = new StringBuffer();
      sbuf.append("iid:" + iid);
      sbuf.append("name:" + name);
      sbuf.append("bday:" + bday);
      return sbuf.toString();
   }
   public static void clearAllPreferences(Context ctx)
   {
      APrefWidgetModel.clearAllPreferences(ctx,BDayWidgetModel.BDAY_WIDGET_PROVIDER_NAME);
   }
   
   public static BDayWidgetModel retrieveModel(Context ctx, int widgetId)
   {
      BDayWidgetModel m = new BDayWidgetModel(widgetId);
      boolean found = m.retrievePrefs(ctx);
      return found ? m:null; 
   }
}

Here is a method that can be used to test these


   public static void testPrefSave(Context ctx)
   {
      BDayWidgetModel.clearAllPreferences(ctx);
      BDayWidgetModel m = new BDayWidgetModel(1,"Satya","1/2/2009");
      m.savePreferences(ctx);
      BDayWidgetModel m1 = BDayWidgetModel.retrieveModel(ctx, 1);
      if (m1 == null)
      {
         Log.d(tag,"Cant locate the wm");
         return;
      }
      Log.d(tag,m1.toString());
      m1.setName("Satya2");
      m1.setBday("1/3/2009");
      m1.savePreferences(ctx);
      m1.retrievePrefs(ctx);
      Log.d(tag,"Retrieved m1");
      Log.d(tag,m1.toString());
      
      BDayWidgetModel m3 = new BDayWidgetModel(3,"Satya3","1/3/2009");
      m3.savePreferences(ctx);
      BDayWidgetModel m3r = BDayWidgetModel.retrieveModel(ctx, 3);
      Log.d(tag,"Retrieved m3");
      Log.d(tag,m3.toString());
   }

ConfigureBDayWidgetActivity

Here is how I have used the model in the configure activity


public class ConfigureBDayWidgetActivity extends Activity 
{
   private static String tag = "ConfigureBDayWidgetActivity";
   private int mAppWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID;
   
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.edit_bday_widget);
        setupButton();
        
        Intent intent = getIntent();
        Bundle extras = intent.getExtras();
        if (extras != null) {
            mAppWidgetId = extras.getInt(
                    AppWidgetManager.EXTRA_APPWIDGET_ID, 
                    AppWidgetManager.INVALID_APPWIDGET_ID);
        }
        
    }
    
    private void setupButton()
    {
       Button b = (Button)this.findViewById(R.id.bdw_button_update_bday_widget);
       b.setOnClickListener(
             new Button.OnClickListener(){
                public void onClick(View v)
                {
                   parentButtonClicked(v);
                }
             });
       
    }
    private void parentButtonClicked(View v)
    {
       String name = this.getName();
       String date = this.getDate();
       if (Utils.validateDate(date) == false)
       {
          this.setDate("wrong date:" + date);
          return;
       }
       if (this.mAppWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID)
       {
          Log.d(tag, "invalid app widget id");
          return;
       }
       updateAppWidgetLocal(name,date);
       Intent resultValue = new Intent();
       resultValue.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, mAppWidgetId);
       setResult(RESULT_OK, resultValue);
       finish();
    }
    private String getName()
    {
        EditText nameEdit = (EditText)this.findViewById(R.id.bdw_bday_name_id);
        String name = nameEdit.getText().toString();
        return name;
    }
    private String getDate()
    {
        EditText dateEdit = (EditText)this.findViewById(R.id.bdw_bday_date_id);
        String dateString = dateEdit.getText().toString();
        return dateString;
    }
    private void setDate(String errorDate)
    {
        EditText dateEdit = (EditText)this.findViewById(R.id.bdw_bday_date_id);
        dateEdit.setText("error");
        dateEdit.requestFocus();
    }
    private void updateAppWidgetLocal(String name, String dob)
    {
       BDayWidgetModel m = new BDayWidgetModel(mAppWidgetId,name,dob);
       updateAppWidget(this,AppWidgetManager.getInstance(this),m);
       m.savePreferences(this);
    }

    public static void updateAppWidget(Context context, 
            AppWidgetManager appWidgetManager,
            BDayWidgetModel widgetModel) 
   {
      RemoteViews views = new RemoteViews(context.getPackageName(), 
                    R.layout.bday_widget);
      
      views.setTextViewText(R.id.bdw_w_name
         , widgetModel.getName() + ":" + widgetModel.iid);
      
      views.setTextViewText(R.id.bdw_w_date
            , widgetModel.getBday());
      
      //update the name
      views.setTextViewText(R.id.bdw_w_days,Long.toString(widgetModel.howManyDays()));
      
        Intent defineIntent = new Intent(Intent.ACTION_VIEW, 
              Uri.parse("http://www.google.com"));
        PendingIntent pendingIntent = 
           PendingIntent.getActivity(context,
                    0 /* no requestCode */, 
                    defineIntent, 
                    0 /* no flags */);
        views.setOnClickPendingIntent(R.id.bdw_w_button_buy, pendingIntent);
      
      // Tell the widget manager
      appWidgetManager.updateAppWidget(widgetModel.iid, views);
   }
}   

BDayWidgetProvider

And here is how I have used the models in the widget provider


public class BDayWidgetProvider extends AppWidgetProvider 
{
   private static final String tag = "BDayWidgetProvider";
    public void onUpdate(Context context, 
                        AppWidgetManager appWidgetManager, 
                        int[] appWidgetIds) 
   {
        Log.d(tag, "onUpdate called");
        final int N = appWidgetIds.length;
        Log.d(tag, "Number of widgets:" + N);
        for (int i=0; i<N; i++) 
        {
            int appWidgetId = appWidgetIds[i];
            updateAppWidget(context, appWidgetManager, appWidgetId);
        }
    }
    
    public void onDeleted(Context context, int[] appWidgetIds) 
    {
        Log.d(tag, "onDelete called");
        final int N = appWidgetIds.length;
        for (int i=0; i<N; i++) 
        {
           Log.d(tag,"deleting:" + appWidgetIds[i]);
           BDayWidgetModel.retrieveModel(context, appWidgetIds[i]);
        }
    }
    @Override 
    public void onReceive(Context context, Intent intent) 
    { 
        final String action = intent.getAction(); 
        if (AppWidgetManager.ACTION_APPWIDGET_DELETED.equals(action)) 
        {
           Bundle extras = intent.getExtras();
           
            final int appWidgetId = extras.getInt 
                              (AppWidgetManager.EXTRA_APPWIDGET_ID, 
                               AppWidgetManager.INVALID_APPWIDGET_ID);
            
            if (appWidgetId != AppWidgetManager.INVALID_APPWIDGET_ID) 
            { 
                this.onDeleted(context, new int[] { appWidgetId }); 
            } 
        } 
        else
        { 
            super.onReceive(context, intent); 
        } 
    }
    
    public void onEnabled(Context context) 
   {
        Log.d(tag, "onEnabled called");
       BDayWidgetModel.clearAllPreferences(context);
        PackageManager pm = context.getPackageManager();
        pm.setComponentEnabledSetting(
                new ComponentName("com.ai.android.BDayWidget", 
                       ".BDayWidgetProvider"),
                PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
                PackageManager.DONT_KILL_APP);
    }

    public void onDisabled(Context context) 
    {
        Log.d(tag, "onDisabled called");
       BDayWidgetModel.clearAllPreferences(context);
        PackageManager pm = context.getPackageManager();
        pm.setComponentEnabledSetting(
                new ComponentName("com.ai.android.BDayWidget", 
                       ".BDayWidgetProvider"),
                PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
                PackageManager.DONT_KILL_APP);
    }

    private void updateAppWidget(Context context, 
                          AppWidgetManager appWidgetManager,
                           int appWidgetId) 
   {
       BDayWidgetModel bwm = BDayWidgetModel.retrieveModel(context, appWidgetId);
       if (bwm == null)
       {
          Log.d(tag,"No widget model found for:" + appWidgetId);
          return;
       }
       ConfigureBDayWidgetActivity.updateAppWidget(context, appWidgetManager, bwm);
   }
}

Resources

1. Here is my research on home widgets

2. Here is the formal documentation on appwidgets from google

3. Here is the api for shared preferences

4. Here is the API for shared preferences editor