This document contains

1. Sample code for a live folder based on contacts

2. Uses a cursor wrapper to make it live to respond to changes in the underlying contacts

3. Contains pictures of using a live folder

satya - Thursday, April 23, 2009 3:39:44 PM

manifest file


<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
      package="com.ai.android.livefolders"
      android:versionCode="1"
      android:versionName="1.0">
    <application android:icon="@drawable/icon" android:label="@string/app_name">
        <activity android:name=".SimpleActivity"
                  android:label="@string/app_name">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        
       <!-- LIVE FOLDERS -->
        <activity
            android:name=".AllContactsLiveFolderCreatorActivity"
            android:label="New live folder activity"
            android:icon="@drawable/icon">

            <intent-filter>
                <action android:name="android.intent.action.CREATE_LIVE_FOLDER" />
                <category android:name="android.intent.category.DEFAULT" />
            </intent-filter>
        </activity>

		<provider android:authorities="com.ai.livefolders.contacts"
		android:multiprocess="true"
            android:name=".MyContactsProvider" />
        
    </application>
    <uses-sdk android:minSdkVersion="3" />
<uses-permission android:name="android.permission.READ_CONTACTS"></uses-permission>
</manifest>

satya - Thursday, April 23, 2009 3:40:23 PM

SimpleActivity


package com.ai.android.livefolders;

import android.app.Activity;
import android.os.Bundle;

public class SimpleActivity extends Activity {
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
    }
}

satya - Thursday, April 23, 2009 3:41:06 PM

AllContactsLiveFolderCreatorActivity


package com.ai.android.livefolders;

import android.app.Activity;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.provider.LiveFolders;

public class AllContactsLiveFolderCreatorActivity extends Activity 
{
    @Override
    protected void onCreate(Bundle savedInstanceState) 
    {
        super.onCreate(savedInstanceState);

        final Intent intent = getIntent();
        final String action = intent.getAction();
        
        if (LiveFolders.ACTION_CREATE_LIVE_FOLDER.equals(action)) 
        {
            setResult(RESULT_OK, 
            		createLiveFolder(MyContactsProvider.CONTACTS_URI,
            					"Contacts LF",
            					R.drawable.icon)
            		);
        } else 
        {
            setResult(RESULT_CANCELED);
        }
        finish();
    }
    
    private Intent createLiveFolder(Uri uri, String name, int icon) 
    {
        final Intent intent = new Intent();
        intent.setData(uri);
        intent.putExtra(LiveFolders.EXTRA_LIVE_FOLDER_NAME, name);
        intent.putExtra(LiveFolders.EXTRA_LIVE_FOLDER_ICON,
                Intent.ShortcutIconResource.fromContext(this, icon));
        intent.putExtra(LiveFolders.EXTRA_LIVE_FOLDER_DISPLAY_MODE, 
        				LiveFolders.DISPLAY_MODE_LIST);
        return intent;
    }
}

satya - Thursday, April 23, 2009 3:43:08 PM

MyContactsProvider


package com.ai.android.livefolders;

import android.content.ContentProvider;
import android.content.ContentValues;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.net.Uri;
import android.provider.BaseColumns;
import android.provider.LiveFolders;
import android.provider.Contacts.People;
import android.util.Log;

public class MyContactsProvider extends ContentProvider {

    public static final String AUTHORITY = "com.ai.livefolders.contacts";
   
    //Uri that goes as input to the livefolder creation
    public static final Uri CONTACTS_URI = Uri.parse("content://" +
            AUTHORITY + "/contacts"   );

    //To distinguish this URI
    private static final int TYPE_MY_URI = 0;
    private static final UriMatcher URI_MATCHER;
    static{
      URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH);
      URI_MATCHER.addURI(AUTHORITY, "contacts", TYPE_MY_URI);
    }
    
    @Override
    public boolean onCreate() {
        return true;
    }

    @Override
    public int bulkInsert(Uri arg0, ContentValues[] values) {
      return 0; //nothing to insert
    }

    //Set of columns needed by a LiveFolder
    //This is the live folder contract
    private static final String[] CURSOR_COLUMNS = new String[]{
      BaseColumns._ID, 
      LiveFolders.NAME, 
      LiveFolders.DESCRIPTION, 
      LiveFolders.INTENT, 
      LiveFolders.ICON_PACKAGE, 
      LiveFolders.ICON_RESOURCE
    };
    
    //In case there are no rows
    //use this stand in as an error message
    //Notice it has the same set of columns of a live folder
    private static final String[] CURSOR_ERROR_COLUMNS = new String[]{
      BaseColumns._ID, 
      LiveFolders.NAME, 
      LiveFolders.DESCRIPTION
    };
    
    
    //The error message row
    private static final Object[] ERROR_MESSAGE_ROW = 
         new Object[]
         {
          -1, //id
          "No contacts found", //name 
          "Check your contacts database" //description
         };
    
    //The error cursor to use
    private static MatrixCursor sErrorCursor = new MatrixCursor(CURSOR_ERROR_COLUMNS);
    static {
      sErrorCursor.addRow(ERROR_MESSAGE_ROW);
    }

    //Columns to be retrieved from the contacts database
    private static final String[] CONTACTS_COLUMN_NAMES = new String[]{
      People._ID, 
      People.DISPLAY_NAME, 
      People.TIMES_CONTACTED, 
      People.STARRED
    };
    
    public Cursor query(Uri uri, String[] projection, String selection,
            String[] selectionArgs, String sortOrder) 
    {
       //Figure out the uri and return error if not matching
      int type = URI_MATCHER.match(uri);
      if(type == UriMatcher.NO_MATCH)
      {
        return sErrorCursor;
      }

      Log.i("ss", "query called");
      
      try 
      {
       MatrixCursor mc = loadNewData(this);
        mc.setNotificationUri(getContext().getContentResolver(),  
              Uri.parse("content://contacts/people/"));
        MyCursor wmc = new MyCursor(mc,this);
        return wmc;
      } 
      catch (Throwable e) 
      {
        return sErrorCursor;
      }
    }
    
    public static MatrixCursor loadNewData(ContentProvider cp)
    {
       MatrixCursor mc = new MatrixCursor(CURSOR_COLUMNS);
        Cursor allContacts = null;
        try
        {
           allContacts = cp.getContext().getContentResolver().query(
              People.CONTENT_URI, 
              CONTACTS_COLUMN_NAMES, 
              null, //row filter 
              null, 
              People.DISPLAY_NAME); //order by
           
           while(allContacts.moveToNext())
           {
             String timesContacted = "Times contacted: "+allContacts.getInt(2);
             
             Object[] rowObject = new Object[]
             {
                 allContacts.getLong(0),    //id
                 allContacts.getString(1),    //name
                 timesContacted,          //description
                 Uri.parse("content://contacts/people/"+allContacts.getLong(0)), //intent
                 cp.getContext().getPackageName(), //package
                 R.drawable.icon   //icon
             };
             mc.addRow(rowObject);
           }
          return mc;
        }
        finally
        {
           allContacts.close();
        }
        
    }
    
    
    @Override
    public String getType(Uri uri) 
    {
      //indicates the MIME type for a given URI
      //targeted for this wrapper provider
      //This usually looks like 
      // "vnd.android.cursor.dir/vnd.google.note"
      return People.CONTENT_TYPE;
    }

    public Uri insert(Uri uri, ContentValues initialValues) {
      throw new UnsupportedOperationException(
            "no insert as this is just a wrapper");
    }
    
    @Override
    public int delete(Uri uri, String selection, String[] selectionArgs) {
        throw new UnsupportedOperationException(
        "no delete as this is just a wrapper");
    }

    public int update(Uri uri, ContentValues values, 
            String selection, String[] selectionArgs) 
   {
        throw new UnsupportedOperationException(
        "no update as this is just a wrapper");
    }
}

satya - Thursday, April 23, 2009 3:43:46 PM

MyCursor


package com.ai.android.livefolders;

import android.content.ContentProvider;
import android.database.MatrixCursor;
import android.util.Log;

public class MyCursor extends BetterCursorWrapper 
{
   private ContentProvider mcp = null;
   
    public MyCursor(MatrixCursor mc, ContentProvider inCp) 
    {
        super(mc);
        mcp = inCp;
    }   
    public boolean requery() 
    {
       Log.i("ss", "requery called");
       MatrixCursor mc = MyContactsProvider.loadNewData(mcp);
       this.setInternalCursor(mc);
       return super.requery();
    }
}

satya - Thursday, April 23, 2009 3:45:02 PM

BetterCursorWrapper


package com.ai.android.livefolders;

import android.content.ContentResolver;
import android.database.CharArrayBuffer;
import android.database.ContentObserver;
import android.database.CrossProcessCursor;
import android.database.CursorWindow;
import android.database.DataSetObserver;
import android.net.Uri;
import android.os.Bundle;

/**
 * Wrapper class for Cursor that delegates 
 * all calls to the actual cursor object
 */

public class BetterCursorWrapper implements CrossProcessCursor
{
   //Holds the internal cursor to delegate methods to
   protected CrossProcessCursor internalCursor;
   
   //Constructor takes a crossprocesscursor as an input
   public BetterCursorWrapper(CrossProcessCursor inCursor)
   {
      this.setInternalCursor(inCursor);
   }
   
   //You can reset in one of the derived classes methods
   public void setInternalCursor(CrossProcessCursor inCursor)
   {
      internalCursor = inCursor;
   }
   
   //All delegated methods follow
   public void fillWindow(int arg0, CursorWindow arg1) {
      internalCursor.fillWindow(arg0, arg1);
   }
   public CursorWindow getWindow() {
      return internalCursor.getWindow();
   }
   public boolean onMove(int arg0, int arg1) {
      return internalCursor.onMove(arg0, arg1);
   }

   public void close() {
      internalCursor.close();
   }

   public void copyStringToBuffer(int arg0, CharArrayBuffer arg1) {
      internalCursor.copyStringToBuffer(arg0, arg1);
   }

   public void deactivate() {
      internalCursor.deactivate();
   }

   public byte[] getBlob(int columnIndex) {
      return internalCursor.getBlob(columnIndex);
   }

   public int getColumnCount() {
      return internalCursor.getColumnCount();
   }

   public int getColumnIndex(String columnName) {
      return internalCursor.getColumnIndex(columnName);
   }

   public int getColumnIndexOrThrow(String columnName)
         throws IllegalArgumentException {
      return internalCursor.getColumnIndexOrThrow(columnName);
   }

   public String getColumnName(int columnIndex) {
      return internalCursor.getColumnName(columnIndex);
   }

   public String[] getColumnNames() {
      return internalCursor.getColumnNames();
   }

   public int getCount() {
      return internalCursor.getCount();
   }

   public double getDouble(int columnIndex) {
      return internalCursor.getDouble(columnIndex);
   }

   public Bundle getExtras() {
      return internalCursor.getExtras();
   }

   public float getFloat(int columnIndex) {
      return internalCursor.getFloat(columnIndex);
   }

   public int getInt(int columnIndex) {
      return internalCursor.getInt(columnIndex);
   }

   public long getLong(int columnIndex) {
      return internalCursor.getLong(columnIndex);
   }

   public int getPosition() {
      return internalCursor.getPosition();
   }

   public short getShort(int columnIndex) {
      return internalCursor.getShort(columnIndex);
   }

   public String getString(int columnIndex) {
      return internalCursor.getString(columnIndex);
   }

   public boolean getWantsAllOnMoveCalls() {
      return internalCursor.getWantsAllOnMoveCalls();
   }

   public boolean isAfterLast() {
      return internalCursor.isAfterLast();
   }

   public boolean isBeforeFirst() {
      return internalCursor.isBeforeFirst();
   }

   public boolean isClosed() {
      return internalCursor.isClosed();
   }

   public boolean isFirst() {
      return internalCursor.isFirst();
   }

   public boolean isLast() {
      return internalCursor.isLast();
   }

   public boolean isNull(int columnIndex) {
      return internalCursor.isNull(columnIndex);
   }

   public boolean move(int offset) {
      return internalCursor.move(offset);
   }

   public boolean moveToFirst() {
      return internalCursor.moveToFirst();
   }

   public boolean moveToLast() {
      return internalCursor.moveToLast();
   }

   public boolean moveToNext() {
      return internalCursor.moveToNext();
   }

   public boolean moveToPosition(int position) {
      return internalCursor.moveToPosition(position);
   }

   public boolean moveToPrevious() {
      return internalCursor.moveToPrevious();
   }

   public void registerContentObserver(ContentObserver observer) {
      internalCursor.registerContentObserver(observer);
   }

   public void registerDataSetObserver(DataSetObserver observer) {
      internalCursor.registerDataSetObserver(observer);
   }

   public boolean requery() {
      return internalCursor.requery();
   }

   public Bundle respond(Bundle extras) {
      return internalCursor.respond(extras);
   }

   public void setNotificationUri(ContentResolver cr, Uri uri) {
      internalCursor.setNotificationUri(cr, uri);
   }

   public void unregisterContentObserver(ContentObserver observer) {
      internalCursor.unregisterContentObserver(observer);
   }

   public void unregisterDataSetObserver(DataSetObserver observer) {
      internalCursor.unregisterDataSetObserver(observer);
   }
}

satya - Friday, April 24, 2009 8:48:45 AM

Here is the look of 1.5 desktop on the emulator

satya - Friday, April 24, 2009 8:49:46 AM

Use a long click to see the folder options on the home page

satya - Friday, April 24, 2009 8:50:32 AM

Open up the folders menu item

satya - Friday, April 24, 2009 8:51:25 AM

Click on New Live Folder item to create the following live folder

satya - Friday, April 24, 2009 8:52:35 AM

Click on Contacts LF live folder to see a list of contacts

satya - Friday, April 24, 2009 8:54:04 AM

Click on one of the contacts to see it displayed like the following

satya - Friday, April 24, 2009 8:54:40 AM

Click on the menu tab to see options for that single contact

satya - Friday, April 24, 2009 8:55:18 AM

Click on edit contact to see contact details or change them

satya - Friday, April 24, 2009 8:59:04 AM

Here are the key elements you need to know to make live folders to work

1. You will need to create a cursor that has a set of predefined standard column names

2. You will need a cursor that is capable of "requery" semantics

3. At the end of the query you will need to register your cursor for dynamic updates aginst the underlying content provider as you are likely to wrap that content provider inorder to translate the column names.

4. I needed to wrap the cross process cursor to make it work

satya - Monday, May 04, 2009 11:35:04 AM

Click here to download the zip file for the livefolders project

Click here to download the zip file for the livefolders project