Initial gdata2 checkin. Just package name changes to make diffing the changes later easier
diff --git a/src/com/google/wireless/gdata2/GDataException.java b/src/com/google/wireless/gdata2/GDataException.java
new file mode 100644
index 0000000..b5850f1
--- /dev/null
+++ b/src/com/google/wireless/gdata2/GDataException.java
@@ -0,0 +1,64 @@
+// Copyright 2007 The Android Open Source Project
+package com.google.wireless.gdata2;
+
+/**
+ * The base exception for GData operations.
+ */
+public class GDataException extends Exception {
+
+    private final Throwable cause;
+
+    /**
+     * Creates a new empty GDataException.
+     */
+    public GDataException() {
+        super();
+        cause = null;
+    }
+
+    /**
+     * Creates a new GDataException with the supplied message.
+     * @param message The message for this GDataException.
+     */
+    public GDataException(String message) {
+        super(message);
+        cause = null;
+    }
+
+    /**
+     * Creates a new GDataException with the supplied message and underlying
+     * cause.
+     *
+     * @param message The message for this GDataException.
+     * @param cause The underlying cause that was caught and wrapped by this
+     * GDataException.
+     */
+    public GDataException(String message, Throwable cause) {
+        super(message);
+        this.cause = cause;
+    }
+
+    /**
+     * Creates a new GDataException with the underlying cause.
+     *
+     * @param cause The underlying cause that was caught and wrapped by this
+     * GDataException.
+     */
+    public GDataException(Throwable cause) {
+        this("", cause);
+    }
+
+    /**
+     * @return the cause of this GDataException or null if the cause is unknown.
+     */
+    public Throwable getCause() {
+        return cause;
+    }
+
+    /**
+     * @return a string representation of this exception.
+     */
+    public String toString() {
+        return super.toString() + (cause != null ? " " + cause.toString() : "");
+    }
+}
diff --git a/src/com/google/wireless/gdata2/calendar/client/CalendarClient.java b/src/com/google/wireless/gdata2/calendar/client/CalendarClient.java
new file mode 100644
index 0000000..8bac2fa
--- /dev/null
+++ b/src/com/google/wireless/gdata2/calendar/client/CalendarClient.java
@@ -0,0 +1,96 @@
+// Copyright 2007 The Android Open Source Project
+
+package com.google.wireless.gdata2.calendar.client;
+
+import com.google.wireless.gdata2.calendar.data.CalendarEntry;
+import com.google.wireless.gdata2.client.GDataClient;
+import com.google.wireless.gdata2.client.GDataParserFactory;
+import com.google.wireless.gdata2.client.GDataServiceClient;
+import com.google.wireless.gdata2.client.HttpException;
+import com.google.wireless.gdata2.client.QueryParams;
+import com.google.wireless.gdata2.parser.GDataParser;
+import com.google.wireless.gdata2.parser.ParseException;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * GDataServiceClient for accessing Google Calendar.  This client can access and
+ * parse both the meta feed (list of calendars for a user) and events feeds
+ * (calendar entries for a specific user).  The parsers this class uses handle
+ * the XML version of feeds.
+ */
+// TODO: add a method that applies projections such as cutting the attendees.
+public class CalendarClient extends GDataServiceClient {
+    /** Service value for calendar. */
+    public static final String SERVICE = "cl";
+
+    public static final String PROJECTION_PRIVATE_FULL = "/private/full";
+    public static final String PROJECTION_PRIVATE_SELF_ATTENDANCE = "/private/full-selfattendance";
+
+    /** Standard base url for a calendar feed. */
+    private static final String CALENDAR_BASE_FEED_URL =
+        "http://www.google.com/calendar/feeds/";
+
+    /**
+     * Create a new CalendarClient.  Uses the standard base URL for calendar feeds.
+     * @param client The GDataClient that should be used to authenticate
+     * requests, retrieve feeds, etc.
+     * @param factory The factory that should be used to obtain {@link GDataParser}s used by this
+     * client.
+     */
+    public CalendarClient(GDataClient client, GDataParserFactory factory) {
+        super(client, factory);
+    }
+
+    /* (non-Javadoc)
+     * @see GDataServiceClient#getServiceName
+     */
+    public String getServiceName() {
+        return SERVICE;
+    }
+
+    /**
+     * Returns the url for the default feed for a user, after applying the
+     * provided QueryParams.
+     * @param username The username for this user.
+     * @param projection the projection to use
+     * @param params The QueryParams that should be applied to the default feed.
+     * @return The url that should be used to retrieve a user's default feed.
+     */
+    public String getDefaultCalendarUrl(String username, String projection, QueryParams params) {
+        String feedUrl = CALENDAR_BASE_FEED_URL + getGDataClient().encodeUri(username);
+        feedUrl += projection;
+        if (params == null) {
+            return feedUrl;
+        }
+        return params.generateQueryUrl(feedUrl);
+    }
+
+    /**
+     * Returns the url for the metafeed for user, which contains the information about
+     * the user's calendars.
+     * @param username The username for this user.
+     * @return The url that should be used to retrieve a user's default feed.
+     */
+    public String getUserCalendarsUrl(String username) {
+        return CALENDAR_BASE_FEED_URL + getGDataClient().encodeUri(username);
+    }
+
+    /**
+     * Fetches the meta feed containing the list of calendars for a user.  The
+     * caller is responsible for closing the returned {@link GDataParser}.
+     *
+     * @param feedUrl the URL of the user calendars feed
+     * @param authToken The authentication token for this user
+     * @return A GDataParser with the meta feed containing the list of
+     *   calendars for this user.
+     * @throws ParseException Thrown if the feed could not be fetched.
+     */
+    public GDataParser getParserForUserCalendars(String feedUrl, String authToken)
+            throws ParseException, IOException, HttpException {
+        GDataClient gDataClient = getGDataClient();
+        InputStream is = gDataClient.getFeedAsStream(feedUrl, authToken);
+        return getGDataParserFactory().createParser(CalendarEntry.class, is);
+    }
+}
diff --git a/src/com/google/wireless/gdata2/calendar/client/package.html b/src/com/google/wireless/gdata2/calendar/client/package.html
new file mode 100644
index 0000000..1c9bf9d
--- /dev/null
+++ b/src/com/google/wireless/gdata2/calendar/client/package.html
@@ -0,0 +1,5 @@
+<html>
+<body>
+    {@hide}
+</body>
+</html>
diff --git a/src/com/google/wireless/gdata2/calendar/data/CalendarEntry.java b/src/com/google/wireless/gdata2/calendar/data/CalendarEntry.java
new file mode 100644
index 0000000..069d0af
--- /dev/null
+++ b/src/com/google/wireless/gdata2/calendar/data/CalendarEntry.java
@@ -0,0 +1,161 @@
+// Copyright 2007 The Android Open Source Project
+
+package com.google.wireless.gdata2.calendar.data;
+
+import com.google.wireless.gdata2.data.Entry;
+
+/**
+ * Entry containing information about a calendar.
+ */
+public class CalendarEntry extends Entry {
+    
+    /**
+     * Access level constant indicating the user has no access to a calendar.
+     */
+    public static final byte ACCESS_NONE = 0;
+    
+    /**
+     * Access level constant indicating the user can read (but not write) to
+     * a calendar. 
+     */
+    public static final byte ACCESS_READ = 1;
+    
+    /**
+     * Access level constant indicating the user can only view free-busy 
+     * information for a calendar.
+     */
+    public static final byte ACCESS_FREEBUSY = 2;
+    
+    /**
+     * Access level constant indicating the user can edit this calendar.
+     */
+    public static final byte ACCESS_EDITOR = 3;
+    
+    /**
+     * Access level constant indicating the user owns this calendar.
+     */
+    public static final byte ACCESS_OWNER = 4;
+    
+    private byte accessLevel = ACCESS_READ;
+    // TODO: rename to feed Url?
+    private String alternateLink = null;
+    private String color = null;
+    private boolean hidden = false;
+    private boolean selected = true;
+    private String timezone = null;
+    
+    /**
+     * Creates a new, empty calendar entry.
+     */
+    public CalendarEntry() {
+    }
+
+    public void clear() {
+        super.clear();
+        accessLevel = ACCESS_READ;
+        alternateLink = null;
+        color = null;
+        hidden = false;
+        selected = true;
+        timezone = null;
+    }
+
+    /**
+     * @return the accessLevel
+     */
+    public byte getAccessLevel() {
+        return accessLevel;
+    }
+
+    /**
+     * @param accessLevel the accessLevel to set
+     */
+    public void setAccessLevel(byte accessLevel) {
+        this.accessLevel = accessLevel;
+    }
+
+    /**
+     * @return the alternateLink
+     */
+    public String getAlternateLink() {
+        return alternateLink;
+    }
+
+    /**
+     * @param alternateLink the alternateLink to set
+     */
+    public void setAlternateLink(String alternateLink) {
+        this.alternateLink = alternateLink;
+    }
+    
+    /**
+     * @return the color
+     */
+    public String getColor() {
+        return color;
+    }
+
+    /**
+     * @param color the color to set
+     */
+    public void setColor(String color) {
+        this.color = color;
+    }
+
+    /**
+     * @return the hidden
+     */
+    public boolean isHidden() {
+        return hidden;
+    }
+
+    /**
+     * @param hidden the hidden to set
+     */
+    public void setHidden(boolean hidden) {
+        this.hidden = hidden;
+    }
+
+    /**
+     * @return the selected
+     */
+    public boolean isSelected() {
+        return selected;
+    }
+
+    /**
+     * @param selected the selected to set
+     */
+    public void setSelected(boolean selected) {
+        this.selected = selected;
+    }
+
+    /**
+     * @return the timezone
+     */
+    public String getTimezone() {
+        return timezone;
+    }
+
+    /**
+     * @param timezone the timezone to set
+     */
+    public void setTimezone(String timezone) {
+        this.timezone = timezone;
+    }
+    
+    public void toString(StringBuffer sb) {
+        sb.append("ACCESS LEVEL: ");
+        sb.append(accessLevel);
+        sb.append('\n');
+        appendIfNotNull(sb, "ALTERNATE LINK", alternateLink);
+        appendIfNotNull(sb, "COLOR", color);
+        sb.append("HIDDEN: ");
+        sb.append(hidden);
+        sb.append('\n');
+        sb.append("SELECTED: ");
+        sb.append(selected);
+        sb.append('\n');
+        appendIfNotNull(sb, "TIMEZONE", timezone);
+    }
+}
diff --git a/src/com/google/wireless/gdata2/calendar/data/CalendarsFeed.java b/src/com/google/wireless/gdata2/calendar/data/CalendarsFeed.java
new file mode 100644
index 0000000..b2b4669
--- /dev/null
+++ b/src/com/google/wireless/gdata2/calendar/data/CalendarsFeed.java
@@ -0,0 +1,18 @@
+// Copyright 2007 The Android Open Source Project
+
+package com.google.wireless.gdata2.calendar.data;
+
+import com.google.wireless.gdata2.data.Feed;
+
+/**
+ * Meta feed containing the list of calendars for a user.
+ */
+public class CalendarsFeed extends Feed {
+    
+    /**
+     * Creates a new empty calendars feed.
+     */
+    public CalendarsFeed() {
+    }
+    
+}
diff --git a/src/com/google/wireless/gdata2/calendar/data/EventEntry.java b/src/com/google/wireless/gdata2/calendar/data/EventEntry.java
new file mode 100644
index 0000000..ea7c249
--- /dev/null
+++ b/src/com/google/wireless/gdata2/calendar/data/EventEntry.java
@@ -0,0 +1,315 @@
+// Copyright 2007 The Android Open Source Project
+
+package com.google.wireless.gdata2.calendar.data;
+
+import com.google.wireless.gdata2.data.Entry;
+
+import java.util.Hashtable;
+import java.util.Vector;
+import java.util.Enumeration;
+
+/**
+ * Entry containing information about an event in a calendar.
+ */
+public class EventEntry extends Entry {
+
+    // TODO: pack all of these enums into an int
+
+    /**
+     * Status constant indicating that a user's attendance at an event is
+     * tentative.
+     */
+    public static final byte STATUS_TENTATIVE = 0;
+
+    /**
+     * Status constant indicating that a user's attendance at an event is
+     * confirmed.
+     */
+    public static final byte STATUS_CONFIRMED = 1;
+
+    /**
+     * Status constant indicating that an event has been cancelled.
+     */
+    public static final byte STATUS_CANCELED = 2;
+
+    /**
+     * Visibility constant indicating that an event uses the user's default
+     * visibility.
+     */
+    public static final byte VISIBILITY_DEFAULT = 0;
+
+    /**
+     * Visibility constant indicating that an event has been marked
+     * confidential.
+     */
+    public static final byte VISIBILITY_CONFIDENTIAL = 1;
+
+    /**
+     * Visibility constant indicating that an event has been marked private.
+     */
+    public static final byte VISIBILITY_PRIVATE = 2;
+
+    /**
+     * Visibility constant indicating that an event has been marked public.
+     */
+    public static final byte VISIBILITY_PUBLIC = 3;
+
+    /**
+     * Transparency constant indicating that an event has been marked opaque.
+     */
+    public static final byte TRANSPARENCY_OPAQUE = 0;
+
+    /**
+     * Transparency constant indicating that an event has been marked
+     * transparent.
+     */
+    public static final byte TRANSPARENCY_TRANSPARENT = 1;
+
+    private byte status = STATUS_TENTATIVE;
+    private String recurrence = null;
+    private byte visibility = VISIBILITY_DEFAULT;
+    private byte transparency = TRANSPARENCY_OPAQUE;
+    private Vector attendees = new Vector();
+    private Vector whens = new Vector();
+    private Vector reminders = null;
+    private String originalEventId = null;
+    private String originalEventStartTime = null;
+    private String where = null;
+    private String commentsUri = null;
+    private Hashtable extendedProperties = null;
+
+    /**
+     * Creates a new empty event entry.
+     */
+    public EventEntry() {
+    }
+
+    /*
+     * (non-Javadoc)
+     * @see com.google.wireless.gdata2.data.Entry#clear()
+     */
+    public void clear() {
+        super.clear();
+        status = STATUS_TENTATIVE;
+        recurrence = null;
+        visibility = VISIBILITY_DEFAULT;
+        transparency = TRANSPARENCY_OPAQUE;
+        attendees.removeAllElements();
+        whens.removeAllElements();
+        reminders = null;
+        originalEventId = null;
+        originalEventStartTime = null;
+        where = null;
+        commentsUri = null;
+        extendedProperties = null;
+    }
+
+    /**
+     * @return the recurrence
+     */
+    public String getRecurrence() {
+        return recurrence;
+    }
+
+    /**
+     * @param recurrence the recurrence to set
+     */
+    public void setRecurrence(String recurrence) {
+        this.recurrence = recurrence;
+    }
+
+    /**
+     * @return the status
+     */
+    public byte getStatus() {
+        return status;
+    }
+
+    /**
+     * @param status the status to set
+     */
+    public void setStatus(byte status) {
+        this.status = status;
+    }
+
+    /**
+     * @return the transparency
+     */
+    public byte getTransparency() {
+        return transparency;
+    }
+
+    /**
+     * @param transparency the transparency to set
+     */
+    public void setTransparency(byte transparency) {
+        this.transparency = transparency;
+    }
+
+    /**
+     * @return the visibility
+     */
+    public byte getVisibility() {
+        return visibility;
+    }
+
+    /**
+     * @param visibility the visibility to set
+     */
+    public void setVisibility(byte visibility) {
+        this.visibility = visibility;
+    }
+
+    public void clearAttendees() {
+        attendees.clear();
+    }
+
+    public void addAttendee(Who attendee) {
+        attendees.add(attendee);
+    }
+
+    public Vector getAttendees() {
+        return attendees;
+    }
+
+    public void clearWhens() {
+        whens.clear();
+    }
+
+    public void addWhen(When when) {
+        whens.add(when);
+    }
+
+    public Vector getWhens() {
+        return whens;
+    }
+
+    public When getFirstWhen() {
+        if (whens.isEmpty()) {
+            return null;
+        }
+        return (When) whens.elementAt(0);
+    }
+
+    public Vector getReminders() {
+        return reminders;
+    }
+
+    public void addReminder(Reminder reminder) {
+        if (reminders == null) {
+            reminders = new Vector();
+        }
+        reminders.add(reminder);
+    }
+
+    public void clearReminders() {
+        reminders = null;
+    }
+
+    public String getOriginalEventId() {
+        return originalEventId;
+    }
+
+    public void setOriginalEventId(String originalEventId) {
+        this.originalEventId = originalEventId;
+    }
+
+    public String getOriginalEventStartTime() {
+        return originalEventStartTime;
+    }
+
+    public void setOriginalEventStartTime(String originalEventStartTime) {
+        this.originalEventStartTime = originalEventStartTime;
+    }
+
+    /**
+     * @return the where
+     */
+    public String getWhere() {
+        return where;
+    }
+
+    /**
+     * @param where the where to set
+     */
+    public void setWhere(String where) {
+        this.where = where;
+    }
+
+    public Hashtable getExtendedProperties() {
+        return extendedProperties;
+    }
+
+    public String getExtendedProperty(String name) {
+        if (extendedProperties == null) {
+            return null;
+        }
+        String value = null;
+        if (extendedProperties.containsKey(name)) {
+            value = (String) extendedProperties.get(name);
+        }
+        return value;
+    }
+
+    public void addExtendedProperty(String name, String value) {
+        if (extendedProperties == null) {
+            extendedProperties = new Hashtable();
+        }
+        extendedProperties.put(name, value);
+    }
+
+    public void clearExtendedProperties() {
+        extendedProperties = null;
+    }
+
+    public String getCommentsUri() {
+        return commentsUri;
+    }
+
+    public void setCommentsUri(String commentsUri) {
+        this.commentsUri = commentsUri;
+    }
+
+    public void toString(StringBuffer sb) {
+        super.toString(sb);
+        sb.append("STATUS: " + status + "\n");
+        appendIfNotNull(sb, "RECURRENCE", recurrence);
+        sb.append("VISIBILITY: " + visibility + "\n");
+        sb.append("TRANSPARENCY: " + transparency + "\n");
+        
+        appendIfNotNull(sb, "ORIGINAL_EVENT_ID", originalEventId);
+        appendIfNotNull(sb, "ORIGINAL_START_TIME", originalEventStartTime);
+
+        Enumeration whos = this.attendees.elements();
+        while (whos.hasMoreElements()) {
+            Who who = (Who) whos.nextElement();
+            who.toString(sb);
+        }
+
+        Enumeration times = this.whens.elements();
+        while (times.hasMoreElements()) {
+            When when = (When) times.nextElement();
+            when.toString(sb);
+        }
+        if (reminders != null) {
+            Enumeration alarms = reminders.elements();
+            while (alarms.hasMoreElements()) {
+                Reminder reminder = (Reminder) alarms.nextElement();
+                reminder.toString(sb);
+            }
+        }
+        appendIfNotNull(sb, "WHERE", where);
+        appendIfNotNull(sb, "COMMENTS", commentsUri);
+        if (extendedProperties != null) {
+            Enumeration entryNames = extendedProperties.keys();
+            while (entryNames.hasMoreElements()) {
+                String name = (String) entryNames.nextElement();
+                String value = (String) extendedProperties.get(name);
+                sb.append(name);
+                sb.append(':');
+                sb.append(value);
+                sb.append('\n');
+            }
+        }
+    }
+}
diff --git a/src/com/google/wireless/gdata2/calendar/data/EventsFeed.java b/src/com/google/wireless/gdata2/calendar/data/EventsFeed.java
new file mode 100644
index 0000000..b524e26
--- /dev/null
+++ b/src/com/google/wireless/gdata2/calendar/data/EventsFeed.java
@@ -0,0 +1,33 @@
+// Copyright 2007 The Android Open Source Project
+
+package com.google.wireless.gdata2.calendar.data;
+
+import com.google.wireless.gdata2.data.Feed;
+
+/**
+ * Feed containing events in a calendar.
+ */
+public class EventsFeed extends Feed {
+    
+    private String timezone = null;
+    
+    /**
+     * Creates a new empty events feed.
+     */
+    public EventsFeed() {
+    }
+
+    /**
+     * @return the timezone
+     */
+    public String getTimezone() {
+        return timezone;
+    }
+
+    /**
+     * @param timezone the timezone to set
+     */
+    public void setTimezone(String timezone) {
+        this.timezone = timezone;
+    }
+}
diff --git a/src/com/google/wireless/gdata2/calendar/data/Recurrence.java b/src/com/google/wireless/gdata2/calendar/data/Recurrence.java
new file mode 100644
index 0000000..17a0a49
--- /dev/null
+++ b/src/com/google/wireless/gdata2/calendar/data/Recurrence.java
@@ -0,0 +1,28 @@
+// Copyright 2007 The Android Open Source Project
+
+package com.google.wireless.gdata2.calendar.data;
+
+/**
+ * Container for information about a Recurrence.
+ */
+// TODO: get rid of this?
+public class Recurrence {
+    
+    private final String recurrence;
+    
+    /**
+     * Creates a new recurrence for the provide recurrence string.
+     * @param recurrence The recurrence string that should be parsed.
+     */
+    public Recurrence(String recurrence) {
+        this.recurrence = recurrence;
+    }
+    
+    /*
+     * (non-Javadoc)
+     * @see java.lang.Object#toString()
+     */
+    public String toString() {
+        return recurrence;
+    }
+}
diff --git a/src/com/google/wireless/gdata2/calendar/data/Reminder.java b/src/com/google/wireless/gdata2/calendar/data/Reminder.java
new file mode 100644
index 0000000..9672123
--- /dev/null
+++ b/src/com/google/wireless/gdata2/calendar/data/Reminder.java
@@ -0,0 +1,92 @@
+// Copyright 2007 The Android Open Source Project
+package com.google.wireless.gdata2.calendar.data;
+
+/**
+ * Contains information about a reminder for an event.
+ */
+public class Reminder {
+    /**
+     * Default reminder method as defined on the calendar server.
+     */
+    public static final byte METHOD_DEFAULT = 0;
+
+    /**
+     * Reminder that uses e-mail for notification.
+     */
+    public static final byte METHOD_EMAIL = 1;
+
+    /**
+     * Reminder that uses sms for notification.
+     */
+    public static final byte METHOD_SMS = 2;
+
+    /**
+     * Reminder that uses a local alert for notification.
+     */
+    public static final byte METHOD_ALERT = 3;
+
+    /**
+     * Reminder that uses a calendar-wide default time for the alarm.
+     */
+    public static final int MINUTES_DEFAULT = -1;    
+
+    // do absolute times work with recurrences?
+    // private String absoluteTime;
+    private int minutes = MINUTES_DEFAULT;
+    private byte method = METHOD_DEFAULT;
+
+    /**
+     * Creates a new empty reminder.
+     */
+    public Reminder() {
+    }
+
+    /**
+     * Returns the method of the reminder.
+     * @return The method of the reminder.
+     */
+    public byte getMethod() {
+        return method;
+    }
+
+    /**
+     * Sets the method of the reminder.
+     * @param method The method of the reminder.
+     */
+    public void setMethod(byte method) {
+        this.method = method;
+    }
+
+    /**
+     * Gets how many minutes before an event that the reminder should be
+     * triggered.
+     * @return How many minutes before an event that the reminder should be
+     * triggered.
+     */
+    public int getMinutes() {
+        return minutes;
+    }
+
+    /**
+     * Sets how many minutes before an event that the reminder should be
+     * triggered.
+     * @param minutes How many minutes before an event that the reminder should
+     * be triggered.
+     */
+    public void setMinutes(int minutes) {
+        this.minutes = minutes;
+    }
+
+    public void toString(StringBuffer sb) {
+        sb.append("REMINDER MINUTES: " + minutes);
+        sb.append("\n");
+        sb.append("REMINDER METHOD: " + method);
+        sb.append("\n");
+    }
+
+    public String toString() {
+        StringBuffer sb = new StringBuffer();
+        toString(sb);
+        return sb.toString();
+    }
+}
diff --git a/src/com/google/wireless/gdata2/calendar/data/When.java b/src/com/google/wireless/gdata2/calendar/data/When.java
new file mode 100644
index 0000000..a2b1975
--- /dev/null
+++ b/src/com/google/wireless/gdata2/calendar/data/When.java
@@ -0,0 +1,53 @@
+// Copyright 2007 The Android Open Source Project
+package com.google.wireless.gdata2.calendar.data;
+
+import com.google.wireless.gdata2.data.StringUtils;
+
+/**
+ * Contains information about the start and end of an instance of an event.
+ */
+public class When {
+    private final String startTime;
+    private final String endTime;
+
+    /**
+     * Creates a new When.
+     * @param startTime The start of the event.
+     * @param endTime The end of the event.
+     */
+    public When(String startTime, String endTime) {
+        this.startTime = startTime;
+        this.endTime = endTime;
+    }
+
+    /**
+     * Returns the start time for the event.
+     * @return The start time for the event.
+     */
+    public String getStartTime() {
+        return startTime;
+    }
+
+    /**
+     * Returns the end time for the event.
+     * @return The end time for the event.
+     */
+    public String getEndTime() {
+        return endTime;
+    }
+
+    public void toString(StringBuffer sb) {
+        if (!StringUtils.isEmpty(startTime)) {
+            sb.append("START TIME: " + startTime + "\n");
+        }
+        if (!StringUtils.isEmpty(endTime)) {
+            sb.append("END TIME: " + endTime + "\n");
+        }
+    }
+
+    public String toString() {
+        StringBuffer sb = new StringBuffer();
+        toString(sb);
+        return sb.toString();
+    }
+}
diff --git a/src/com/google/wireless/gdata2/calendar/data/Who.java b/src/com/google/wireless/gdata2/calendar/data/Who.java
new file mode 100644
index 0000000..8a21dfa
--- /dev/null
+++ b/src/com/google/wireless/gdata2/calendar/data/Who.java
@@ -0,0 +1,153 @@
+package com.google.wireless.gdata2.calendar.data;
+
+import com.google.wireless.gdata2.data.StringUtils;
+
+/**
+ * Contains information about a event attendee.
+ */
+public class Who {
+
+    /**
+     * No attendee relationhip set.  Used in {@link #setRelationship}
+     * and {@link #getRelationship}.
+     */
+    public static final byte RELATIONSHIP_NONE = 0;
+
+    /**
+     * A general meeting/event attendee.  Used in {@link #setRelationship}
+     * and {@link #getRelationship}.
+     */
+    public static final byte RELATIONSHIP_ATTENDEE = 1;
+
+    /**
+     * Event organizer.  An organizer is not necessary an attendee.
+     * Used in {@link #setRelationship} and {@link #getRelationship}.
+     */
+    public static final byte RELATIONSHIP_ORGANIZER = 2;
+
+    /**
+     * Performer.  Similar to {@link #RELATIONSHIP_SPEAKER}, but with more emphasis on art than
+     * speech delivery.
+     * Used in {@link #setRelationship} and {@link #getRelationship}.
+     */
+    public static final byte RELATIONSHIP_PERFORMER = 3;
+
+    /**
+     * Speaker.  Used in {@link #setRelationship} and {@link #getRelationship}.
+     */
+    public static final byte RELATIONSHIP_SPEAKER = 4;
+
+    /**
+     * No attendee type set.  Used in {@link #setType} and {@link #getType}.
+     */
+    public static final byte TYPE_NONE = 0;
+
+    /**
+     * Optional attendee.  Used in {@link #setType} and {@link #getType}.
+     */
+    public static final byte TYPE_OPTIONAL = 1;
+
+    /**
+     * Required attendee.  Used in {@link #setType} and {@link #getType}.
+     */
+    public static final byte TYPE_REQUIRED = 2;
+
+    /**
+     * No attendee status set.  Used in {@link #setStatus} and {@link #getStatus}.
+     */
+    public static final byte STATUS_NONE = 0;
+
+
+    /**
+     * Attendee has accepted.  Used in {@link #setStatus} and {@link #getStatus}.
+     */
+    public static final byte STATUS_ACCEPTED = 1;
+
+    /**
+     * Attendee has declined.  Used in {@link #setStatus} and {@link #getStatus}.
+     */
+    public static final byte STATUS_DECLINED = 2;
+
+    /**
+     * Invitation has been sent, but the person has not accepted.
+     * Used in {@link #setStatus} and {@link #getStatus}.
+     */
+    public static final byte STATUS_INVITED = 3;
+
+    /**
+     * Attendee has accepted tentatively.  Used in {@link #setStatus} and {@link #getStatus}.
+     */
+    public static final byte STATUS_TENTATIVE = 4;
+
+    private String email;
+    private String value;
+    private byte relationship = RELATIONSHIP_NONE;
+    private byte type = TYPE_NONE;
+    private byte status = STATUS_NONE;
+
+    /**
+     * Creates a new Who, representing event attendee information.
+     */
+    public Who() {
+    }
+
+    public String getEmail() {
+        return email;
+    }
+
+    public void setEmail(String email) {
+        this.email = email;
+    }
+
+    public String getValue() {
+        return value;
+    }
+
+    public void setValue(String value) {
+        this.value = value;
+    }
+
+    public byte getRelationship() {
+        return relationship;
+    }
+
+    public void setRelationship(byte relationship) {
+        this.relationship = relationship;
+    }
+
+    public byte getType() {
+        return type;
+    }
+
+    public void setType(byte type) {
+        this.type = type;
+    }
+
+    public byte getStatus() {
+        return status;
+    }
+
+    public void setStatus(byte status) {
+        this.status = status;
+    }
+
+    protected void toString(StringBuffer sb) {
+        if (!StringUtils.isEmpty(email)) {
+            sb.append("EMAIL: " + email + "\n");
+        }
+
+        if (!StringUtils.isEmpty(value)) {
+            sb.append("VALUE: " + value + "\n");
+        }
+
+        sb.append("RELATIONSHIP: " + relationship + "\n");
+        sb.append("TYPE: " + type + "\n");
+        sb.append("STATUS: " + status + "\n");
+    }
+
+    public String toString() {
+        StringBuffer sb = new StringBuffer();
+        toString(sb);
+        return sb.toString();
+    }
+}
diff --git a/src/com/google/wireless/gdata2/calendar/data/package.html b/src/com/google/wireless/gdata2/calendar/data/package.html
new file mode 100644
index 0000000..1c9bf9d
--- /dev/null
+++ b/src/com/google/wireless/gdata2/calendar/data/package.html
@@ -0,0 +1,5 @@
+<html>
+<body>
+    {@hide}
+</body>
+</html>
diff --git a/src/com/google/wireless/gdata2/calendar/package.html b/src/com/google/wireless/gdata2/calendar/package.html
new file mode 100644
index 0000000..1c9bf9d
--- /dev/null
+++ b/src/com/google/wireless/gdata2/calendar/package.html
@@ -0,0 +1,5 @@
+<html>
+<body>
+    {@hide}
+</body>
+</html>
diff --git a/src/com/google/wireless/gdata2/calendar/parser/package.html b/src/com/google/wireless/gdata2/calendar/parser/package.html
new file mode 100644
index 0000000..1c9bf9d
--- /dev/null
+++ b/src/com/google/wireless/gdata2/calendar/parser/package.html
@@ -0,0 +1,5 @@
+<html>
+<body>
+    {@hide}
+</body>
+</html>
diff --git a/src/com/google/wireless/gdata2/calendar/parser/xml/XmlCalendarGDataParserFactory.java b/src/com/google/wireless/gdata2/calendar/parser/xml/XmlCalendarGDataParserFactory.java
new file mode 100644
index 0000000..214fadf
--- /dev/null
+++ b/src/com/google/wireless/gdata2/calendar/parser/xml/XmlCalendarGDataParserFactory.java
@@ -0,0 +1,98 @@
+// Copyright 2007 The Android Open Source Project
+
+package com.google.wireless.gdata2.calendar.parser.xml;
+
+import com.google.wireless.gdata2.calendar.data.CalendarEntry;
+import com.google.wireless.gdata2.calendar.data.EventEntry;
+import com.google.wireless.gdata2.calendar.serializer.xml.XmlEventEntryGDataSerializer;
+import com.google.wireless.gdata2.client.GDataParserFactory;
+import com.google.wireless.gdata2.data.Entry;
+import com.google.wireless.gdata2.parser.GDataParser;
+import com.google.wireless.gdata2.parser.ParseException;
+import com.google.wireless.gdata2.parser.xml.XmlParserFactory;
+import com.google.wireless.gdata2.serializer.GDataSerializer;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.InputStream;
+
+/**
+ * GDataParserFactory that creates XML GDataParsers and GDataSerializers for
+ * Google Calendar.
+ */
+public class XmlCalendarGDataParserFactory implements GDataParserFactory {
+
+    private final XmlParserFactory xmlFactory;
+
+    public XmlCalendarGDataParserFactory(XmlParserFactory xmlFactory) {
+        this.xmlFactory = xmlFactory;
+    }
+
+    /**
+     * Returns a parser for a calendars meta-feed.
+     *
+     * @param is The input stream to be parsed.
+     * @return A parser for the stream.
+     */
+    public GDataParser createCalendarsFeedParser(InputStream is)
+            throws ParseException {
+        XmlPullParser xmlParser;
+        try {
+            xmlParser = xmlFactory.createParser();
+        } catch (XmlPullParserException xppe) {
+            throw new ParseException("Could not create XmlPullParser", xppe);
+        }
+        return new XmlCalendarsGDataParser(is, xmlParser);
+    }
+
+    /*
+     * (non-javadoc)
+     *
+     * @see GDataParserFactory#createParser
+     */
+    public GDataParser createParser(InputStream is) throws ParseException {
+        XmlPullParser xmlParser;
+        try {
+            xmlParser = xmlFactory.createParser();
+        } catch (XmlPullParserException xppe) {
+            throw new ParseException("Could not create XmlPullParser", xppe);
+        }
+        return new XmlEventsGDataParser(is, xmlParser);
+    }
+
+    /*
+     * (non-Javadoc)
+     *
+     * @see com.google.wireless.gdata2.client.GDataParserFactory#createParser(
+     *      int, java.io.InputStream)
+     */
+    public GDataParser createParser(Class entryClass, InputStream is)
+            throws ParseException {
+        if (entryClass == CalendarEntry.class) {
+            return createCalendarsFeedParser(is);
+        } else if (entryClass == EventEntry.class) {
+            return createParser(is);
+        }
+        throw new IllegalArgumentException("Unknown entry class '" + entryClass.getName()
+                + "' specified.");
+    }
+
+    /**
+     * Creates a new {@link GDataSerializer} for the provided entry. The entry
+     * <strong>must</strong> be an instance of {@link EventEntry}.
+     *
+     * @param entry The {@link EventEntry} that should be serialized.
+     * @return The {@link GDataSerializer} that will serialize this entry.
+     * @throws IllegalArgumentException Thrown if entry is not an
+     *         {@link EventEntry}.
+     * @see GDataParserFactory#createSerializer
+     */
+    public GDataSerializer createSerializer(Entry entry) {
+        if (!(entry instanceof EventEntry)) {
+            throw new IllegalArgumentException("Expected EventEntry!");
+        }
+        EventEntry eventEntry = (EventEntry) entry;
+        return new XmlEventEntryGDataSerializer(xmlFactory, eventEntry);
+    }
+}
diff --git a/src/com/google/wireless/gdata2/calendar/parser/xml/XmlCalendarsGDataParser.java b/src/com/google/wireless/gdata2/calendar/parser/xml/XmlCalendarsGDataParser.java
new file mode 100644
index 0000000..6a8d2c4
--- /dev/null
+++ b/src/com/google/wireless/gdata2/calendar/parser/xml/XmlCalendarsGDataParser.java
@@ -0,0 +1,136 @@
+// Copyright 2007 The Android Open Source Project
+
+package com.google.wireless.gdata2.calendar.parser.xml;
+
+import com.google.wireless.gdata2.calendar.data.CalendarEntry;
+import com.google.wireless.gdata2.calendar.data.CalendarsFeed;
+import com.google.wireless.gdata2.data.Entry;
+import com.google.wireless.gdata2.data.Feed;
+import com.google.wireless.gdata2.parser.ParseException;
+import com.google.wireless.gdata2.parser.xml.XmlGDataParser;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * GDataParser for the meta feed listing a user's calendars.
+ */
+public class XmlCalendarsGDataParser extends XmlGDataParser {
+
+    /**
+     * Creates a new XmlCalendarsGDataParser.
+     * @param is The InputStream containing the calendars feed.
+     * @throws ParseException Thrown if an XmlPullParser could not be created.
+     */
+    public XmlCalendarsGDataParser(InputStream is, XmlPullParser parser)
+            throws ParseException {
+        super(is, parser);
+    }
+
+    /*
+     * (non-Javadoc)
+     * @see com.google.wireless.gdata2.parser.xml.XmlGDataParser#createFeed()
+     */
+    protected Feed createFeed() {
+        return new CalendarsFeed();
+    }
+
+    /*
+     * (non-Javadoc)
+     * @see com.google.wireless.gdata2.parser.xml.XmlGDataParser#createEntry()
+     */
+    protected Entry createEntry() {
+        return new CalendarEntry();
+    }
+
+    /*
+     * (non-Javadoc)
+     * @see XmlGDataParser#handleExtraElementInEntry
+     */
+    protected void handleExtraElementInEntry(Entry entry)
+        throws XmlPullParserException, IOException {
+
+        XmlPullParser parser = getParser();
+
+        if (!(entry instanceof CalendarEntry)) {
+            throw new IllegalArgumentException("Expected CalendarEntry!");
+        }
+        CalendarEntry calendarEntry = (CalendarEntry) entry;
+
+        // NOTE: all of these names are assumed to be in the "gcal" namespace.
+        // we do not bother checking that here.
+        String name = parser.getName();
+        if ("accesslevel".equals(name)) {
+            String accesslevelStr = parser.getAttributeValue(null /* ns */,
+                    "value");
+            byte accesslevel = CalendarEntry.ACCESS_READ;
+            if ("none".equals(accesslevelStr)) {
+                accesslevel = CalendarEntry.ACCESS_NONE;
+            } else if ("read".equals(accesslevelStr)) {
+                accesslevel = CalendarEntry.ACCESS_READ;
+            } else if ("freebusy".equals(accesslevelStr)) {
+                accesslevel = CalendarEntry.ACCESS_FREEBUSY;
+            } else if ("contributor".equals(accesslevelStr)) {
+                // contributor is the access level that used to be used, but it seems to have
+                // been deprecated in favor of "editor".
+                accesslevel = CalendarEntry.ACCESS_EDITOR;
+            } else if ("editor".equals(accesslevelStr)) {
+                accesslevel = CalendarEntry.ACCESS_EDITOR;
+            } else if ("owner".equals(accesslevelStr)) {
+                accesslevel = CalendarEntry.ACCESS_OWNER;
+            }
+            calendarEntry.setAccessLevel(accesslevel);
+        } else if ("color".equals(name)) {
+            String color =
+                parser.getAttributeValue(null /* ns */, "value");
+            calendarEntry.setColor(color);
+        } else if ("hidden".equals(name)) {
+            String hiddenStr =
+                parser.getAttributeValue(null /* ns */, "value");
+            boolean hidden = false;
+            if ("false".equals(hiddenStr)) {
+                hidden = false;
+            } else if ("true".equals(hiddenStr)) {
+                hidden = true;
+            }
+            calendarEntry.setHidden(hidden);
+            // if the calendar is hidden, it cannot be selected.
+            if (hidden) {
+                calendarEntry.setSelected(false);
+            }
+        } else if ("selected".equals(name)) {
+            String selectedStr =
+                parser.getAttributeValue(null /* ns */, "value");
+            boolean selected = false;
+            if ("false".equals(selectedStr)) {
+                selected = false;
+            } else if ("true".equals(selectedStr)) {
+                selected = true;
+            }
+            calendarEntry.setSelected(selected);
+        } else if ("timezone".equals(name)) {
+            String timezone =
+                parser.getAttributeValue(null /* ns */, "value");
+            calendarEntry.setTimezone(timezone);
+        }
+    }
+
+    /*
+     * (non-Javadoc)
+     * @see XmlGDataParser#handleExtraLinkInEntry
+     */
+    protected void handleExtraLinkInEntry(String rel,
+                                          String type,
+                                          String href,
+                                          Entry entry)
+        throws XmlPullParserException, IOException {
+        if (("alternate".equals(rel)) &&
+            ("application/atom+xml".equals(type))) {
+            CalendarEntry calendarEntry = (CalendarEntry) entry;
+            calendarEntry.setAlternateLink(href);
+        }
+    }
+}
diff --git a/src/com/google/wireless/gdata2/calendar/parser/xml/XmlEventsGDataParser.java b/src/com/google/wireless/gdata2/calendar/parser/xml/XmlEventsGDataParser.java
new file mode 100644
index 0000000..99543ba
--- /dev/null
+++ b/src/com/google/wireless/gdata2/calendar/parser/xml/XmlEventsGDataParser.java
@@ -0,0 +1,426 @@
+// Copyright 2007 The Android Open Source Project
+
+package com.google.wireless.gdata2.calendar.parser.xml;
+
+import com.google.wireless.gdata2.calendar.data.EventEntry;
+import com.google.wireless.gdata2.calendar.data.EventsFeed;
+import com.google.wireless.gdata2.calendar.data.When;
+import com.google.wireless.gdata2.calendar.data.Reminder;
+import com.google.wireless.gdata2.calendar.data.Who;
+import com.google.wireless.gdata2.data.Entry;
+import com.google.wireless.gdata2.data.Feed;
+import com.google.wireless.gdata2.data.StringUtils;
+import com.google.wireless.gdata2.data.XmlUtils;
+import com.google.wireless.gdata2.parser.ParseException;
+import com.google.wireless.gdata2.parser.xml.XmlGDataParser;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * GDataParser for an events feed containing events in a calendar.
+ */
+public class XmlEventsGDataParser extends XmlGDataParser {
+
+    // whether or not we've seen reminders directly under the entry.
+    // the calendar feed sends duplicate <reminder> entries in case of
+    // recurrences, if the recurrences are expanded.
+    // if the <reminder> elements precede the <when> elements, we'll only
+    // process the <reminder> elements directly under the entry and ignore
+    // the <reminder> elements within a <when>.
+    // if the <when> elements precede the <reminder> elements, we'll first
+    // process reminders under the when, and then we'll clear them and process
+    // the reminders directly under the entry (which should take precedence).
+    // if we only see <reminder> as direct children of the entry or only see
+    // <reminder> as children of <when> elements, there is no conflict.
+    private boolean hasSeenReminder = false;
+
+    /**
+     * Creates a new XmlEventsGDataParser.
+     * @param is The InputStream that should be parsed.
+     * @throws ParseException Thrown if a parser cannot be created.
+     */
+    public XmlEventsGDataParser(InputStream is, XmlPullParser parser)
+            throws ParseException {
+        super(is, parser);
+    }
+
+    /*
+     * (non-Javadoc)
+     * @see com.google.wireless.gdata2.parser.xml.XmlGDataParser#createFeed()
+     */
+    protected Feed createFeed() {
+        return new EventsFeed();
+    }
+
+    /*
+     * (non-Javadoc)
+     * @see com.google.wireless.gdata2.parser.xml.XmlGDataParser#createEntry()
+     */
+    protected Entry createEntry() {
+        return new EventEntry();
+    }
+
+    @Override
+    protected void handleEntry(Entry entry) throws XmlPullParserException,
+            IOException, ParseException {
+        hasSeenReminder = false; // Reset the state for the new entry
+        super.handleEntry(entry);
+    }
+
+    protected void handleExtraElementInFeed(Feed feed)
+            throws XmlPullParserException, IOException {
+        XmlPullParser parser = getParser();
+        if (!(feed instanceof EventsFeed)) {
+            throw new IllegalArgumentException("Expected EventsFeed!");
+        }
+        EventsFeed eventsFeed = (EventsFeed) feed;
+        String name = parser.getName();
+        if ("timezone".equals(name)) {
+            String timezone = parser.getAttributeValue(null /* ns */, "value");
+            if (!StringUtils.isEmpty(timezone)) {
+                eventsFeed.setTimezone(timezone);
+            }
+        }
+    }
+
+    /*
+     * (non-Javadoc)
+     * @see XmlGDataParser#handleExtraElementInEntry
+     */
+    protected void handleExtraElementInEntry(Entry entry)
+            throws XmlPullParserException, IOException, ParseException {
+
+        XmlPullParser parser = getParser();
+
+        if (!(entry instanceof EventEntry)) {
+            throw new IllegalArgumentException("Expected EventEntry!");
+        }
+        EventEntry eventEntry = (EventEntry) entry;
+
+        // NOTE: all of these names are assumed to be in the "gd" namespace.
+        // we do not bother checking that here.
+
+        String name = parser.getName();
+        if ("eventStatus".equals(name)) {
+            String eventStatusStr = parser.getAttributeValue(null, "value");
+            byte eventStatus = EventEntry.STATUS_TENTATIVE;
+            if ("http://schemas.google.com/g/2005#event.canceled".
+                    equals(eventStatusStr)) {
+                eventStatus = EventEntry.STATUS_CANCELED;
+            } else if ("http://schemas.google.com/g/2005#event.confirmed".
+                    equals(eventStatusStr)) {
+                eventStatus = EventEntry.STATUS_CONFIRMED;
+            } else if ("http://schemas.google.com/g/2005#event.tentative".
+                    equals(eventStatusStr)) {
+                eventStatus = EventEntry.STATUS_TENTATIVE;
+            }
+            eventEntry.setStatus(eventStatus);
+        } else if ("recurrence".equals(name)) {
+            String recurrence = XmlUtils.extractChildText(parser);
+            eventEntry.setRecurrence(recurrence);
+        } else if ("transparency".equals(name)) {
+            String transparencyStr = parser.getAttributeValue(null, "value");
+            byte transparency = EventEntry.TRANSPARENCY_OPAQUE;
+            if ("http://schemas.google.com/g/2005#event.opaque".
+                    equals(transparencyStr)) {
+                transparency = EventEntry.TRANSPARENCY_OPAQUE;
+            } else if ("http://schemas.google.com/g/2005#event.transparent".
+                    equals(transparencyStr)) {
+                transparency = EventEntry.TRANSPARENCY_TRANSPARENT;
+            }
+            eventEntry.setTransparency(transparency);
+        } else if ("visibility".equals(name)) {
+            String visibilityStr = parser.getAttributeValue(null, "value");
+            byte visibility = EventEntry.VISIBILITY_DEFAULT;
+            if ("http://schemas.google.com/g/2005#event.confidential".
+                    equals(visibilityStr)) {
+                visibility = EventEntry.VISIBILITY_CONFIDENTIAL;
+            } else if ("http://schemas.google.com/g/2005#event.default"
+                    .equals(visibilityStr)) {
+                visibility = EventEntry.VISIBILITY_DEFAULT;
+            } else if ("http://schemas.google.com/g/2005#event.private"
+                    .equals(visibilityStr)) {
+                visibility = EventEntry.VISIBILITY_PRIVATE;
+            } else if ("http://schemas.google.com/g/2005#event.public"
+                    .equals(visibilityStr)) {
+                visibility = EventEntry.VISIBILITY_PUBLIC;
+            }
+            eventEntry.setVisibility(visibility);
+        } else if ("who".equals(name)) {
+            handleWho(eventEntry);
+        } else if ("when".equals(name)) {
+            handleWhen(eventEntry);
+        } else if ("reminder".equals(name)) {
+            if (!hasSeenReminder) {
+                // if this is the first <reminder> we've seen directly under the
+                // entry, clear any previously seen reminders (under <when>s)
+                eventEntry.clearReminders();
+                hasSeenReminder = true;
+            }
+            handleReminder(eventEntry);
+        } else if ("originalEvent".equals(name)) {
+            handleOriginalEvent(eventEntry);
+        } else if ("where".equals(name)) {
+            String where = parser.getAttributeValue(null /* ns */,
+                    "valueString");
+            String rel = parser.getAttributeValue(null /* ns */,
+                    "rel");
+            if (StringUtils.isEmpty(rel) ||
+                    "http://schemas.google.com/g/2005#event".equals(rel)) {
+                eventEntry.setWhere(where);
+            }
+            // TODO: handle entryLink?
+        } else if ("feedLink".equals(name)) {
+            // TODO: check that the parent is a gd:comments            
+            String commentsUri = parser.getAttributeValue(null /* ns */, "href");
+            eventEntry.setCommentsUri(commentsUri);
+        } else if ("extendedProperty".equals(name)) {
+            String propertyName = parser.getAttributeValue(null /* ns */, "name");
+            String propertyValue = parser.getAttributeValue(null /* ns */, "value");
+            eventEntry.addExtendedProperty(propertyName, propertyValue);
+        }
+    }
+
+    private void handleWho(EventEntry eventEntry)
+            throws XmlPullParserException, IOException, ParseException {
+
+        XmlPullParser parser = getParser();
+
+        int eventType = parser.getEventType();
+        String name = parser.getName();
+
+        if (eventType != XmlPullParser.START_TAG ||
+                (!"who".equals(parser.getName()))) {
+            // should not happen.
+            throw new
+                    IllegalStateException("Expected <who>: Actual "
+                    + "element: <"
+                    + name + ">");
+        }
+
+        String email =
+                parser.getAttributeValue(null /* ns */, "email");
+        String relString =
+                parser.getAttributeValue(null /* ns */, "rel");
+        String value =
+                parser.getAttributeValue(null /* ns */, "valueString");
+
+        Who who = new Who();
+        who.setEmail(email);
+        who.setValue(value);
+        byte rel = Who.RELATIONSHIP_NONE;
+        if ("http://schemas.google.com/g/2005#event.attendee".equals(relString)) {
+            rel = Who.RELATIONSHIP_ATTENDEE;
+        } else if ("http://schemas.google.com/g/2005#event.organizer".equals(relString)) {
+            rel = Who.RELATIONSHIP_ORGANIZER;
+        } else if ("http://schemas.google.com/g/2005#event.performer".equals(relString)) {
+            rel = Who.RELATIONSHIP_PERFORMER;
+        } else if ("http://schemas.google.com/g/2005#event.speaker".equals(relString)) {
+            rel = Who.RELATIONSHIP_SPEAKER;
+        } else if (StringUtils.isEmpty(relString)) {
+            rel = Who.RELATIONSHIP_ATTENDEE;
+        } else {
+            throw new ParseException("Unexpected rel: " + relString);
+        }
+        who.setRelationship(rel);
+
+        eventEntry.addAttendee(who);
+
+        while (eventType != XmlPullParser.END_DOCUMENT) {
+            switch (eventType) {
+                case XmlPullParser.START_TAG:
+                    name = parser.getName();
+                    if ("attendeeStatus".equals(name)) {
+                        String statusString =
+                                parser.getAttributeValue(null /* ns */, "value");
+                        byte status = Who.STATUS_NONE;
+                        if ("http://schemas.google.com/g/2005#event.accepted".
+                                equals(statusString)) {
+                            status = Who.STATUS_ACCEPTED;
+                        } else if ("http://schemas.google.com/g/2005#event.declined".
+                                equals(statusString)) {
+                            status = Who.STATUS_DECLINED;
+                        } else if ("http://schemas.google.com/g/2005#event.invited".
+                                equals(statusString)) {
+                            status = Who.STATUS_INVITED;
+                        } else if ("http://schemas.google.com/g/2005#event.tentative".
+                                equals(statusString)) {
+                            status = Who.STATUS_TENTATIVE;
+                        } else if (StringUtils.isEmpty(statusString)) {
+                            status = Who.STATUS_TENTATIVE;
+                        } else {
+                            throw new ParseException("Unexpected status: " + statusString);
+                        }
+                        who.setStatus(status);
+                    } else if ("attendeeType".equals(name)) {
+                        String typeString= XmlUtils.extractChildText(parser);
+                        byte type = Who.TYPE_NONE;
+                        if ("http://schemas.google.com/g/2005#event.optional".equals(typeString)) {
+                            type = Who.TYPE_OPTIONAL;
+                        } else if ("http://schemas.google.com/g/2005#event.required".
+                                equals(typeString)) {
+                            type = Who.TYPE_REQUIRED;
+                        } else if (StringUtils.isEmpty(typeString)) {
+                            type = Who.TYPE_REQUIRED;
+                        } else {
+                            throw new ParseException("Unexpected type: " + typeString);
+                        }
+                        who.setType(type);
+                    }
+                    break;
+                case XmlPullParser.END_TAG:
+                    name = parser.getName();
+                    if ("who".equals(name)) {
+                        return;
+                    }
+                default:
+                    // ignore
+            }
+
+            eventType = parser.next();
+        }
+    }
+
+    private void handleWhen(EventEntry eventEntry)
+            throws XmlPullParserException, IOException {
+
+        XmlPullParser parser = getParser();
+
+        int eventType = parser.getEventType();
+        String name = parser.getName();
+
+        if (eventType != XmlPullParser.START_TAG ||
+                (!"when".equals(parser.getName()))) {
+            // should not happen.
+            throw new
+                    IllegalStateException("Expected <when>: Actual "
+                    + "element: <"
+                    + name + ">");
+        }
+
+        String startTime =
+                parser.getAttributeValue(null /* ns */, "startTime");
+        String endTime =
+                parser.getAttributeValue(null /* ns */, "endTime");
+
+        When when = new When(startTime, endTime);
+        eventEntry.addWhen(when);
+        boolean firstWhen = eventEntry.getWhens().size() == 1;
+        // we only parse reminders under the when if reminders have not already
+        // been handled (directly under the entry, or in a previous when for
+        // this entry)
+        boolean handleReminders = firstWhen && !hasSeenReminder;
+
+        eventType = parser.next();
+        while (eventType != XmlPullParser.END_DOCUMENT) {
+            switch (eventType) {
+                case XmlPullParser.START_TAG:
+                    name = parser.getName();
+                    if ("reminder".equals(name)) {
+                        // only want to store reminders on the first when.  they
+                        // should have the same values for all other instances.
+                        if (handleReminders) {
+                            handleReminder(eventEntry);
+                        }
+                    }
+                    break;
+                case XmlPullParser.END_TAG:
+                    name = parser.getName();
+                    if ("when".equals(name)) {
+                        return;
+                    }
+                default:
+                    // ignore
+            }
+
+            eventType = parser.next();
+        }
+    }
+
+    private void handleReminder(EventEntry eventEntry) {
+        XmlPullParser parser = getParser();
+
+        Reminder reminder = new Reminder();
+        eventEntry.addReminder(reminder);
+
+        String methodStr = parser.getAttributeValue(null /* ns */,
+                "method");
+        String minutesStr = parser.getAttributeValue(null /* ns */,
+                "minutes");
+        String hoursStr = parser.getAttributeValue(null /* ns */,
+                "hours");
+        String daysStr = parser.getAttributeValue(null /* ns */,
+                "days");
+
+        if (!StringUtils.isEmpty(methodStr)) {
+            if ("alert".equals(methodStr)) {
+                reminder.setMethod(Reminder.METHOD_ALERT);
+            } else if ("email".equals(methodStr)) {
+                reminder.setMethod(Reminder.METHOD_EMAIL);
+            } else if ("sms".equals(methodStr)) {
+                reminder.setMethod(Reminder.METHOD_SMS);
+            }
+        }
+
+        int minutes = Reminder.MINUTES_DEFAULT;
+        if (!StringUtils.isEmpty(minutesStr)) {
+            minutes = StringUtils.parseInt(minutesStr, minutes);
+        } else if (!StringUtils.isEmpty(hoursStr)) {
+            minutes = 60*StringUtils.parseInt(hoursStr, minutes);
+        } else if (!StringUtils.isEmpty(daysStr)) {
+            minutes = 24*60*StringUtils.parseInt(daysStr, minutes);
+        }
+        // TODO: support absolute times?
+        if (minutes < 0) {
+            minutes = Reminder.MINUTES_DEFAULT;
+        }
+        reminder.setMinutes(minutes);
+    }
+
+    private void handleOriginalEvent(EventEntry eventEntry)
+            throws XmlPullParserException, IOException {
+
+        XmlPullParser parser = getParser();
+
+        int eventType = parser.getEventType();
+        String name = parser.getName();
+
+        if (eventType != XmlPullParser.START_TAG ||
+                (!"originalEvent".equals(parser.getName()))) {
+            // should not happen.
+            throw new
+                    IllegalStateException("Expected <originalEvent>: Actual "
+                    + "element: <"
+                    + name + ">");
+        }
+
+        eventEntry.setOriginalEventId(
+                parser.getAttributeValue(null /* ns */, "href"));
+
+        eventType = parser.next();
+        while (eventType != XmlPullParser.END_DOCUMENT) {
+            switch (eventType) {
+                case XmlPullParser.START_TAG:
+                    name = parser.getName();
+                    if ("when".equals(name)) {
+                        eventEntry.setOriginalEventStartTime(
+                                parser.getAttributeValue(null/*ns*/, "startTime"));
+                    }
+                    break;
+                case XmlPullParser.END_TAG:
+                    name = parser.getName();
+                    if ("originalEvent".equals(name)) {
+                        return;
+                    }
+                default:
+                    // ignore
+            }
+
+            eventType = parser.next();
+        }
+    }
+}
diff --git a/src/com/google/wireless/gdata2/calendar/parser/xml/package.html b/src/com/google/wireless/gdata2/calendar/parser/xml/package.html
new file mode 100644
index 0000000..1c9bf9d
--- /dev/null
+++ b/src/com/google/wireless/gdata2/calendar/parser/xml/package.html
@@ -0,0 +1,5 @@
+<html>
+<body>
+    {@hide}
+</body>
+</html>
diff --git a/src/com/google/wireless/gdata2/calendar/serializer/package.html b/src/com/google/wireless/gdata2/calendar/serializer/package.html
new file mode 100644
index 0000000..1c9bf9d
--- /dev/null
+++ b/src/com/google/wireless/gdata2/calendar/serializer/package.html
@@ -0,0 +1,5 @@
+<html>
+<body>
+    {@hide}
+</body>
+</html>
diff --git a/src/com/google/wireless/gdata2/calendar/serializer/xml/XmlEventEntryGDataSerializer.java b/src/com/google/wireless/gdata2/calendar/serializer/xml/XmlEventEntryGDataSerializer.java
new file mode 100644
index 0000000..aabef79
--- /dev/null
+++ b/src/com/google/wireless/gdata2/calendar/serializer/xml/XmlEventEntryGDataSerializer.java
@@ -0,0 +1,399 @@
+// Copyright 2007 The Android Open Source Project
+
+package com.google.wireless.gdata2.calendar.serializer.xml;
+
+import com.google.wireless.gdata2.calendar.data.EventEntry;
+import com.google.wireless.gdata2.calendar.data.When;
+import com.google.wireless.gdata2.calendar.data.Reminder;
+import com.google.wireless.gdata2.calendar.data.Who;
+import com.google.wireless.gdata2.data.StringUtils;
+import com.google.wireless.gdata2.parser.xml.XmlGDataParser;
+import com.google.wireless.gdata2.parser.xml.XmlParserFactory;
+import com.google.wireless.gdata2.parser.ParseException;
+import com.google.wireless.gdata2.serializer.xml.XmlEntryGDataSerializer;
+
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.IOException;
+import java.util.Enumeration;
+import java.util.Hashtable;
+
+
+/**
+ *  Serializes Google Calendar event entries into the Atom XML format.
+ */
+// TODO: move all strings into constants.  share with parser?
+public class XmlEventEntryGDataSerializer extends XmlEntryGDataSerializer {
+
+    public static final String NAMESPACE_GCAL = "gCal";
+    public static final String NAMESPACE_GCAL_URI =
+            "http://schemas.google.com/gCal/2005";
+
+    public XmlEventEntryGDataSerializer(XmlParserFactory factory,
+            EventEntry entry) {
+        super(factory, entry);
+    }
+
+    protected EventEntry getEventEntry() {
+        return (EventEntry) getEntry();
+    }
+
+    protected void declareExtraEntryNamespaces(XmlSerializer serializer)
+            throws IOException {
+        serializer.setPrefix(NAMESPACE_GCAL, NAMESPACE_GCAL_URI);
+    }
+
+    /* (non-Javadoc)
+     * @see XmlEntryGDataSerializer#serializeExtraEntryContents
+     */
+    protected void serializeExtraEntryContents(XmlSerializer serializer,
+            int format)
+            throws IOException, ParseException {
+        EventEntry entry = getEventEntry();
+
+        serializeEventStatus(serializer, entry.getStatus());
+        serializeTransparency(serializer, entry.getTransparency());
+        serializeVisibility(serializer, entry.getVisibility());
+        Enumeration attendees = entry.getAttendees().elements();
+        while (attendees.hasMoreElements()) {
+            Who attendee = (Who) attendees.nextElement();
+            serializeWho(serializer, entry, attendee);
+        }
+
+        serializeRecurrence(serializer, entry.getRecurrence());
+        // either serialize reminders directly under the entry, or serialize
+        // whens (with reminders within the whens) -- should be just one.
+        if (entry.getRecurrence() != null) {
+            if (entry.getReminders() != null) {
+                Enumeration reminders = entry.getReminders().elements();
+                while (reminders.hasMoreElements()) {
+                    Reminder reminder = (Reminder) reminders.nextElement();
+                    serializeReminder(serializer, reminder);
+                }
+            }
+        } else {
+            Enumeration whens = entry.getWhens().elements();
+            while (whens.hasMoreElements()) {
+                When when = (When) whens.nextElement();
+                serializeWhen(serializer, entry, when);
+            }
+        }
+        serializeOriginalEvent(serializer,
+                entry.getOriginalEventId(),
+                entry.getOriginalEventStartTime());
+        serializeWhere(serializer, entry.getWhere());
+
+        serializeCommentsUri(serializer, entry.getCommentsUri());
+
+        Hashtable extendedProperties = entry.getExtendedProperties();
+        if (extendedProperties != null) {
+            Enumeration propertyNames = extendedProperties.keys();
+            while (propertyNames.hasMoreElements()) {
+                String propertyName = (String) propertyNames.nextElement();
+                String propertyValue = (String) extendedProperties.get(propertyName);
+                serializeExtendedProperty(serializer, propertyName, propertyValue);
+            }
+        }
+    }
+
+    private static void serializeEventStatus(XmlSerializer serializer,
+            byte status)
+            throws IOException {
+
+        String statusString;
+
+        switch (status) {
+            case EventEntry.STATUS_TENTATIVE:
+                statusString = "http://schemas.google.com/g/2005#event.tentative";
+                break;
+            case EventEntry.STATUS_CANCELED:
+                statusString = "http://schemas.google.com/g/2005#event.canceled";
+                break;
+            case EventEntry.STATUS_CONFIRMED:
+                statusString = "http://schemas.google.com/g/2005#event.confirmed";
+                break;
+            default:
+                // should not happen
+                // TODO: log this
+                statusString = "http://schemas.google.com/g/2005#event.tentative";
+        }
+
+        serializer.startTag(XmlGDataParser.NAMESPACE_GD_URI, "eventStatus");
+        serializer.attribute(null /* ns */, "value", statusString);
+        serializer.endTag(XmlGDataParser.NAMESPACE_GD_URI, "eventStatus");
+    }
+
+    private static void serializeRecurrence(XmlSerializer serializer,
+            String recurrence)
+            throws IOException {
+        if (StringUtils.isEmpty(recurrence)) {
+            return;
+        }
+        serializer.startTag(XmlGDataParser.NAMESPACE_GD_URI, "recurrence");
+        serializer.text(recurrence);
+        serializer.endTag(XmlGDataParser.NAMESPACE_GD_URI, "recurrence");
+    }
+
+    private static void serializeTransparency(XmlSerializer serializer,
+            byte transparency)
+            throws IOException {
+
+        String transparencyString;
+
+        switch (transparency) {
+            case EventEntry.TRANSPARENCY_OPAQUE:
+                transparencyString =
+                        "http://schemas.google.com/g/2005#event.opaque";
+                break;
+            case EventEntry.TRANSPARENCY_TRANSPARENT:
+                transparencyString =
+                        "http://schemas.google.com/g/2005#event.transparent";
+                break;
+            default:
+                // should not happen
+                // TODO: log this
+                transparencyString =
+                        "http://schemas.google.com/g/2005#event.transparent";
+        }
+
+        serializer.startTag(XmlGDataParser.NAMESPACE_GD_URI, "transparency");
+        serializer.attribute(null /* ns */, "value", transparencyString);
+        serializer.endTag(XmlGDataParser.NAMESPACE_GD_URI, "transparency");
+    }
+
+
+    private static void serializeVisibility(XmlSerializer serializer,
+            byte visibility)
+            throws IOException {
+
+        String visibilityString;
+
+        switch (visibility) {
+            case EventEntry.VISIBILITY_DEFAULT:
+                visibilityString = "http://schemas.google.com/g/2005#event.default";
+                break;
+            case EventEntry.VISIBILITY_CONFIDENTIAL:
+                visibilityString =
+                        "http://schemas.google.com/g/2005#event.confidential";
+                break;
+            case EventEntry.VISIBILITY_PRIVATE:
+                visibilityString = "http://schemas.google.com/g/2005#event.private";
+                break;
+            case EventEntry.VISIBILITY_PUBLIC:
+                visibilityString = "http://schemas.google.com/g/2005#event.public";
+                break;
+            default:
+                // should not happen
+                // TODO: log this
+                visibilityString = "http://schemas.google.com/g/2005#event.default";
+        }
+
+        serializer.startTag(XmlGDataParser.NAMESPACE_GD_URI, "visibility");
+        serializer.attribute(null /* ns */, "value", visibilityString);
+        serializer.endTag(XmlGDataParser.NAMESPACE_GD_URI, "visibility");
+    }
+
+    private static void serializeWho(XmlSerializer serializer,
+            EventEntry entry,
+            Who who)
+            throws IOException, ParseException {
+        serializer.startTag(XmlGDataParser.NAMESPACE_GD_URI, "who");
+        String email = who.getEmail();
+        if (!StringUtils.isEmpty(email)) {
+            serializer.attribute(null /* ns */, "email", email);
+        }
+
+        String value = who.getValue();
+        if (!StringUtils.isEmpty(value)) {
+            serializer.attribute(null /* ns */, "valueString", value);
+        }
+
+        String rel = null;
+        switch (who.getRelationship()) {
+            case Who.RELATIONSHIP_NONE:
+                break;
+            case Who.RELATIONSHIP_ATTENDEE:
+                rel = "http://schemas.google.com/g/2005#event.attendee";
+                break;
+            case Who.RELATIONSHIP_ORGANIZER:
+                rel = "http://schemas.google.com/g/2005#event.organizer";
+                break;
+            case Who.RELATIONSHIP_PERFORMER:
+                rel = "http://schemas.google.com/g/2005#event.performer";
+                break;
+            case Who.RELATIONSHIP_SPEAKER:
+                rel = "http://schemas.google.com/g/2005#event.speaker";
+                break;
+            default:
+                throw new ParseException("Unexpected rel: " + who.getRelationship());
+        }
+        if (!StringUtils.isEmpty(rel)) {
+            serializer.attribute(null /* ns */, "rel", rel);
+        }
+
+        String status = null;
+        switch (who.getStatus()) {
+            case Who.STATUS_NONE:
+                break;
+            case Who.STATUS_ACCEPTED:
+                status = "http://schemas.google.com/g/2005#event.accepted";
+                break;
+            case Who.STATUS_DECLINED:
+                status = "http://schemas.google.com/g/2005#event.declined";
+                break;
+            case Who.STATUS_INVITED:
+                status = "http://schemas.google.com/g/2005#event.invited";
+                break;
+            case Who.STATUS_TENTATIVE:
+                status = "http://schemas.google.com/g/2005#event.tentative";
+                break;
+            default:
+                throw new ParseException("Unexpected status: " + who.getStatus());
+        }
+        if (!StringUtils.isEmpty(status)) {
+            serializer.startTag(XmlGDataParser.NAMESPACE_GD_URI,
+                    "attendeeStatus");
+            serializer.attribute(null /* ns */, "value", status);
+            serializer.endTag(XmlGDataParser.NAMESPACE_GD_URI,
+                    "attendeeStatus");
+        }
+
+        String type = null;
+        switch (who.getType()) {
+            case Who.TYPE_NONE:
+                break;
+            case Who.TYPE_REQUIRED:
+                type = "http://schemas.google.com/g/2005#event.required";
+                break;
+            case Who.TYPE_OPTIONAL:
+                type = "http://schemas.google.com/g/2005#event.optional";
+                break;
+            default:
+                throw new ParseException("Unexpected type: " + who.getType());
+        }
+        if (!StringUtils.isEmpty(type)) {
+            serializer.startTag(XmlGDataParser.NAMESPACE_GD_URI,
+                    "attendeeType");
+            serializer.attribute(null /* ns */, "value", type);
+            serializer.endTag(XmlGDataParser.NAMESPACE_GD_URI, "attendeeType");
+        }
+        serializer.endTag(XmlGDataParser.NAMESPACE_GD_URI, "who");
+    }
+
+    private static void serializeWhen(XmlSerializer serializer,
+            EventEntry entry,
+            When when)
+            throws IOException {
+        // TODO: throw exn if startTime is empty but endTime is not?
+        String startTime = when.getStartTime();
+        String endTime = when.getEndTime();
+        if (StringUtils.isEmpty(when.getStartTime())) {
+            return;
+        }
+
+        serializer.startTag(XmlGDataParser.NAMESPACE_GD_URI, "when");
+        serializer.attribute(null /* ns */, "startTime", startTime);
+        if (!StringUtils.isEmpty(endTime)) {
+            serializer.attribute(null /* ns */, "endTime", endTime);
+        }
+        if (entry.getReminders() != null) {
+            Enumeration reminders = entry.getReminders().elements();
+            while (reminders.hasMoreElements()) {
+                Reminder reminder = (Reminder) reminders.nextElement();
+                serializeReminder(serializer, reminder);
+            }
+        }
+        serializer.endTag(XmlGDataParser.NAMESPACE_GD_URI, "when");
+    }
+
+    private static void serializeReminder(XmlSerializer serializer,
+            Reminder reminder)
+            throws IOException {
+
+        serializer.startTag(XmlGDataParser.NAMESPACE_GD_URI, "reminder");
+        byte method = reminder.getMethod();
+        String methodStr = null;
+        switch (method) {
+            case Reminder.METHOD_ALERT:
+                methodStr = "alert";
+                break;
+            case Reminder.METHOD_EMAIL:
+                methodStr = "email";
+                break;
+            case Reminder.METHOD_SMS:
+                methodStr = "sms";
+                break;
+        }
+        if (methodStr != null) {
+            serializer.attribute(null /* ns */, "method", methodStr);
+        }
+
+        int minutes = reminder.getMinutes();
+        if (minutes != Reminder.MINUTES_DEFAULT) {
+            serializer.attribute(null /* ns */, "minutes",
+                    Integer.toString(minutes));
+        }
+
+        serializer.endTag(XmlGDataParser.NAMESPACE_GD_URI, "reminder");
+    }
+
+    private static void serializeOriginalEvent(XmlSerializer serializer,
+            String originalEventId,
+            String originalEventTime)
+            throws IOException {
+        if (StringUtils.isEmpty(originalEventId) ||
+                StringUtils.isEmpty(originalEventTime)) {
+            return;
+        }
+
+
+        serializer.startTag(XmlGDataParser.NAMESPACE_GD_URI, "originalEvent");
+        int index = originalEventId.lastIndexOf("/");
+        if (index != -1) {
+            String id = originalEventId.substring(index + 1);
+            if (!StringUtils.isEmpty(id)) {
+                serializer.attribute(null /* ns */, "id", id);
+            }
+        }
+        serializer.attribute(null /* ns */, "href", originalEventId);
+        serializer.startTag(XmlGDataParser.NAMESPACE_GD_URI, "when");
+        serializer.attribute(null /* ns */, "startTime", originalEventTime);
+        serializer.endTag(XmlGDataParser.NAMESPACE_GD_URI, "when");
+        serializer.endTag(XmlGDataParser.NAMESPACE_GD_URI, "originalEvent");
+    }
+
+
+    private static void serializeWhere(XmlSerializer serializer,
+            String where)
+            throws IOException {
+        if (StringUtils.isEmpty(where)) {
+            return;
+        }
+
+        serializer.startTag(XmlGDataParser.NAMESPACE_GD_URI, "where");
+        serializer.attribute(null /* ns */, "valueString", where);
+        serializer.endTag(XmlGDataParser.NAMESPACE_GD_URI, "where");
+    }
+
+    private static void serializeCommentsUri(XmlSerializer serializer,
+            String commentsUri)
+            throws IOException {
+        if (commentsUri == null) {
+            return;
+        }
+
+        serializer.startTag(XmlGDataParser.NAMESPACE_GD_URI, "feedLink");
+        serializer.attribute(null /* ns */, "href", commentsUri);
+        serializer.endTag(XmlGDataParser.NAMESPACE_GD_URI, "feedLink");
+    }
+
+    private static void serializeExtendedProperty(XmlSerializer serializer,
+            String name,
+            String value)
+            throws IOException {
+        serializer.startTag(XmlGDataParser.NAMESPACE_GD_URI, "extendedProperty");
+        serializer.attribute(null /* ns */, "name", name);
+        serializer.attribute(null /* ns */, "value", value);
+        serializer.endTag(XmlGDataParser.NAMESPACE_GD_URI, "extendedProperty");
+    }
+}
diff --git a/src/com/google/wireless/gdata2/calendar/serializer/xml/package.html b/src/com/google/wireless/gdata2/calendar/serializer/xml/package.html
new file mode 100644
index 0000000..1c9bf9d
--- /dev/null
+++ b/src/com/google/wireless/gdata2/calendar/serializer/xml/package.html
@@ -0,0 +1,5 @@
+<html>
+<body>
+    {@hide}
+</body>
+</html>
diff --git a/src/com/google/wireless/gdata2/client/GDataClient.java b/src/com/google/wireless/gdata2/client/GDataClient.java
new file mode 100644
index 0000000..0568f71
--- /dev/null
+++ b/src/com/google/wireless/gdata2/client/GDataClient.java
@@ -0,0 +1,149 @@
+// Copyright 2007 The Android Open Source Project
+
+package com.google.wireless.gdata2.client;
+
+import com.google.wireless.gdata2.serializer.GDataSerializer;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Interface for interacting with a GData server.  Specific platforms can
+ * provide their own implementations using the available networking and HTTP
+ * stack for that platform.
+ */
+public interface GDataClient {
+
+    /**
+     * Closes this GDataClient, cleaning up any resources, persistent connections, etc.,
+     * it may have.
+     */
+    void close();
+
+    /**
+     * URI encodes the supplied uri (using UTF-8).
+     * @param uri The uri that should be encoded.
+     * @return The encoded URI.
+     */
+    // TODO: get rid of this, if we write our own URI encoding library.
+    String encodeUri(String uri);
+
+    /**
+     * Creates a new QueryParams that should be used to restrict the feed
+     * contents that are fetched.
+     * @return A new QueryParams.
+     */
+    // TODO: get rid of this, if we write a generic QueryParams that can encode
+    // querystring params/values.
+    QueryParams createQueryParams();
+
+    /**
+     * Connects to a GData server (specified by the feedUrl) and fetches the
+     * specified feed as an InputStream.  The caller is responsible for calling
+     * {@link InputStream#close()} on the returned {@link InputStream}.
+     *
+     * @param feedUrl The feed that should be fetched.
+     * @param authToken The authentication token that should be used when
+     * fetching the feed.
+     * @return An InputStream for the feed.
+     * @throws IOException Thrown if an io error occurs while communicating with
+     * the service.
+     * @throws HttpException if the service returns an error response.
+     */
+    InputStream getFeedAsStream(String feedUrl,
+                                String authToken)
+        throws HttpException, IOException;
+
+    /**
+     * Connects to a GData server (specified by the mediaEntryUrl) and fetches the
+     * specified media entry as an InputStream.  The caller is responsible for calling
+     * {@link InputStream#close()} on the returned {@link InputStream}.
+     *
+     * @param mediaEntryUrl The media entry that should be fetched.
+     * @param authToken The authentication token that should be used when
+     * fetching the media entry.
+     * @return An InputStream for the media entry.
+     * @throws IOException Thrown if an io error occurs while communicating with
+     * the service.
+     * @throws HttpException if the service returns an error response.
+     */
+    InputStream getMediaEntryAsStream(String mediaEntryUrl, String authToken)
+        throws HttpException, IOException;
+
+    // TODO: support batch update
+
+    /**
+     * Connects to a GData server (specified by the feedUrl) and creates a new
+     * entry.  The response from the server is returned as an 
+     * {@link InputStream}.  The caller is responsible for calling
+     * {@link InputStream#close()} on the returned {@link InputStream}.
+     * 
+     * @param feedUrl The feed url where the entry should be created.
+     * @param authToken The authentication token that should be used when 
+     * creating the entry.
+     * @param entry The entry that should be created.
+     * @throws IOException Thrown if an io error occurs while communicating with
+     * the service.
+     * @throws HttpException if the service returns an error response.
+     */
+    InputStream createEntry(String feedUrl,
+                            String authToken,
+                            GDataSerializer entry)
+        throws HttpException, IOException;
+
+    /**
+     * Connects to a GData server (specified by the editUri) and updates an
+     * existing entry.  The response from the server is returned as an 
+     * {@link InputStream}.  The caller is responsible for calling
+     * {@link InputStream#close()} on the returned {@link InputStream}.
+     * 
+     * @param editUri The edit uri that should be used for updating the entry.
+     * @param authToken The authentication token that should be used when 
+     * updating the entry.
+     * @param entry The entry that should be updated.
+     * @throws IOException Thrown if an io error occurs while communicating with
+     * the service.
+     * @throws HttpException if the service returns an error response.
+     */
+    InputStream updateEntry(String editUri,
+                            String authToken,
+                            GDataSerializer entry)
+        throws HttpException, IOException;
+
+    /**
+     * Connects to a GData server (specified by the editUri) and deletes an
+     * existing entry.
+     * 
+     * @param editUri The edit uri that should be used for deleting the entry.
+     * @param authToken The authentication token that should be used when
+     * deleting the entry.
+     * @throws IOException Thrown if an io error occurs while communicating with
+     * the service.
+     * @throws HttpException if the service returns an error response.
+     */
+    void deleteEntry(String editUri,
+                     String authToken)
+        throws HttpException, IOException;
+
+    /**
+     * Connects to a GData server (specified by the editUri) and updates an
+     * existing media entry.  The response from the server is returned as an
+     * {@link InputStream}.  The caller is responsible for calling
+     * {@link InputStream#close()} on the returned {@link InputStream}.
+     *
+     * @param editUri The edit uri that should be used for updating the entry.
+     * @param authToken The authentication token that should be used when
+     * updating the entry.
+     * @param mediaEntryInputStream The {@link InputStream} that contains the new
+     *   value of the resource
+     * @param contentType The contentType of the new media entry
+     * @throws IOException Thrown if an io error occurs while communicating with
+     * the service.
+     * @throws HttpException if the service returns an error response.
+     * @return The {@link InputStream} that contains the metadata associated with the
+     *   new version of the media entry.
+     */
+    public InputStream updateMediaEntry(String editUri, String authToken,
+            InputStream mediaEntryInputStream, String contentType)
+        throws HttpException, IOException;
+}
diff --git a/src/com/google/wireless/gdata2/client/GDataParserFactory.java b/src/com/google/wireless/gdata2/client/GDataParserFactory.java
new file mode 100644
index 0000000..56546fe
--- /dev/null
+++ b/src/com/google/wireless/gdata2/client/GDataParserFactory.java
@@ -0,0 +1,48 @@
+// Copyright 2007 The Android Open Source Project
+
+package com.google.wireless.gdata2.client;
+
+import com.google.wireless.gdata2.parser.GDataParser;
+import com.google.wireless.gdata2.parser.ParseException;
+import com.google.wireless.gdata2.data.Entry;
+import com.google.wireless.gdata2.serializer.GDataSerializer;
+
+import java.io.InputStream;
+
+/**
+ * Factory that creates {@link GDataParser}s and {@link GDataSerializer}s.
+ */
+public interface GDataParserFactory {
+  /**
+   * Creates a new {@link GDataParser} for the provided InputStream.
+   *
+   * @param entryClass Specify the class of Entry objects that are to be parsed. This
+   *   lets createParser know which parser to create. 
+   * @param is The InputStream that should be parsed. @return The GDataParser that will parse is.
+   * @throws ParseException Thrown if the GDataParser could not be created.
+   * @throws IllegalArgumentException if the feed type is unknown.
+   */
+  GDataParser createParser(Class entryClass, InputStream is)
+      throws ParseException;
+
+  /**
+   * Creates a new {@link GDataParser} for the provided InputStream, using the
+   * default feed type for the client.
+   *
+   * @param is The InputStream that should be parsed.
+   * @return The GDataParser that will parse is.
+   * @throws ParseException Thrown if the GDataParser could not be created.
+   *         Note that this can occur if the feed in the InputStream is not of
+   *         the default type assumed by this method.
+   * @see #createParser(Class,InputStream)
+   */
+  GDataParser createParser(InputStream is) throws ParseException;
+
+  /**
+   * Creates a new {@link GDataSerializer} for the provided Entry.
+   *
+   * @param entry The Entry that should be serialized.
+   * @return The GDataSerializer that will serialize entry.
+   */
+  GDataSerializer createSerializer(Entry entry);
+}
diff --git a/src/com/google/wireless/gdata2/client/GDataServiceClient.java b/src/com/google/wireless/gdata2/client/GDataServiceClient.java
new file mode 100644
index 0000000..738879e
--- /dev/null
+++ b/src/com/google/wireless/gdata2/client/GDataServiceClient.java
@@ -0,0 +1,209 @@
+// Copyright 2008 The Android Open Source Project
+
+package com.google.wireless.gdata2.client;
+
+import com.google.wireless.gdata2.data.Entry;
+import com.google.wireless.gdata2.data.MediaEntry;
+import com.google.wireless.gdata2.data.StringUtils;
+import com.google.wireless.gdata2.parser.GDataParser;
+import com.google.wireless.gdata2.parser.ParseException;
+import com.google.wireless.gdata2.serializer.GDataSerializer;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Abstract base class for service-specific clients to access GData feeds.
+ */
+public abstract class GDataServiceClient {
+    private final GDataClient gDataClient;
+    private final GDataParserFactory gDataParserFactory;
+
+    public GDataServiceClient(GDataClient gDataClient,
+                              GDataParserFactory gDataParserFactory) {
+        this.gDataClient = gDataClient;
+        this.gDataParserFactory = gDataParserFactory;
+    }
+
+    /**
+     * Returns the {@link GDataClient} being used by this GDataServiceClient.
+     * @return The {@link GDataClient} being used by this GDataServiceClient.
+     */
+    protected GDataClient getGDataClient() {
+        return gDataClient;
+    }
+
+    /**
+     * Returns the {@link GDataParserFactory} being used by this
+     * GDataServiceClient.
+     * @return The {@link GDataParserFactory} being used by this
+     * GDataServiceClient.
+     */
+    protected GDataParserFactory getGDataParserFactory() {
+        return gDataParserFactory;
+    }
+
+    /**
+     * Returns the name of the service.  Used for authentication.
+     * @return The name of the service.
+     */
+    public abstract String getServiceName();
+
+    /**
+     * Creates {@link QueryParams} that can be used to restrict the feed
+     * contents that are fetched.
+     * @return The QueryParams that can be used with this client.
+     */
+    public QueryParams createQueryParams() {
+        return gDataClient.createQueryParams();
+    }
+
+    /**
+     * Fetches a feed for this user.  The caller is responsible for closing the
+     * returned {@link GDataParser}.
+     *
+     * @param feedEntryClass the class of Entry that is contained in the feed
+     * @param feedUrl ThAe URL of the feed that should be fetched.
+     * @param authToken The authentication token for this user.
+     * @return A {@link GDataParser} for the requested feed.
+     * @throws ParseException Thrown if the server response cannot be parsed.
+     * @throws IOException Thrown if an error occurs while communicating with
+     * the GData service.
+     * @throws HttpException Thrown if the http response contains a result other than 2xx
+     */
+    public GDataParser getParserForFeed(Class feedEntryClass, String feedUrl, String authToken)
+            throws ParseException, IOException, HttpException {
+        InputStream is = gDataClient.getFeedAsStream(feedUrl, authToken);
+        return gDataParserFactory.createParser(feedEntryClass, is);
+    }
+
+    /**
+     * Fetches a media entry as an InputStream.  The caller is responsible for closing the
+     * returned {@link InputStream}.
+     *
+     * @param mediaEntryUrl The URL of the media entry that should be fetched.
+     * @param authToken The authentication token for this user.
+     * @return A {@link InputStream} for the requested media entry.
+     * @throws IOException Thrown if an error occurs while communicating with
+     * the GData service.
+     */
+    public InputStream getMediaEntryAsStream(String mediaEntryUrl, String authToken)
+            throws IOException, HttpException {
+        return gDataClient.getMediaEntryAsStream(mediaEntryUrl, authToken);
+    }
+
+    /**
+     * Creates a new entry at the provided feed.  Parses the server response
+     * into the version of the entry stored on the server.
+     *
+     * @param feedUrl The feed where the entry should be created.
+     * @param authToken The authentication token for this user.
+     * @param entry The entry that should be created.
+     * @return The entry returned by the server as a result of creating the
+     * provided entry.
+     * @throws ParseException Thrown if the server response cannot be parsed.
+     * @throws IOException Thrown if an error occurs while communicating with
+     * the GData service.
+     * @throws HttpException if the service returns an error response
+     */
+    public Entry createEntry(String feedUrl, String authToken, Entry entry)
+            throws ParseException, IOException, HttpException {
+        GDataSerializer serializer = gDataParserFactory.createSerializer(entry);
+        InputStream is = gDataClient.createEntry(feedUrl, authToken, serializer);
+        return parseEntry(entry.getClass(), is);
+    }
+
+  /**
+   * Fetches an existing entry.
+   * @param entryClass the type of entry to expect
+   * @param id of the entry to fetch.
+   * @param authToken The authentication token for this user. @return The entry returned by the server.
+   * @throws ParseException Thrown if the server response cannot be parsed.
+   * @throws HttpException if the service returns an error response
+   * @throws IOException Thrown if an error occurs while communicating with
+   * the GData service.
+   * @return The entry returned by the server
+   */
+    public Entry getEntry(Class entryClass, String id, String authToken)
+          throws ParseException, IOException, HttpException {
+        InputStream is = getGDataClient().getFeedAsStream(id, authToken);
+        return parseEntry(entryClass, is);
+    }
+
+    /**
+     * Updates an existing entry.  Parses the server response into the version
+     * of the entry stored on the server.
+     *
+     * @param entry The entry that should be updated.
+     * @param authToken The authentication token for this user.
+     * @return The entry returned by the server as a result of updating the
+     * provided entry.
+     * @throws ParseException Thrown if the server response cannot be parsed.
+     * @throws IOException Thrown if an error occurs while communicating with
+     * the GData service.
+     * @throws HttpException if the service returns an error response
+     */
+    public Entry updateEntry(Entry entry, String authToken)
+            throws ParseException, IOException, HttpException {
+        String editUri = entry.getEditUri();
+        if (StringUtils.isEmpty(editUri)) {
+            throw new ParseException("No edit URI -- cannot update.");
+        }
+
+        GDataSerializer serializer = gDataParserFactory.createSerializer(entry);
+        InputStream is = gDataClient.updateEntry(editUri, authToken, serializer);
+        return parseEntry(entry.getClass(), is);
+    }
+
+    /**
+     * Updates an existing entry.  Parses the server response into the metadata
+     * of the entry stored on the server.
+     *
+     * @param editUri The URI of the resource that should be updated.
+     * @param inputStream The {@link java.io.InputStream} that contains the new value
+     *   of the media entry
+     * @param contentType The content type of the new media entry
+     * @param authToken The authentication token for this user.
+     * @return The entry returned by the server as a result of updating the
+     * provided entry.
+     * @throws HttpException if the service returns an error response
+     * @throws ParseException Thrown if the server response cannot be parsed.
+     * @throws IOException Thrown if an error occurs while communicating with
+     * the GData service.
+     */
+    public MediaEntry updateMediaEntry(String editUri, InputStream inputStream, String contentType,
+            String authToken) throws IOException, HttpException, ParseException {
+        if (StringUtils.isEmpty(editUri)) {
+            throw new IllegalArgumentException("No edit URI -- cannot update.");
+        }
+
+        InputStream is = gDataClient.updateMediaEntry(editUri, authToken, inputStream, contentType);
+        return (MediaEntry)parseEntry(MediaEntry.class, is);
+    }
+
+    /**
+     * Deletes an existing entry.
+     *
+     * @param editUri The editUri for the entry that should be deleted.
+     * @param authToken The authentication token for this user.
+     * @throws IOException Thrown if an error occurs while communicating with
+     * the GData service.
+     * @throws HttpException if the service returns an error response
+     */
+    public void deleteEntry(String editUri, String authToken)
+            throws IOException, HttpException {
+        gDataClient.deleteEntry(editUri, authToken);
+    }
+
+    private Entry parseEntry(Class entryClass, InputStream is) throws ParseException, IOException {
+        GDataParser parser = null;
+        try {
+            parser = gDataParserFactory.createParser(entryClass, is);
+            return parser.parseStandaloneEntry();
+        } finally {
+            if (parser != null) {
+                parser.close();
+            }
+        }
+    }
+}
diff --git a/src/com/google/wireless/gdata2/client/HttpException.java b/src/com/google/wireless/gdata2/client/HttpException.java
new file mode 100644
index 0000000..ad5dd92
--- /dev/null
+++ b/src/com/google/wireless/gdata2/client/HttpException.java
@@ -0,0 +1,58 @@
+// Copyright 2007 The Android Open Source Project
+
+package com.google.wireless.gdata2.client;
+
+import java.io.InputStream;
+
+/**
+ * A class representing exceptional (i.e., non 200) responses from an HTTP
+ * Server.
+ */
+public class HttpException extends Exception {
+
+  public static final int SC_BAD_REQUEST = 400;
+
+  public static final int SC_UNAUTHORIZED = 401;
+
+  public static final int SC_FORBIDDEN = 403;
+
+  public static final int SC_NOT_FOUND = 404;
+
+  public static final int SC_CONFLICT = 409;
+
+  public static final int SC_GONE = 410;
+
+  public static final int SC_INTERNAL_SERVER_ERROR = 500;
+
+  private final int statusCode;
+
+  private final InputStream responseStream;
+
+  /**
+   * Creates an HttpException with the given message, statusCode and
+   * responseStream.
+   */
+  //TODO: also record response headers?
+  public HttpException(String message, int statusCode,
+      InputStream responseStream) {
+    super(message);
+    this.statusCode = statusCode;
+    this.responseStream = responseStream;
+  }
+
+  /**
+   * Gets the status code associated with this exception.
+   * @return the status code returned by the server, typically one of the SC_*
+   * constants.
+   */
+  public int getStatusCode() {
+    return statusCode;
+  }
+
+  /**
+   * @return the error response stream from the server.
+   */
+  public InputStream getResponseStream() {
+    return responseStream;
+  }
+}
diff --git a/src/com/google/wireless/gdata2/client/HttpQueryParams.java b/src/com/google/wireless/gdata2/client/HttpQueryParams.java
new file mode 100644
index 0000000..bfd9eaf
--- /dev/null
+++ b/src/com/google/wireless/gdata2/client/HttpQueryParams.java
@@ -0,0 +1,73 @@
+// Copyright 2007 The Android Open Source Project
+
+package com.google.wireless.gdata2.client;
+
+import java.util.Hashtable;
+import java.util.Vector;
+
+/**
+ * A concrete implementation of QueryParams that uses the encodeUri method of a
+ * GDataClient (passed in at construction time) to URL encode parameters.
+ *
+ * This implementation maintains the order of parameters, which is useful for
+ * testing.  Instances of this class are not thread safe.
+ */
+public class HttpQueryParams extends QueryParams {
+
+  private GDataClient client;
+
+  /* Used to store the mapping of names to values */
+  private Hashtable params;
+
+  /* Used to maintain the order of parameter additions */
+  private Vector names;
+
+  /**
+   * Constructs a new, empty HttpQueryParams.
+   *
+   * @param client GDataClient whose encodeUri method is used for URL encoding.
+   */
+  public HttpQueryParams(GDataClient client) {
+    this.client = client;
+    // We expect most queries to have a relatively small number of parameters.
+    names = new Vector(4);
+    params = new Hashtable(7);
+  }
+
+  public String generateQueryUrl(String feedUrl) {
+    StringBuffer url = new StringBuffer(feedUrl);
+    url.append(feedUrl.indexOf('?') >= 0 ? '&' : '?');
+
+    for (int i = 0; i < names.size(); i++) {
+      if (i > 0) {
+        url.append('&');
+      }
+      String name = (String) names.elementAt(i);
+      url.append(client.encodeUri(name)).append('=');
+      url.append(client.encodeUri(getParamValue(name)));
+    }
+    return url.toString();
+  }
+
+  public String getParamValue(String param) {
+    return (String) params.get(param);
+  }
+
+  public void setParamValue(String param, String value) {
+    if (value != null) {
+      if (!params.containsKey(param)) {
+        names.addElement(param);
+      }
+      params.put(param, value);
+    } else {
+      if (params.remove(param) != null) {
+        names.removeElement(param);
+      }
+    }
+  }
+
+  public void clear() {
+    names.removeAllElements();
+    params.clear();
+  }
+}
diff --git a/src/com/google/wireless/gdata2/client/QueryParams.java b/src/com/google/wireless/gdata2/client/QueryParams.java
new file mode 100644
index 0000000..503f7f5
--- /dev/null
+++ b/src/com/google/wireless/gdata2/client/QueryParams.java
@@ -0,0 +1,240 @@
+// Copyright 2007 The Android Open Source Project
+
+package com.google.wireless.gdata2.client;
+
+/**
+ * Class for specifying parameters and constraints for a GData feed.
+ * These are used to modify the feed URL and add querystring parameters to the
+ * feed URL.
+ *
+ * Note that if an entry ID has been set, no other query params can be set.
+ *
+ * @see QueryParams#generateQueryUrl(String)
+ */
+// TODO: add support for projections?
+// TODO: add support for categories?
+public abstract class QueryParams {
+
+    /**
+     * Param name constant for a search query.
+     */
+    public static final String QUERY_PARAM = "q";
+
+    /**
+     * Param name constant for filtering by author.
+     */
+    public static final String AUTHOR_PARAM = "author";
+
+    /**
+     * Param name constant for alternate representations of GData.
+     */
+    public static final String ALT_PARAM = "alt";
+    public static final String ALT_RSS = "rss";
+    public static final String ALT_JSON = "json";
+
+    /**
+     * Param name constant for the updated min.
+     */
+    public static final String UPDATED_MIN_PARAM = "updated-min";
+
+    /**
+     * Param name constant for the updated max.
+     */
+    public static final String UPDATED_MAX_PARAM = "updated-max";
+
+    /**
+     * Param name constant for the published min.
+     */
+    public static final String PUBLISHED_MIN_PARAM = "published-min";
+
+    /**
+     * Param name constant for the published max.
+     */
+    public static final String PUBLISHED_MAX_PARAM = "published-max";
+
+    /**
+     * Param name constant for the start index for results.
+     */
+    public static final String START_INDEX_PARAM = "start-index";
+
+    /**
+     * Param name constant for the max number of results that should be fetched.
+     */
+    public static final String MAX_RESULTS_PARAM = "max-results";
+
+    private String entryId;
+
+    /**
+     * Creates a new empty QueryParams.
+     */
+    public QueryParams() {
+    }
+
+    /**
+     * Generates the url that should be used to query a GData feed.
+     * @param feedUrl The original feed URL.
+     * @return The URL that should be used to query the GData feed.
+     */
+    public abstract String generateQueryUrl(String feedUrl);
+
+    /**
+     * Gets a parameter value from this QueryParams.
+     * @param param The parameter name.
+     * @return The parameter value.  Returns null if the parameter is not
+     * defined in this QueryParams.
+     */
+    public abstract String getParamValue(String param);
+
+    /**
+     * Sets a parameter value in this QueryParams.
+     * @param param The parameter name.
+     * @param value The parameter value.
+     */
+    public abstract void setParamValue(String param, String value);
+
+    /**
+     * Clears everything in this QueryParams.
+     */
+    public abstract void clear();
+
+    /**
+     * @return the alt
+     */
+    public String getAlt() {
+        return getParamValue(ALT_PARAM);
+    }
+
+    /**
+     * @param alt the alt to set
+     */
+    public void setAlt(String alt) {
+        setParamValue(ALT_PARAM, alt);
+    }
+
+    /**
+     * @return the author
+     */
+    public String getAuthor() {
+        return getParamValue(AUTHOR_PARAM);
+    }
+
+    /**
+     * @param author the author to set
+     */
+    public void setAuthor(String author) {
+        setParamValue(AUTHOR_PARAM, author);
+    }
+
+    /**
+     * @return the entryId
+     */
+    public String getEntryId() {
+        return entryId;
+    }
+
+    /**
+     * @param entryId the entryId to set
+     */
+    public void setEntryId(String entryId) {
+        this.entryId = entryId;
+    }
+
+    /**
+     * @return the maxResults
+     */
+    public String getMaxResults() {
+        return getParamValue(MAX_RESULTS_PARAM);
+    }
+
+    // TODO: use an int!
+    /**
+     * @param maxResults the maxResults to set
+     */
+    public void setMaxResults(String maxResults) {
+        setParamValue(MAX_RESULTS_PARAM, maxResults);
+    }
+
+    /**
+     * @return the publishedMax
+     */
+    public String getPublishedMax() {
+        return getParamValue(PUBLISHED_MAX_PARAM);
+    }
+
+    /**
+     * @param publishedMax the publishedMax to set
+     */
+    public void setPublishedMax(String publishedMax) {
+        setParamValue(PUBLISHED_MAX_PARAM, publishedMax);
+    }
+
+    /**
+     * @return the publishedMin
+     */
+    public String getPublishedMin() {
+        return getParamValue(PUBLISHED_MIN_PARAM);
+    }
+
+    /**
+     * @param publishedMin the publishedMin to set
+     */
+    public void setPublishedMin(String publishedMin) {
+        setParamValue(PUBLISHED_MIN_PARAM, publishedMin);
+    }
+
+    /**
+     * @return the query
+     */
+    public String getQuery() {
+        return getParamValue(QUERY_PARAM);
+    }
+
+    /**
+     * @param query the query to set
+     */
+    public void setQuery(String query) {
+        setParamValue(QUERY_PARAM, query);
+    }
+
+    /**
+     * @return the startIndex
+     */
+    public String getStartIndex() {
+        return getParamValue(START_INDEX_PARAM);
+    }
+
+    /**
+     * @param startIndex the startIndex to set
+     */
+    public void setStartIndex(String startIndex) {
+        setParamValue(START_INDEX_PARAM, startIndex);
+    }
+
+    /**
+     * @return the updatedMax
+     */
+    public String getUpdatedMax() {
+        return getParamValue(UPDATED_MAX_PARAM);
+    }
+
+    /**
+     * @param updatedMax the updatedMax to set
+     */
+    public void setUpdatedMax(String updatedMax) {
+        setParamValue(UPDATED_MAX_PARAM, updatedMax);
+    }
+
+    /**
+     * @return the updatedMin
+     */
+    public String getUpdatedMin() {
+        return getParamValue(UPDATED_MIN_PARAM);
+    }
+
+    /**
+     * @param updatedMin the updatedMin to set
+     */
+    public void setUpdatedMin(String updatedMin) {
+        setParamValue(UPDATED_MIN_PARAM, updatedMin);
+    }
+}
diff --git a/src/com/google/wireless/gdata2/client/package.html b/src/com/google/wireless/gdata2/client/package.html
new file mode 100644
index 0000000..1c9bf9d
--- /dev/null
+++ b/src/com/google/wireless/gdata2/client/package.html
@@ -0,0 +1,5 @@
+<html>
+<body>
+    {@hide}
+</body>
+</html>
diff --git a/src/com/google/wireless/gdata2/contacts/client/ContactsClient.java b/src/com/google/wireless/gdata2/contacts/client/ContactsClient.java
new file mode 100644
index 0000000..f71a05f
--- /dev/null
+++ b/src/com/google/wireless/gdata2/contacts/client/ContactsClient.java
@@ -0,0 +1,33 @@
+// Copyright 2007 The Android Open Source Project
+
+package com.google.wireless.gdata2.contacts.client;
+
+import com.google.wireless.gdata2.client.GDataClient;
+import com.google.wireless.gdata2.client.GDataParserFactory;
+import com.google.wireless.gdata2.client.GDataServiceClient;
+
+/**
+ * GDataServiceClient for accessing Google Contacts.  This client can access and
+ * parse the contacts feeds for specific user. The parser this class uses handle
+ * the XML version of feeds.
+ */
+public class ContactsClient extends GDataServiceClient {
+    /** Service value for contacts. */
+  public static final String SERVICE = "cp";
+
+  /**
+   * Create a new ContactsClient.
+   * @param client The GDataClient that should be used to authenticate
+   * if we are using the caribou feed
+   */
+  public ContactsClient(GDataClient client, GDataParserFactory factory) {
+    super(client, factory);
+  }
+
+  /* (non-Javadoc)
+  * @see GDataServiceClient#getServiceName
+  */
+  public String getServiceName() {
+    return SERVICE;
+  }
+}
diff --git a/src/com/google/wireless/gdata2/contacts/client/package.html b/src/com/google/wireless/gdata2/contacts/client/package.html
new file mode 100644
index 0000000..1c9bf9d
--- /dev/null
+++ b/src/com/google/wireless/gdata2/contacts/client/package.html
@@ -0,0 +1,5 @@
+<html>
+<body>
+    {@hide}
+</body>
+</html>
diff --git a/src/com/google/wireless/gdata2/contacts/data/ContactEntry.java b/src/com/google/wireless/gdata2/contacts/data/ContactEntry.java
new file mode 100644
index 0000000..f21e60f
--- /dev/null
+++ b/src/com/google/wireless/gdata2/contacts/data/ContactEntry.java
@@ -0,0 +1,231 @@
+// Copyright 2007 The Android Open Source Project
+
+package com.google.wireless.gdata2.contacts.data;
+
+import com.google.wireless.gdata2.data.Entry;
+import com.google.wireless.gdata2.data.ExtendedProperty;
+import com.google.wireless.gdata2.data.StringUtils;
+import com.google.wireless.gdata2.parser.ParseException;
+
+import java.util.Vector;
+import java.util.Enumeration;
+
+/**
+ * Entry containing information about a contact.
+ */
+public class ContactEntry extends Entry {
+  private String linkPhotoHref;
+  private String linkEditPhotoHref;
+  private String linkPhotoType;
+  private String linkEditPhotoType;
+  private final Vector emailAddresses = new Vector();
+  private final Vector imAddresses = new Vector();
+  private final Vector phoneNumbers = new Vector();
+  private final Vector postalAddresses = new Vector();
+  private final Vector organizations = new Vector();
+  private final Vector extendedProperties = new Vector();
+  private final Vector groups = new Vector();
+  private String yomiName;
+
+  public ContactEntry() {
+    super();
+  }
+
+  public void setLinkEditPhoto(String href, String type) {
+    this.linkEditPhotoHref = href;
+    this.linkEditPhotoType = type;
+  }
+
+  public String getLinkEditPhotoHref() {
+    return linkEditPhotoHref;
+  }
+
+  public String getLinkEditPhotoType() {
+    return linkEditPhotoType;
+  }
+
+  public void setLinkPhoto(String href, String type) {
+    this.linkPhotoHref = href;
+    this.linkPhotoType = type;
+  }
+
+  public String getLinkPhotoHref() {
+    return linkPhotoHref;
+  }
+
+  public String getLinkPhotoType() {
+    return linkPhotoType;
+  }
+
+  public void addEmailAddress(EmailAddress emailAddress) {
+    emailAddresses.addElement(emailAddress);
+  }
+
+  public Vector getEmailAddresses() {
+    return emailAddresses;
+  }
+
+  public void addImAddress(ImAddress imAddress) {
+    imAddresses.addElement(imAddress);
+  }
+
+  public Vector getImAddresses() {
+    return imAddresses;
+  }
+
+  public void addPostalAddress(PostalAddress postalAddress) {
+    postalAddresses.addElement(postalAddress);
+  }
+
+  public Vector getPostalAddresses() {
+    return postalAddresses;
+  }
+
+  public void addPhoneNumber(PhoneNumber phoneNumber) {
+    phoneNumbers.addElement(phoneNumber);
+  }
+
+  public Vector getPhoneNumbers() {
+    return phoneNumbers;
+  }
+
+  public void addOrganization(Organization organization) {
+    organizations.addElement(organization);
+  }
+
+  public Vector getExtendedProperties() {
+    return extendedProperties;
+  }
+
+  public void addExtendedProperty(ExtendedProperty extendedProperty) {
+    extendedProperties.addElement(extendedProperty);
+  }
+
+  public Vector getGroups() {
+    return groups;
+  }
+
+  public void addGroup(GroupMembershipInfo group) {
+    groups.addElement(group);
+  }
+
+  public Vector getOrganizations() {
+    return organizations;
+  }
+
+  public void setYomiName(String yomiName) {
+    this.yomiName = yomiName;
+  }
+
+  public String getYomiName() {
+    return yomiName;
+  }
+
+  /*
+  * (non-Javadoc)
+  * @see com.google.wireless.gdata2.data.Entry#clear()
+  */
+  public void clear() {
+    super.clear();
+    linkEditPhotoHref = null;
+    linkEditPhotoType = null;
+    linkPhotoHref = null;
+    linkPhotoType = null;
+    emailAddresses.removeAllElements();
+    imAddresses.removeAllElements();
+    phoneNumbers.removeAllElements();
+    postalAddresses.removeAllElements();
+    organizations.removeAllElements();
+    extendedProperties.removeAllElements();
+    groups.removeAllElements();
+    yomiName = null;
+  }
+
+  protected void toString(StringBuffer sb) {
+    super.toString(sb);
+    sb.append("\n");
+    sb.append("ContactEntry:");
+    if (!StringUtils.isEmpty(linkPhotoHref)) {
+      sb.append(" linkPhotoHref:").append(linkPhotoHref).append("\n");
+    }
+    if (!StringUtils.isEmpty(linkPhotoType)) {
+      sb.append(" linkPhotoType:").append(linkPhotoType).append("\n");
+    }
+    if (!StringUtils.isEmpty(linkEditPhotoHref)) {
+      sb.append(" linkEditPhotoHref:").append(linkEditPhotoHref).append("\n");
+    }
+    if (!StringUtils.isEmpty(linkEditPhotoType)) {
+      sb.append(" linkEditPhotoType:").append(linkEditPhotoType).append("\n");
+    }
+    for (Enumeration iter = emailAddresses.elements();
+        iter.hasMoreElements(); ) {
+      sb.append("  ");
+      ((EmailAddress) iter.nextElement()).toString(sb);
+      sb.append("\n");
+    }
+    for (Enumeration iter = imAddresses.elements();
+        iter.hasMoreElements(); ) {
+      sb.append("  ");
+      ((ImAddress) iter.nextElement()).toString(sb);
+      sb.append("\n");
+    }
+    for (Enumeration iter = postalAddresses.elements();
+        iter.hasMoreElements(); ) {
+      sb.append("  ");
+      ((PostalAddress) iter.nextElement()).toString(sb);
+      sb.append("\n");
+    }
+    for (Enumeration iter = phoneNumbers.elements();
+        iter.hasMoreElements(); ) {
+      sb.append("  ");
+      ((PhoneNumber) iter.nextElement()).toString(sb);
+      sb.append("\n");
+    }
+    for (Enumeration iter = organizations.elements();
+        iter.hasMoreElements(); ) {
+      sb.append("  ");
+      ((Organization) iter.nextElement()).toString(sb);
+      sb.append("\n");
+    }
+    for (Enumeration iter = extendedProperties.elements();
+        iter.hasMoreElements(); ) {
+      sb.append("  ");
+      ((ExtendedProperty) iter.nextElement()).toString(sb);
+      sb.append("\n");
+    }
+    for (Enumeration iter = groups.elements();
+        iter.hasMoreElements(); ) {
+      sb.append("  ");
+      ((GroupMembershipInfo) iter.nextElement()).toString(sb);
+      sb.append("\n");
+    }
+    if (!StringUtils.isEmpty(yomiName)) {
+      sb.append(" yomiName:").append(yomiName).append("\n");
+    }
+  }
+
+  public void validate() throws ParseException {
+    super.validate();
+    for (Enumeration iter = emailAddresses.elements(); iter.hasMoreElements(); ) {
+      ((EmailAddress) iter.nextElement()).validate();
+    }
+    for (Enumeration iter = imAddresses.elements(); iter.hasMoreElements(); ) {
+      ((ImAddress) iter.nextElement()).validate();
+    }
+    for (Enumeration iter = postalAddresses.elements(); iter.hasMoreElements(); ) {
+      ((PostalAddress) iter.nextElement()).validate();
+    }
+    for (Enumeration iter = phoneNumbers.elements(); iter.hasMoreElements(); ) {
+      ((PhoneNumber) iter.nextElement()).validate();
+    }
+    for (Enumeration iter = organizations.elements(); iter.hasMoreElements(); ) {
+      ((Organization) iter.nextElement()).validate();
+    }
+    for (Enumeration iter = extendedProperties.elements(); iter.hasMoreElements(); ) {
+      ((ExtendedProperty) iter.nextElement()).validate();
+    }
+    for (Enumeration iter = groups.elements(); iter.hasMoreElements(); ) {
+      ((GroupMembershipInfo) iter.nextElement()).validate();
+    }
+  }
+}
diff --git a/src/com/google/wireless/gdata2/contacts/data/ContactsElement.java b/src/com/google/wireless/gdata2/contacts/data/ContactsElement.java
new file mode 100644
index 0000000..1d944eb
--- /dev/null
+++ b/src/com/google/wireless/gdata2/contacts/data/ContactsElement.java
@@ -0,0 +1,71 @@
+package com.google.wireless.gdata2.contacts.data;
+
+import com.google.wireless.gdata2.parser.ParseException;
+
+/**
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Contains attributes that are common to all elements in a ContactEntry.
+ */
+public abstract class ContactsElement {
+  public static final byte TYPE_NONE = -1;
+  private byte type = TYPE_NONE;
+
+  private String label;
+
+  private boolean isPrimary;
+
+  public boolean isPrimary() {
+    return isPrimary;
+  }
+
+  public void setIsPrimary(boolean primary) {
+    isPrimary = primary;
+  }
+
+  public byte getType() {
+    return type;
+  }
+
+  public void setType(byte rel) {
+    this.type = rel;
+  }
+
+  public String getLabel() {
+    return label;
+  }
+
+  public void setLabel(String label) {
+    this.label = label;
+  }
+
+  public void toString(StringBuffer sb) {
+    sb.append(" type:").append(type);
+    sb.append(" isPrimary:").append(isPrimary);
+    if (label != null) sb.append(" label:").append(label);
+  }
+
+  public String toString() {
+    StringBuffer sb = new StringBuffer();
+    toString(sb);
+    return sb.toString();
+  }
+
+  public void validate() throws ParseException {
+    if ((label == null && type == TYPE_NONE) || (label != null && type != TYPE_NONE)) {
+      throw new ParseException("exactly one of label or type must be set");
+    }
+  }
+}
diff --git a/src/com/google/wireless/gdata2/contacts/data/ContactsFeed.java b/src/com/google/wireless/gdata2/contacts/data/ContactsFeed.java
new file mode 100644
index 0000000..1e879f0
--- /dev/null
+++ b/src/com/google/wireless/gdata2/contacts/data/ContactsFeed.java
@@ -0,0 +1,16 @@
+// Copyright 2007 The Android Open Source Project
+
+package com.google.wireless.gdata2.contacts.data;
+
+import com.google.wireless.gdata2.data.Feed;
+
+/**
+ * Feed containing contacts.
+ */
+public class ContactsFeed extends Feed {
+    /**
+     * Creates a new empty events feed.
+     */
+    public ContactsFeed() {
+    }
+}
diff --git a/src/com/google/wireless/gdata2/contacts/data/EmailAddress.java b/src/com/google/wireless/gdata2/contacts/data/EmailAddress.java
new file mode 100644
index 0000000..432a4dd
--- /dev/null
+++ b/src/com/google/wireless/gdata2/contacts/data/EmailAddress.java
@@ -0,0 +1,28 @@
+// Copyright 2007 The Android Open Source Project
+
+package com.google.wireless.gdata2.contacts.data;
+
+/**
+ * The EmailAddress GData type.
+ */
+public class EmailAddress extends ContactsElement {
+  public static final byte TYPE_HOME = 1;
+  public static final byte TYPE_WORK = 2;
+  public static final byte TYPE_OTHER = 3;
+
+  private String address;
+
+  public String getAddress() {
+    return address;
+  }
+
+  public void setAddress(String address) {
+    this.address = address;
+  }
+
+  public void toString(StringBuffer sb) {
+    sb.append("EmailAddress");
+    super.toString(sb);
+    if (address != null) sb.append(" address:").append(address);
+  }
+}
diff --git a/src/com/google/wireless/gdata2/contacts/data/GeoPt.java b/src/com/google/wireless/gdata2/contacts/data/GeoPt.java
new file mode 100644
index 0000000..a3ffe37
--- /dev/null
+++ b/src/com/google/wireless/gdata2/contacts/data/GeoPt.java
@@ -0,0 +1,70 @@
+package com.google.wireless.gdata2.contacts.data;
+
+/**
+ * The GeoPt GData type.
+ */
+public class GeoPt {
+    private String label;
+    private Float latitude;
+    private Float longitude;
+    private Float elevation;
+
+    // TODO: figure out how to store the GeoPt time
+    private String time;
+
+    public String getLabel() {
+        return label;
+    }
+
+    public void setLabel(String label) {
+        this.label = label;
+    }
+
+    public Float getLatitute() {
+        return latitude;
+    }
+
+    public void setLatitude(Float lat) {
+        this.latitude = lat;
+    }
+
+    public Float getLongitute() {
+        return longitude;
+    }
+
+    public void setLongitude(Float lon) {
+        this.longitude = lon;
+    }
+
+    public Float getElevation() {
+        return elevation;
+    }
+
+    public void setElevation(Float elev) {
+        this.elevation = elev;
+    }
+
+    public String getTime() {
+      return time;
+    }
+
+    public void setTime(String time) {
+      this.time = time;
+    }
+
+    public void toString(StringBuffer sb) {
+        sb.append("GeoPt");
+        if (latitude != null) sb.append(" latitude:").append(latitude);
+        if (longitude != null) sb.append(" longitude:").append(longitude);
+        if (elevation != null) sb.append(" elevation:").append(elevation);
+        if (time != null) sb.append(" time:").append(time);
+        if (label != null) sb.append(" label:").append(label);
+    }
+
+    @Override
+    public String toString() {
+        StringBuffer sb = new StringBuffer();
+        toString(sb);
+        return sb.toString();
+    }
+}
diff --git a/src/com/google/wireless/gdata2/contacts/data/GroupEntry.java b/src/com/google/wireless/gdata2/contacts/data/GroupEntry.java
new file mode 100644
index 0000000..81bddc4
--- /dev/null
+++ b/src/com/google/wireless/gdata2/contacts/data/GroupEntry.java
@@ -0,0 +1,39 @@
+package com.google.wireless.gdata2.contacts.data;
+
+import com.google.wireless.gdata2.data.Entry;
+import com.google.wireless.gdata2.data.StringUtils;
+
+/**
+ * Entry containing information about a contact group.
+ */
+public class GroupEntry extends Entry {
+  // If this is a system group then this field will be set with the name of the system group.
+  private String systemGroup = null;
+
+  public GroupEntry() {
+    super();
+  }
+
+  public String getSystemGroup() {
+    return systemGroup;
+  }
+
+  @Override
+  public void clear() {
+    super.clear();
+    systemGroup = null;
+  }
+
+  public void setSystemGroup(String systemGroup) {
+    this.systemGroup = systemGroup;
+  }
+
+  protected void toString(StringBuffer sb) {
+    super.toString(sb);
+    sb.append("\n");
+    sb.append("GroupEntry:");
+    if (!StringUtils.isEmpty(systemGroup)) {
+      sb.append(" systemGroup:").append(systemGroup).append("\n");
+    }
+  }
+}
diff --git a/src/com/google/wireless/gdata2/contacts/data/GroupMembershipInfo.java b/src/com/google/wireless/gdata2/contacts/data/GroupMembershipInfo.java
new file mode 100644
index 0000000..ef44332
--- /dev/null
+++ b/src/com/google/wireless/gdata2/contacts/data/GroupMembershipInfo.java
@@ -0,0 +1,38 @@
+package com.google.wireless.gdata2.contacts.data;
+
+import com.google.wireless.gdata2.data.StringUtils;
+import com.google.wireless.gdata2.parser.ParseException;
+
+/** The groupMembershipInfo GData type. */
+public class GroupMembershipInfo {
+  private String group;
+  private boolean deleted;
+
+  public String getGroup() {
+    return group;
+  }
+
+  public void setGroup(String group) {
+    this.group = group;
+  }
+
+  public boolean isDeleted() {
+    return deleted;
+  }
+
+  public void setDeleted(boolean deleted) {
+    this.deleted = deleted;
+  }
+
+  public void toString(StringBuffer sb) {
+    sb.append("GroupMembershipInfo");
+    if (group != null) sb.append(" group:").append(group);
+    sb.append(" deleted:").append(deleted);
+  }
+
+  public void validate() throws ParseException {
+    if (StringUtils.isEmpty(group)) {
+      throw new ParseException("the group must be present");
+    }
+  }
+}
diff --git a/src/com/google/wireless/gdata2/contacts/data/GroupsFeed.java b/src/com/google/wireless/gdata2/contacts/data/GroupsFeed.java
new file mode 100644
index 0000000..eb606d3
--- /dev/null
+++ b/src/com/google/wireless/gdata2/contacts/data/GroupsFeed.java
@@ -0,0 +1,14 @@
+package com.google.wireless.gdata2.contacts.data;
+
+import com.google.wireless.gdata2.data.Feed;
+
+/**
+ * Feed containing contact groups.
+ */
+public class GroupsFeed extends Feed {
+    /**
+     * Creates a new empty contact groups feed.
+     */
+    public GroupsFeed() {
+    }
+}
diff --git a/src/com/google/wireless/gdata2/contacts/data/ImAddress.java b/src/com/google/wireless/gdata2/contacts/data/ImAddress.java
new file mode 100644
index 0000000..2ef86f5
--- /dev/null
+++ b/src/com/google/wireless/gdata2/contacts/data/ImAddress.java
@@ -0,0 +1,59 @@
+// Copyright 2007 The Android Open Source Project
+
+package com.google.wireless.gdata2.contacts.data;
+
+/**
+ * The ImAddress gdata type
+ */
+public class ImAddress extends ContactsElement {
+  public static final byte TYPE_HOME = 1;
+  public static final byte TYPE_WORK = 2;
+  public static final byte TYPE_OTHER = 3;
+
+  public static final byte PROTOCOL_CUSTOM = 1;
+  public static final byte PROTOCOL_AIM = 2;
+  public static final byte PROTOCOL_MSN = 3;
+  public static final byte PROTOCOL_YAHOO = 4;
+  public static final byte PROTOCOL_SKYPE = 5;
+  public static final byte PROTOCOL_QQ = 6;
+  public static final byte PROTOCOL_GOOGLE_TALK = 7;
+  public static final byte PROTOCOL_ICQ = 8;
+  public static final byte PROTOCOL_JABBER = 9;
+  public static final byte PROTOCOL_NONE = 10;
+
+  private byte protocolPredefined;
+  private String protocolCustom;
+  private String address;
+
+  public byte getProtocolPredefined() {
+    return protocolPredefined;
+  }
+
+  public void setProtocolPredefined(byte protocolPredefined) {
+    this.protocolPredefined = protocolPredefined;
+  }
+
+  public String getProtocolCustom() {
+    return protocolCustom;
+  }
+
+  public void setProtocolCustom(String protocolCustom) {
+    this.protocolCustom = protocolCustom;
+  }
+
+  public String getAddress() {
+    return address;
+  }
+
+  public void setAddress(String address) {
+    this.address = address;
+  }
+
+  public void toString(StringBuffer sb) {
+    sb.append("ImAddress");
+    super.toString(sb);
+    sb.append(" protocolPredefined:").append(protocolPredefined);
+    if (protocolCustom != null) sb.append(" protocolCustom:").append(protocolCustom);
+    if (address != null) sb.append(" address:").append(address);
+  }
+}
diff --git a/src/com/google/wireless/gdata2/contacts/data/Organization.java b/src/com/google/wireless/gdata2/contacts/data/Organization.java
new file mode 100644
index 0000000..cff6793
--- /dev/null
+++ b/src/com/google/wireless/gdata2/contacts/data/Organization.java
@@ -0,0 +1,43 @@
+package com.google.wireless.gdata2.contacts.data;
+
+import com.google.wireless.gdata2.parser.ParseException;
+
+/** The Organization GData type. */
+public class Organization extends ContactsElement {
+  public static final byte TYPE_WORK = 1;
+  public static final byte TYPE_OTHER = 2;
+
+  private String name;
+  private String title;
+
+  public String getName() {
+    return name;
+  }
+
+  public void setName(String name) {
+    this.name = name;
+  }
+
+  public String getTitle() {
+    return title;
+  }
+
+  public void setTitle(String title) {
+    this.title = title;
+  }
+
+  public void toString(StringBuffer sb) {
+    sb.append("Organization");
+    super.toString(sb);
+    if (name != null) sb.append(" name:").append(name);
+    if (title != null) sb.append(" title:").append(title);
+  }
+
+  public void validate() throws ParseException {
+    super.validate();
+
+    if (name == null && title == null) {
+      throw new ParseException("at least one of name or title must be present");
+    }
+  }
+}
diff --git a/src/com/google/wireless/gdata2/contacts/data/PhoneNumber.java b/src/com/google/wireless/gdata2/contacts/data/PhoneNumber.java
new file mode 100644
index 0000000..3537f73
--- /dev/null
+++ b/src/com/google/wireless/gdata2/contacts/data/PhoneNumber.java
@@ -0,0 +1,33 @@
+// Copyright 2007 The Android Open Source Project
+
+package com.google.wireless.gdata2.contacts.data;
+
+/**
+ * The PhoneNumber gdata type
+ */
+public class PhoneNumber extends ContactsElement {
+  /** The phone number type. */
+  public static final byte TYPE_MOBILE = 1;
+  public static final byte TYPE_HOME = 2;
+  public static final byte TYPE_WORK = 3;
+  public static final byte TYPE_WORK_FAX = 4;
+  public static final byte TYPE_HOME_FAX = 5;
+  public static final byte TYPE_PAGER = 6;
+  public static final byte TYPE_OTHER = 7;
+
+  private String phoneNumber;
+
+  public String getPhoneNumber() {
+    return phoneNumber;
+  }
+
+  public void setPhoneNumber(String phoneNumber) {
+    this.phoneNumber = phoneNumber;
+  }
+
+  public void toString(StringBuffer sb) {
+    sb.append("PhoneNumber");
+    super.toString(sb);
+    if (phoneNumber != null) sb.append(" phoneNumber:").append(phoneNumber);
+  }
+}
diff --git a/src/com/google/wireless/gdata2/contacts/data/PostalAddress.java b/src/com/google/wireless/gdata2/contacts/data/PostalAddress.java
new file mode 100644
index 0000000..782a8a3
--- /dev/null
+++ b/src/com/google/wireless/gdata2/contacts/data/PostalAddress.java
@@ -0,0 +1,28 @@
+// Copyright 2007 The Android Open Source Project
+
+package com.google.wireless.gdata2.contacts.data;
+
+/**
+ * The PostalAddress gdata type
+ */
+public class PostalAddress extends ContactsElement {
+  public static final byte TYPE_HOME = 1;
+  public static final byte TYPE_WORK = 2;
+  public static final byte TYPE_OTHER = 3;
+
+  private String value;
+
+  public String getValue() {
+    return value;
+  }
+
+  public void setValue(String value) {
+    this.value = value;
+  }
+
+  public void toString(StringBuffer sb) {
+    sb.append("PostalAddress");
+    super.toString(sb);
+    if (value != null) sb.append(" value:").append(value);
+  }
+}
diff --git a/src/com/google/wireless/gdata2/contacts/data/package.html b/src/com/google/wireless/gdata2/contacts/data/package.html
new file mode 100644
index 0000000..1c9bf9d
--- /dev/null
+++ b/src/com/google/wireless/gdata2/contacts/data/package.html
@@ -0,0 +1,5 @@
+<html>
+<body>
+    {@hide}
+</body>
+</html>
diff --git a/src/com/google/wireless/gdata2/contacts/package.html b/src/com/google/wireless/gdata2/contacts/package.html
new file mode 100644
index 0000000..1c9bf9d
--- /dev/null
+++ b/src/com/google/wireless/gdata2/contacts/package.html
@@ -0,0 +1,5 @@
+<html>
+<body>
+    {@hide}
+</body>
+</html>
diff --git a/src/com/google/wireless/gdata2/contacts/parser/package.html b/src/com/google/wireless/gdata2/contacts/parser/package.html
new file mode 100644
index 0000000..1c9bf9d
--- /dev/null
+++ b/src/com/google/wireless/gdata2/contacts/parser/package.html
@@ -0,0 +1,5 @@
+<html>
+<body>
+    {@hide}
+</body>
+</html>
diff --git a/src/com/google/wireless/gdata2/contacts/parser/xml/XmlContactsGDataParser.java b/src/com/google/wireless/gdata2/contacts/parser/xml/XmlContactsGDataParser.java
new file mode 100644
index 0000000..5131d7f
--- /dev/null
+++ b/src/com/google/wireless/gdata2/contacts/parser/xml/XmlContactsGDataParser.java
@@ -0,0 +1,305 @@
+// Copyright 2007 The Android Open Source Project
+
+package com.google.wireless.gdata2.contacts.parser.xml;
+
+import com.google.wireless.gdata2.contacts.data.ContactEntry;
+import com.google.wireless.gdata2.contacts.data.ContactsElement;
+import com.google.wireless.gdata2.contacts.data.ContactsFeed;
+import com.google.wireless.gdata2.contacts.data.EmailAddress;
+import com.google.wireless.gdata2.contacts.data.ImAddress;
+import com.google.wireless.gdata2.contacts.data.Organization;
+import com.google.wireless.gdata2.contacts.data.PhoneNumber;
+import com.google.wireless.gdata2.contacts.data.PostalAddress;
+import com.google.wireless.gdata2.contacts.data.GroupMembershipInfo;
+import com.google.wireless.gdata2.data.Entry;
+import com.google.wireless.gdata2.data.Feed;
+import com.google.wireless.gdata2.data.XmlUtils;
+import com.google.wireless.gdata2.data.ExtendedProperty;
+import com.google.wireless.gdata2.parser.ParseException;
+import com.google.wireless.gdata2.parser.xml.XmlGDataParser;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Hashtable;
+import java.util.Enumeration;
+
+/**
+ * GDataParser for a contacts feed.
+ */
+public class XmlContactsGDataParser extends XmlGDataParser {
+  /** Namespace prefix for Contacts */
+  public static final String NAMESPACE_CONTACTS = "gContact";
+
+  /** Namespace URI for Contacts */
+  public static final String NAMESPACE_CONTACTS_URI =
+      "http://schemas.google.com/contact/2008";
+
+  /** The photo link rels */
+  public static final String LINK_REL_PHOTO = "http://schemas.google.com/contacts/2008/rel#photo";
+  public static final String LINK_REL_EDIT_PHOTO =
+          "http://schemas.google.com/contacts/2008/rel#edit-photo";
+
+  /** The phone number type gdata string. */
+  private static final String GD_NAMESPACE = "http://schemas.google.com/g/2005#";
+  public static final String TYPESTRING_MOBILE = GD_NAMESPACE + "mobile";
+  public static final String TYPESTRING_HOME = GD_NAMESPACE + "home";
+  public static final String TYPESTRING_WORK = GD_NAMESPACE + "work";
+  public static final String TYPESTRING_HOME_FAX = GD_NAMESPACE + "home_fax";
+  public static final String TYPESTRING_WORK_FAX = GD_NAMESPACE + "work_fax";
+  public static final String TYPESTRING_PAGER = GD_NAMESPACE + "pager";
+  public static final String TYPESTRING_OTHER = GD_NAMESPACE + "other";
+
+  public static final String IM_PROTOCOL_AIM = GD_NAMESPACE + "AIM";
+  public static final String IM_PROTOCOL_MSN = GD_NAMESPACE + "MSN";
+  public static final String IM_PROTOCOL_YAHOO = GD_NAMESPACE + "YAHOO";
+  public static final String IM_PROTOCOL_SKYPE = GD_NAMESPACE + "SKYPE";
+  public static final String IM_PROTOCOL_QQ = GD_NAMESPACE + "QQ";
+  public static final String IM_PROTOCOL_GOOGLE_TALK = GD_NAMESPACE + "GOOGLE_TALK";
+  public static final String IM_PROTOCOL_ICQ = GD_NAMESPACE + "ICQ";
+  public static final String IM_PROTOCOL_JABBER = GD_NAMESPACE + "JABBER";
+
+  private static final Hashtable REL_TO_TYPE_EMAIL;
+  private static final Hashtable REL_TO_TYPE_PHONE;
+  private static final Hashtable REL_TO_TYPE_POSTAL;
+  private static final Hashtable REL_TO_TYPE_IM;
+  private static final Hashtable REL_TO_TYPE_ORGANIZATION;
+  private static final Hashtable IM_PROTOCOL_STRING_TO_TYPE_MAP;
+
+  public static final Hashtable TYPE_TO_REL_EMAIL;
+  public static final Hashtable TYPE_TO_REL_PHONE;
+  public static final Hashtable TYPE_TO_REL_POSTAL;
+  public static final Hashtable TYPE_TO_REL_IM;
+  public static final Hashtable TYPE_TO_REL_ORGANIZATION;
+  public static final Hashtable IM_PROTOCOL_TYPE_TO_STRING_MAP;
+
+  static {
+    Hashtable map;
+
+    map = new Hashtable();
+    map.put(TYPESTRING_HOME, new Byte(EmailAddress.TYPE_HOME));
+    map.put(TYPESTRING_WORK, new Byte(EmailAddress.TYPE_WORK));
+    map.put(TYPESTRING_OTHER, new Byte(EmailAddress.TYPE_OTHER));
+    // TODO: this is a hack to support the old feed
+    map.put(GD_NAMESPACE + "primary", (byte)4);
+    REL_TO_TYPE_EMAIL = map;
+    TYPE_TO_REL_EMAIL = swapMap(map);
+
+    map = new Hashtable();
+    map.put(TYPESTRING_HOME, new Byte(PhoneNumber.TYPE_HOME));
+    map.put(TYPESTRING_MOBILE, new Byte(PhoneNumber.TYPE_MOBILE));
+    map.put(TYPESTRING_PAGER, new Byte(PhoneNumber.TYPE_PAGER));
+    map.put(TYPESTRING_WORK, new Byte(PhoneNumber.TYPE_WORK));
+    map.put(TYPESTRING_HOME_FAX, new Byte(PhoneNumber.TYPE_HOME_FAX));
+    map.put(TYPESTRING_WORK_FAX, new Byte(PhoneNumber.TYPE_WORK_FAX));
+    map.put(TYPESTRING_OTHER, new Byte(PhoneNumber.TYPE_OTHER));
+    REL_TO_TYPE_PHONE = map;
+    TYPE_TO_REL_PHONE = swapMap(map);
+
+    map = new Hashtable();
+    map.put(TYPESTRING_HOME, new Byte(PostalAddress.TYPE_HOME));
+    map.put(TYPESTRING_WORK, new Byte(PostalAddress.TYPE_WORK));
+    map.put(TYPESTRING_OTHER, new Byte(PostalAddress.TYPE_OTHER));
+    REL_TO_TYPE_POSTAL = map;
+    TYPE_TO_REL_POSTAL = swapMap(map);
+
+    map = new Hashtable();
+    map.put(TYPESTRING_HOME, new Byte(ImAddress.TYPE_HOME));
+    map.put(TYPESTRING_WORK, new Byte(ImAddress.TYPE_WORK));
+    map.put(TYPESTRING_OTHER, new Byte(ImAddress.TYPE_OTHER));
+    REL_TO_TYPE_IM = map;
+    TYPE_TO_REL_IM = swapMap(map);
+
+    map = new Hashtable();
+    map.put(TYPESTRING_WORK, new Byte(Organization.TYPE_WORK));
+    map.put(TYPESTRING_OTHER, new Byte(Organization.TYPE_OTHER));
+    REL_TO_TYPE_ORGANIZATION = map;
+    TYPE_TO_REL_ORGANIZATION = swapMap(map);
+
+    map = new Hashtable();
+    map.put(IM_PROTOCOL_AIM, new Byte(ImAddress.PROTOCOL_AIM));
+    map.put(IM_PROTOCOL_MSN, new Byte(ImAddress.PROTOCOL_MSN));
+    map.put(IM_PROTOCOL_YAHOO, new Byte(ImAddress.PROTOCOL_YAHOO));
+    map.put(IM_PROTOCOL_SKYPE, new Byte(ImAddress.PROTOCOL_SKYPE));
+    map.put(IM_PROTOCOL_QQ, new Byte(ImAddress.PROTOCOL_QQ));
+    map.put(IM_PROTOCOL_GOOGLE_TALK, new Byte(ImAddress.PROTOCOL_GOOGLE_TALK));
+    map.put(IM_PROTOCOL_ICQ, new Byte(ImAddress.PROTOCOL_ICQ));
+    map.put(IM_PROTOCOL_JABBER, new Byte(ImAddress.PROTOCOL_JABBER));
+    IM_PROTOCOL_STRING_TO_TYPE_MAP = map;
+    IM_PROTOCOL_TYPE_TO_STRING_MAP = swapMap(map);
+  }
+
+  private static Hashtable swapMap(Hashtable originalMap) {
+    Hashtable newMap = new Hashtable();
+    Enumeration enumeration = originalMap.keys();
+    while (enumeration.hasMoreElements()) {
+      Object key = enumeration.nextElement();
+      Object value = originalMap.get(key);
+      if (newMap.containsKey(value)) {
+        throw new IllegalArgumentException("value " + value
+            + " was already encountered");
+      }
+      newMap.put(value, key);
+    }
+    return newMap;
+  }
+
+  /**
+   * Creates a new XmlEventsGDataParser.
+   * @param is The InputStream that should be parsed.
+   * @throws ParseException Thrown if a parser cannot be created.
+   */
+  public XmlContactsGDataParser(InputStream is, XmlPullParser parser)
+      throws ParseException {
+    super(is, parser);
+  }
+
+  /*
+  * (non-Javadoc)
+  * @see com.google.wireless.gdata2.parser.xml.XmlGDataParser#createFeed()
+  */
+  protected Feed createFeed() {
+    return new ContactsFeed();
+  }
+
+  /*
+  * (non-Javadoc)
+  * @see com.google.wireless.gdata2.parser.xml.XmlGDataParser#createEntry()
+  */
+  protected Entry createEntry() {
+    return new ContactEntry();
+  }
+
+  protected void handleExtraElementInEntry(Entry entry) throws XmlPullParserException, IOException {
+    XmlPullParser parser = getParser();
+
+    if (!(entry instanceof ContactEntry)) {
+      throw new IllegalArgumentException("Expected ContactEntry!");
+    }
+    ContactEntry contactEntry = (ContactEntry) entry;
+    String name = parser.getName();
+    if ("email".equals(name)) {
+      EmailAddress emailAddress = new EmailAddress();
+      parseContactsElement(emailAddress, parser, REL_TO_TYPE_EMAIL);
+      // TODO: remove this when the feed is upgraded
+      if (emailAddress.getType() == 4) {
+        emailAddress.setType(EmailAddress.TYPE_OTHER);
+        emailAddress.setIsPrimary(true);
+        emailAddress.setLabel(null);
+      }
+      emailAddress.setAddress(parser.getAttributeValue(null  /* ns */, "address"));
+      contactEntry.addEmailAddress(emailAddress);
+    } else if ("deleted".equals(name)) {
+      contactEntry.setDeleted(true);
+    } else if ("im".equals(name)) {
+      ImAddress imAddress = new ImAddress();
+      parseContactsElement(imAddress, parser, REL_TO_TYPE_IM);
+      imAddress.setAddress(parser.getAttributeValue(null  /* ns */, "address"));
+      imAddress.setLabel(parser.getAttributeValue(null  /* ns */, "label"));
+      String protocolString = parser.getAttributeValue(null  /* ns */, "protocol");
+      if (protocolString == null) {
+        imAddress.setProtocolPredefined(ImAddress.PROTOCOL_NONE);
+        imAddress.setProtocolCustom(null);
+      } else {
+        Byte predefinedProtocol = (Byte) IM_PROTOCOL_STRING_TO_TYPE_MAP.get(protocolString);
+        if (predefinedProtocol == null) {
+          imAddress.setProtocolPredefined(ImAddress.PROTOCOL_CUSTOM);
+          imAddress.setProtocolCustom(protocolString);
+        } else {
+          imAddress.setProtocolPredefined(predefinedProtocol.byteValue());
+          imAddress.setProtocolCustom(null);
+        }
+      }
+      contactEntry.addImAddress(imAddress);
+    } else if ("postalAddress".equals(name)) {
+      PostalAddress postalAddress = new PostalAddress();
+      parseContactsElement(postalAddress, parser, REL_TO_TYPE_POSTAL);
+      postalAddress.setValue(XmlUtils.extractChildText(parser));
+      contactEntry.addPostalAddress(postalAddress);
+    } else if ("phoneNumber".equals(name)) {
+      PhoneNumber phoneNumber = new PhoneNumber();
+      parseContactsElement(phoneNumber, parser, REL_TO_TYPE_PHONE);
+      phoneNumber.setPhoneNumber(XmlUtils.extractChildText(parser));
+      contactEntry.addPhoneNumber(phoneNumber);
+    } else if ("organization".equals(name)) {
+      Organization organization = new Organization();
+      parseContactsElement(organization, parser, REL_TO_TYPE_ORGANIZATION);
+      handleOrganizationSubElement(organization, parser);
+      contactEntry.addOrganization(organization);
+    } else if ("extendedProperty".equals(name)) {
+      ExtendedProperty extendedProperty = new ExtendedProperty();
+      parseExtendedProperty(extendedProperty);
+      contactEntry.addExtendedProperty(extendedProperty);
+    } else if ("groupMembershipInfo".equals(name)) {
+      GroupMembershipInfo group = new GroupMembershipInfo();
+      group.setGroup(parser.getAttributeValue(null  /* ns */, "href"));
+      group.setDeleted("true".equals(parser.getAttributeValue(null  /* ns */, "deleted")));
+      contactEntry.addGroup(group);
+    } else if ("yomiName".equals(name)) {
+      String yomiName = XmlUtils.extractChildText(parser);
+      contactEntry.setYomiName(yomiName);
+    }
+  }
+
+  @Override
+  protected void handleExtraLinkInEntry(String rel, String type, String href, Entry entry)
+      throws XmlPullParserException, IOException {
+    if (LINK_REL_PHOTO.equals(rel)) {
+      ContactEntry contactEntry = (ContactEntry) entry;
+      contactEntry.setLinkPhoto(href, type);
+    } else if (LINK_REL_EDIT_PHOTO.equals(rel)) {
+      ContactEntry contactEntry = (ContactEntry) entry;
+      contactEntry.setLinkEditPhoto(href, type);
+    }
+  }
+
+  private static void parseContactsElement(ContactsElement element, XmlPullParser parser,
+      Hashtable relToTypeMap) throws XmlPullParserException {
+    String rel = parser.getAttributeValue(null  /* ns */, "rel");
+    String label = parser.getAttributeValue(null  /* ns */, "label");
+
+    if ((label == null && rel == null) || (label != null && rel != null)) {
+      // TODO: remove this once the focus feed is fixed to not send this case
+      rel = TYPESTRING_OTHER;
+    }
+
+    if (rel != null) {
+      final Object type = relToTypeMap.get(rel.toLowerCase());
+      if (type == null) {
+        throw new XmlPullParserException("unknown rel, " + rel);
+      }
+      element.setType(((Byte) type).byteValue());
+    }
+    element.setLabel(label);
+    element.setIsPrimary("true".equals(parser.getAttributeValue(null  /* ns */, "primary")));
+  }
+
+  private static void handleOrganizationSubElement(Organization element, XmlPullParser parser)
+    throws XmlPullParserException, IOException {
+    int depth = parser.getDepth();
+    while (true) {
+      String tag = XmlUtils.nextDirectChildTag(parser, depth);
+      if (tag == null) break;
+      if ("orgName".equals(tag)) {
+        element.setName(XmlUtils.extractChildText(parser));
+      } else if ("orgTitle".equals(tag)) {
+        element.setTitle(XmlUtils.extractChildText(parser));
+      }
+    }
+  }
+
+  /**
+   * Parse the ExtendedProperty. The parser is assumed to be at the beginning of the tag
+   * for the ExtendedProperty.
+   * @param extendedProperty the ExtendedProperty object to populate
+   */
+  private void parseExtendedProperty(ExtendedProperty extendedProperty)
+      throws IOException, XmlPullParserException {
+    XmlPullParser parser = getParser();
+    extendedProperty.setName(parser.getAttributeValue(null  /* ns */, "name"));
+    extendedProperty.setValue(parser.getAttributeValue(null  /* ns */, "value"));
+    extendedProperty.setXmlBlob(XmlUtils.extractFirstChildTextIgnoreRest(parser));
+  }
+}
diff --git a/src/com/google/wireless/gdata2/contacts/parser/xml/XmlContactsGDataParserFactory.java b/src/com/google/wireless/gdata2/contacts/parser/xml/XmlContactsGDataParserFactory.java
new file mode 100644
index 0000000..2eb3209
--- /dev/null
+++ b/src/com/google/wireless/gdata2/contacts/parser/xml/XmlContactsGDataParserFactory.java
@@ -0,0 +1,125 @@
+// Copyright 2007 The Android Open Source Project
+
+package com.google.wireless.gdata2.contacts.parser.xml;
+
+import com.google.wireless.gdata2.client.GDataParserFactory;
+import com.google.wireless.gdata2.contacts.data.ContactEntry;
+import com.google.wireless.gdata2.contacts.data.GroupEntry;
+import com.google.wireless.gdata2.data.MediaEntry;
+import com.google.wireless.gdata2.contacts.serializer.xml.XmlContactEntryGDataSerializer;
+import com.google.wireless.gdata2.contacts.serializer.xml.XmlGroupEntryGDataSerializer;
+import com.google.wireless.gdata2.data.Entry;
+import com.google.wireless.gdata2.parser.GDataParser;
+import com.google.wireless.gdata2.parser.ParseException;
+import com.google.wireless.gdata2.parser.xml.XmlParserFactory;
+import com.google.wireless.gdata2.parser.xml.XmlMediaEntryGDataParser;
+import com.google.wireless.gdata2.serializer.GDataSerializer;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.InputStream;
+
+/**
+ * GDataParserFactory that creates XML GDataParsers and GDataSerializers for
+ * Google Contacts.
+ */
+public class XmlContactsGDataParserFactory implements GDataParserFactory {
+
+  private final XmlParserFactory xmlFactory;
+
+  public XmlContactsGDataParserFactory(XmlParserFactory xmlFactory) {
+    this.xmlFactory = xmlFactory;
+  }
+
+    /**
+     * Returns a parser for a contacts group feed.
+     *
+     * @param is The input stream to be parsed.
+     * @return A parser for the stream.
+     * @throws com.google.wireless.gdata2.parser.ParseException
+     */
+    public GDataParser createGroupEntryFeedParser(InputStream is) throws ParseException {
+      XmlPullParser xmlParser;
+      try {
+        xmlParser = xmlFactory.createParser();
+      } catch (XmlPullParserException xppe) {
+        throw new ParseException("Could not create XmlPullParser", xppe);
+      }
+      return new XmlGroupEntryGDataParser(is, xmlParser);
+    }
+
+  /**
+   * Returns a parser for a media entry feed.
+   *
+   * @param is The input stream to be parsed.
+   * @return A parser for the stream.
+   * @throws ParseException
+   */
+  public GDataParser createMediaEntryFeedParser(InputStream is) throws ParseException {
+    XmlPullParser xmlParser;
+    try {
+      xmlParser = xmlFactory.createParser();
+    } catch (XmlPullParserException xppe) {
+      throw new ParseException("Could not create XmlPullParser", xppe);
+    }
+    return new XmlMediaEntryGDataParser(is, xmlParser);
+  }
+
+  /*
+  * (non-javadoc)
+  *
+  * @see GDataParserFactory#createParser
+  */
+  public GDataParser createParser(InputStream is) throws ParseException {
+    XmlPullParser xmlParser;
+    try {
+      xmlParser = xmlFactory.createParser();
+    } catch (XmlPullParserException xppe) {
+      throw new ParseException("Could not create XmlPullParser", xppe);
+    }
+    return new XmlContactsGDataParser(is, xmlParser);
+  }
+
+  /*
+  * (non-Javadoc)
+  *
+  * @see com.google.wireless.gdata2.client.GDataParserFactory#createParser(
+  *      int, java.io.InputStream)
+  */
+  public GDataParser createParser(Class entryClass, InputStream is)
+      throws ParseException {
+    if (entryClass == ContactEntry.class) {
+      return createParser(is);
+    }
+    if (entryClass == GroupEntry.class) {
+      return createGroupEntryFeedParser(is);
+    }
+    if (entryClass == MediaEntry.class) {
+      return createMediaEntryFeedParser(is);
+    }
+    throw new IllegalArgumentException("unexpected feed type, " + entryClass.getName());
+  }
+
+  /**
+   * Creates a new {@link GDataSerializer} for the provided entry. The entry
+   * <strong>must</strong> be an instance of {@link ContactEntry} or {@link GroupEntry}.
+   *
+   * @param entry The {@link ContactEntry} that should be serialized.
+   * @return The {@link GDataSerializer} that will serialize this entry.
+   * @throws IllegalArgumentException Thrown if entry is not a
+   *         {@link ContactEntry} or {@link GroupEntry}.
+   * @see com.google.wireless.gdata2.client.GDataParserFactory#createSerializer
+   */
+  public GDataSerializer createSerializer(Entry entry) {
+    if (entry instanceof ContactEntry) {
+      ContactEntry contactEntry = (ContactEntry) entry;
+      return new XmlContactEntryGDataSerializer(xmlFactory, contactEntry);
+    }
+    if (entry instanceof GroupEntry) {
+      GroupEntry groupEntry = (GroupEntry) entry;
+      return new XmlGroupEntryGDataSerializer(xmlFactory, groupEntry);
+    }
+    throw new IllegalArgumentException("unexpected entry type, " + entry.getClass().toString());
+  }
+}
diff --git a/src/com/google/wireless/gdata2/contacts/parser/xml/XmlGroupEntryGDataParser.java b/src/com/google/wireless/gdata2/contacts/parser/xml/XmlGroupEntryGDataParser.java
new file mode 100644
index 0000000..c663e71
--- /dev/null
+++ b/src/com/google/wireless/gdata2/contacts/parser/xml/XmlGroupEntryGDataParser.java
@@ -0,0 +1,60 @@
+package com.google.wireless.gdata2.contacts.parser.xml;
+
+import com.google.wireless.gdata2.contacts.data.GroupEntry;
+import com.google.wireless.gdata2.contacts.data.GroupsFeed;
+import com.google.wireless.gdata2.data.Entry;
+import com.google.wireless.gdata2.data.Feed;
+import com.google.wireless.gdata2.data.StringUtils;
+import com.google.wireless.gdata2.parser.ParseException;
+import com.google.wireless.gdata2.parser.xml.XmlGDataParser;
+
+import org.xmlpull.v1.XmlPullParser;
+
+import java.io.InputStream;
+
+/**
+ * GDataParser for a contact groups feed.
+ */
+public class XmlGroupEntryGDataParser extends XmlGDataParser {
+  /**
+   * Creates a new XmlGroupEntryGDataParser.
+   * @param is The InputStream that should be parsed.
+   * @param parser the XmlPullParser to use for the xml parsing
+   * @throws ParseException Thrown if a parser cannot be created.
+   */
+  public XmlGroupEntryGDataParser(InputStream is, XmlPullParser parser) throws ParseException {
+    super(is, parser);
+  }
+
+  /*
+  * (non-Javadoc)
+  * @see com.google.wireless.gdata2.parser.xml.XmlGDataParser#createFeed()
+  */
+  protected Feed createFeed() {
+    return new GroupsFeed();
+  }
+
+  /*
+  * (non-Javadoc)
+  * @see com.google.wireless.gdata2.parser.xml.XmlGDataParser#createEntry()
+  */
+  protected Entry createEntry() {
+    return new GroupEntry();
+  }
+
+  protected void handleExtraElementInEntry(Entry entry) {
+    XmlPullParser parser = getParser();
+
+    if (!(entry instanceof GroupEntry)) {
+      throw new IllegalArgumentException("Expected GroupEntry!");
+    }
+    GroupEntry groupEntry = (GroupEntry) entry;
+    String name = parser.getName();
+    if ("systemGroup".equals(name)) {
+      String systemGroup = parser.getAttributeValue(null /* ns */, "id");
+      // if the systemGroup is the empty string, convert it to a null
+      if (StringUtils.isEmpty(systemGroup)) systemGroup = null;
+      groupEntry.setSystemGroup(systemGroup);
+    }
+  }
+}
diff --git a/src/com/google/wireless/gdata2/contacts/parser/xml/package.html b/src/com/google/wireless/gdata2/contacts/parser/xml/package.html
new file mode 100644
index 0000000..1c9bf9d
--- /dev/null
+++ b/src/com/google/wireless/gdata2/contacts/parser/xml/package.html
@@ -0,0 +1,5 @@
+<html>
+<body>
+    {@hide}
+</body>
+</html>
diff --git a/src/com/google/wireless/gdata2/contacts/serializer/package.html b/src/com/google/wireless/gdata2/contacts/serializer/package.html
new file mode 100644
index 0000000..1c9bf9d
--- /dev/null
+++ b/src/com/google/wireless/gdata2/contacts/serializer/package.html
@@ -0,0 +1,5 @@
+<html>
+<body>
+    {@hide}
+</body>
+</html>
diff --git a/src/com/google/wireless/gdata2/contacts/serializer/xml/XmlContactEntryGDataSerializer.java b/src/com/google/wireless/gdata2/contacts/serializer/xml/XmlContactEntryGDataSerializer.java
new file mode 100644
index 0000000..4387196
--- /dev/null
+++ b/src/com/google/wireless/gdata2/contacts/serializer/xml/XmlContactEntryGDataSerializer.java
@@ -0,0 +1,256 @@
+// Copyright 2007 The Android Open Source Project
+
+package com.google.wireless.gdata2.contacts.serializer.xml;
+
+import com.google.wireless.gdata2.contacts.data.ContactEntry;
+import com.google.wireless.gdata2.contacts.data.ContactsElement;
+import com.google.wireless.gdata2.contacts.data.EmailAddress;
+import com.google.wireless.gdata2.contacts.data.ImAddress;
+import com.google.wireless.gdata2.contacts.data.Organization;
+import com.google.wireless.gdata2.contacts.data.PhoneNumber;
+import com.google.wireless.gdata2.contacts.data.PostalAddress;
+import com.google.wireless.gdata2.contacts.data.GroupMembershipInfo;
+import com.google.wireless.gdata2.contacts.parser.xml.XmlContactsGDataParser;
+import com.google.wireless.gdata2.data.StringUtils;
+import com.google.wireless.gdata2.data.ExtendedProperty;
+import com.google.wireless.gdata2.parser.ParseException;
+import com.google.wireless.gdata2.parser.xml.XmlGDataParser;
+import com.google.wireless.gdata2.parser.xml.XmlParserFactory;
+import com.google.wireless.gdata2.serializer.xml.XmlEntryGDataSerializer;
+
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.IOException;
+import java.util.Enumeration;
+import java.util.Hashtable;
+
+/**
+ *  Serializes Google Contact entries into the Atom XML format.
+ */
+public class XmlContactEntryGDataSerializer extends XmlEntryGDataSerializer {
+
+  public XmlContactEntryGDataSerializer(XmlParserFactory factory, ContactEntry entry) {
+    super(factory, entry);
+  }
+
+  @Override
+  protected void declareExtraEntryNamespaces(XmlSerializer serializer) throws IOException {
+    super.declareExtraEntryNamespaces(serializer);
+    serializer.setPrefix(XmlContactsGDataParser.NAMESPACE_CONTACTS,
+        XmlContactsGDataParser.NAMESPACE_CONTACTS_URI);
+  }
+
+  protected ContactEntry getContactEntry() {
+    return (ContactEntry) getEntry();
+  }
+
+  /* (non-Javadoc)
+  * @see XmlEntryGDataSerializer#serializeExtraEntryContents
+  */
+  protected void serializeExtraEntryContents(XmlSerializer serializer, int format)
+      throws ParseException, IOException {
+    ContactEntry entry = getContactEntry();
+    entry.validate();
+
+    serializeLink(serializer, XmlContactsGDataParser.LINK_REL_EDIT_PHOTO,
+        entry.getLinkEditPhotoHref(), entry.getLinkEditPhotoType());
+    serializeLink(serializer, XmlContactsGDataParser.LINK_REL_PHOTO,
+        entry.getLinkPhotoHref(), entry.getLinkPhotoType());
+
+    // Serialize the contact specific parts of this entry.  Note that
+    // gd:ContactSection and gd:geoPt are likely to be deprecated, and
+    // are not currently serialized.
+    Enumeration eachEmail = entry.getEmailAddresses().elements();
+    while (eachEmail.hasMoreElements()) {
+      serialize(serializer, (EmailAddress) eachEmail.nextElement());
+    }
+
+    Enumeration eachIm = entry.getImAddresses().elements();
+    while (eachIm.hasMoreElements()) {
+      serialize(serializer, (ImAddress) eachIm.nextElement());
+    }
+
+    Enumeration eachPhone = entry.getPhoneNumbers().elements();
+    while (eachPhone.hasMoreElements()) {
+      serialize(serializer, (PhoneNumber) eachPhone.nextElement());
+    }
+
+    Enumeration eachAddress = entry.getPostalAddresses().elements();
+    while (eachAddress.hasMoreElements()) {
+      serialize(serializer, (PostalAddress) eachAddress.nextElement());
+    }
+
+    Enumeration eachOrganization = entry.getOrganizations().elements();
+    while (eachOrganization.hasMoreElements()) {
+      serialize(serializer, (Organization) eachOrganization.nextElement());
+    }
+
+    Enumeration eachExtendedProperty = entry.getExtendedProperties().elements();
+    while (eachExtendedProperty.hasMoreElements()) {
+      serialize(serializer, (ExtendedProperty) eachExtendedProperty.nextElement());
+    }
+
+    Enumeration eachGroup = entry.getGroups().elements();
+    while (eachGroup.hasMoreElements()) {
+      serialize(serializer, (GroupMembershipInfo) eachGroup.nextElement());
+    }
+
+    serializeYomiName(serializer, entry.getYomiName());
+  }
+
+  private static void serialize(XmlSerializer serializer, EmailAddress email)
+      throws IOException, ParseException {
+    if (StringUtils.isEmptyOrWhitespace(email.getAddress())) return;
+    serializer.startTag(XmlGDataParser.NAMESPACE_GD_URI, "email");
+    serializeContactsElement(serializer, email, XmlContactsGDataParser.TYPE_TO_REL_EMAIL);
+    serializer.attribute(null /* ns */, "address", email.getAddress());
+    serializer.endTag(XmlGDataParser.NAMESPACE_GD_URI, "email");
+  }
+
+  private static void serialize(XmlSerializer serializer, ImAddress im)
+      throws IOException, ParseException {
+    if (StringUtils.isEmptyOrWhitespace(im.getAddress())) return;
+
+    serializer.startTag(XmlGDataParser.NAMESPACE_GD_URI, "im");
+    serializeContactsElement(serializer, im, XmlContactsGDataParser.TYPE_TO_REL_IM);
+    serializer.attribute(null /* ns */, "address", im.getAddress());
+
+    String protocolString;
+    switch (im.getProtocolPredefined()) {
+      case ImAddress.PROTOCOL_NONE:
+        // don't include the attribute if no protocol was specified
+        break;
+
+      case ImAddress.PROTOCOL_CUSTOM:
+        protocolString = im.getProtocolCustom();
+        if (protocolString == null) {
+          throw new IllegalArgumentException(
+              "the protocol is custom, but the custom string is null");
+        }
+        serializer.attribute(null /* ns */, "protocol", protocolString);
+        break;
+
+      default:
+        protocolString = (String)XmlContactsGDataParser.IM_PROTOCOL_TYPE_TO_STRING_MAP.get(
+            new Byte(im.getProtocolPredefined()));
+        serializer.attribute(null /* ns */, "protocol", protocolString);
+        break;
+    }
+
+    serializer.endTag(XmlGDataParser.NAMESPACE_GD_URI, "im");
+  }
+
+  private static void serialize(XmlSerializer serializer, PhoneNumber phone)
+      throws IOException, ParseException {
+    if (StringUtils.isEmptyOrWhitespace(phone.getPhoneNumber())) return;
+    serializer.startTag(XmlGDataParser.NAMESPACE_GD_URI, "phoneNumber");
+    serializeContactsElement(serializer, phone, XmlContactsGDataParser.TYPE_TO_REL_PHONE);
+    serializer.text(phone.getPhoneNumber());
+    serializer.endTag(XmlGDataParser.NAMESPACE_GD_URI, "phoneNumber");
+  }
+
+  private static void serialize(XmlSerializer serializer, Organization organization)
+      throws IOException, ParseException {
+    final String name = organization.getName();
+    final String title = organization.getTitle();
+
+    if (StringUtils.isEmptyOrWhitespace(name) && StringUtils.isEmptyOrWhitespace(title)) return;
+
+    serializer.startTag(XmlGDataParser.NAMESPACE_GD_URI, "organization");
+    serializeContactsElement(serializer,
+            organization, XmlContactsGDataParser.TYPE_TO_REL_ORGANIZATION);
+    if (!StringUtils.isEmpty(name)) {
+      serializer.startTag(XmlGDataParser.NAMESPACE_GD_URI, "orgName");
+      serializer.text(name);
+      serializer.endTag(XmlGDataParser.NAMESPACE_GD_URI, "orgName");
+    }
+
+    if (!StringUtils.isEmpty(title)) {
+      serializer.startTag(XmlGDataParser.NAMESPACE_GD_URI, "orgTitle");
+      serializer.text(title);
+      serializer.endTag(XmlGDataParser.NAMESPACE_GD_URI, "orgTitle");
+    }
+    serializer.endTag(XmlGDataParser.NAMESPACE_GD_URI, "organization");
+  }
+
+  private static void serialize(XmlSerializer serializer, PostalAddress addr)
+      throws IOException, ParseException {
+    if (StringUtils.isEmptyOrWhitespace(addr.getValue())) return;
+    serializer.startTag(XmlGDataParser.NAMESPACE_GD_URI, "postalAddress");
+    serializeContactsElement(serializer, addr, XmlContactsGDataParser.TYPE_TO_REL_POSTAL);
+    final String addressValue = addr.getValue();
+    if (addressValue != null) serializer.text(addressValue);
+    serializer.endTag(XmlGDataParser.NAMESPACE_GD_URI, "postalAddress");
+  }
+
+  private static void serializeContactsElement(XmlSerializer serializer, ContactsElement element,
+      Hashtable typeToRelMap) throws IOException, ParseException {
+    final String label = element.getLabel();
+    boolean hasType = element.getType() != ContactsElement.TYPE_NONE;
+
+    if (((label == null) && !hasType) || ((label != null) && hasType)) {
+      throw new ParseException("exactly one of label or rel must be set");
+    }
+
+    if (label != null) {
+      serializer.attribute(null /* ns */, "label", label);
+    }
+    if (hasType) {
+      serializer.attribute(null /* ns */, "rel",
+          (String)typeToRelMap.get(new Byte(element.getType())));
+    }
+    if (element.isPrimary()) {
+      serializer.attribute(null /* ns */, "primary", "true");
+    }
+  }
+
+  private static void serialize(XmlSerializer serializer, GroupMembershipInfo groupMembershipInfo)
+      throws IOException, ParseException {
+    final String group = groupMembershipInfo.getGroup();
+    final boolean isDeleted = groupMembershipInfo.isDeleted();
+
+    if (StringUtils.isEmptyOrWhitespace(group)) {
+      throw new ParseException("the group must not be empty");
+    }
+
+    serializer.startTag(XmlContactsGDataParser.NAMESPACE_CONTACTS_URI, "groupMembershipInfo");
+    serializer.attribute(null /* ns */, "href", group);
+    serializer.attribute(null /* ns */, "deleted", isDeleted ? "true" : "false");
+    serializer.endTag(XmlContactsGDataParser.NAMESPACE_CONTACTS_URI, "groupMembershipInfo");
+  }
+
+  private static void serialize(XmlSerializer serializer, ExtendedProperty extendedProperty)
+      throws IOException, ParseException {
+    final String name = extendedProperty.getName();
+    final String value = extendedProperty.getValue();
+    final String xmlBlob = extendedProperty.getXmlBlob();
+
+    serializer.startTag(XmlGDataParser.NAMESPACE_GD_URI, "extendedProperty");
+    if (!StringUtils.isEmpty(name)) {
+      serializer.attribute(null /* ns */, "name", name);
+    }
+    if (!StringUtils.isEmpty(value)) {
+      serializer.attribute(null /* ns */, "value", value);
+    }
+    if (!StringUtils.isEmpty(xmlBlob)) {
+      serializeBlob(serializer, xmlBlob);
+    }
+    serializer.endTag(XmlGDataParser.NAMESPACE_GD_URI, "extendedProperty");
+  }
+
+  private static void serializeBlob(XmlSerializer serializer, String blob)
+      throws IOException, ParseException {
+     serializer.text(blob);
+  }
+
+  private static void serializeYomiName(XmlSerializer serializer,
+      String yomiName)
+      throws IOException {
+    if (StringUtils.isEmpty(yomiName)) {
+      return;
+    }
+    serializer.startTag(XmlContactsGDataParser.NAMESPACE_CONTACTS_URI, "yomiName");
+    serializer.text(yomiName);
+    serializer.endTag(XmlContactsGDataParser.NAMESPACE_CONTACTS_URI, "yomiName");
+  }
+}
diff --git a/src/com/google/wireless/gdata2/contacts/serializer/xml/XmlGroupEntryGDataSerializer.java b/src/com/google/wireless/gdata2/contacts/serializer/xml/XmlGroupEntryGDataSerializer.java
new file mode 100644
index 0000000..d34cc43
--- /dev/null
+++ b/src/com/google/wireless/gdata2/contacts/serializer/xml/XmlGroupEntryGDataSerializer.java
@@ -0,0 +1,53 @@
+package com.google.wireless.gdata2.contacts.serializer.xml;
+
+import com.google.wireless.gdata2.contacts.data.GroupEntry;
+import com.google.wireless.gdata2.contacts.parser.xml.XmlContactsGDataParser;
+import com.google.wireless.gdata2.parser.xml.XmlParserFactory;
+import com.google.wireless.gdata2.parser.ParseException;
+import com.google.wireless.gdata2.serializer.xml.XmlEntryGDataSerializer;
+import com.google.wireless.gdata2.data.StringUtils;
+
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.IOException;
+
+/**
+ *  Serializes Google Contact Group entries into the Atom XML format.
+ */
+public class XmlGroupEntryGDataSerializer extends XmlEntryGDataSerializer {
+
+  public XmlGroupEntryGDataSerializer(XmlParserFactory factory, GroupEntry entry) {
+    super(factory, entry);
+  }
+
+  protected GroupEntry getGroupEntry() {
+    return (GroupEntry) getEntry();
+  }
+
+  @Override
+  protected void declareExtraEntryNamespaces(XmlSerializer serializer) throws IOException {
+    super.declareExtraEntryNamespaces(serializer);
+    serializer.setPrefix(XmlContactsGDataParser.NAMESPACE_CONTACTS,
+        XmlContactsGDataParser.NAMESPACE_CONTACTS_URI);
+  }
+
+  /* (non-Javadoc)
+  * @see XmlEntryGDataSerializer#serializeExtraEntryContents
+  */
+  protected void serializeExtraEntryContents(XmlSerializer serializer, int format)
+      throws ParseException, IOException {
+    GroupEntry entry = getGroupEntry();
+    entry.validate();
+
+    serializeSystemGroup(entry, serializer);
+  }
+
+  private void serializeSystemGroup(GroupEntry entry, XmlSerializer serializer) throws IOException {
+    final String systemGroup = entry.getSystemGroup();
+    if (!StringUtils.isEmpty(systemGroup)) {
+      serializer.startTag(null /* ns */, "systemGroup");
+      serializer.attribute(null /* ns */, "id", systemGroup);
+      serializer.endTag(null /* ns */, "systemGroup");
+    }
+  }
+}
diff --git a/src/com/google/wireless/gdata2/contacts/serializer/xml/package.html b/src/com/google/wireless/gdata2/contacts/serializer/xml/package.html
new file mode 100644
index 0000000..1c9bf9d
--- /dev/null
+++ b/src/com/google/wireless/gdata2/contacts/serializer/xml/package.html
@@ -0,0 +1,5 @@
+<html>
+<body>
+    {@hide}
+</body>
+</html>
diff --git a/src/com/google/wireless/gdata2/data/Entry.java b/src/com/google/wireless/gdata2/data/Entry.java
new file mode 100644
index 0000000..1c191d4
--- /dev/null
+++ b/src/com/google/wireless/gdata2/data/Entry.java
@@ -0,0 +1,291 @@
+// Copyright 2007 The Android Open Source Project
+
+package com.google.wireless.gdata2.data;
+
+import com.google.wireless.gdata2.parser.ParseException;
+
+/**
+ * Entry in a GData feed.
+ */
+// TODO: make this an interface?
+// allow for writing directly into data structures used by native PIM, etc.,
+// APIs.
+// TODO: comment that setId(), etc., only used for parsing code.
+public class Entry {
+    private String id = null;
+    private String title = null;
+    private String editUri = null;
+    private String htmlUri = null;
+    private String summary = null;
+    private String content = null;
+    private String author = null;
+    private String email = null;
+    private String category = null;
+    private String categoryScheme = null;
+    private String publicationDate = null;
+    private String updateDate = null;
+    private boolean deleted = false;
+    
+    /**
+     * Creates a new empty entry.
+     */
+    public Entry() {
+    }
+
+    /**
+     * Clears all the values in this entry.
+     */
+    public void clear() {
+        id = null;
+        title = null;
+        editUri = null;
+        htmlUri = null;
+        summary = null;
+        content = null;
+        author = null;
+        email = null;
+        category = null;
+        categoryScheme = null;
+        publicationDate = null;
+        updateDate = null;
+        deleted = false;
+    }
+
+    /**
+     * @return the author
+     */
+    public String getAuthor() {
+        return author;
+    }
+
+    /**
+     * @param author the author to set
+     */
+    public void setAuthor(String author) {
+        this.author = author;
+    }
+
+    /**
+     * @return the category
+     */
+    public String getCategory() {
+        return category;
+    }
+
+    /**
+     * @param category the category to set
+     */
+    public void setCategory(String category) {
+        this.category = category;
+    }
+
+    /**
+     * @return the categoryScheme
+     */
+    public String getCategoryScheme() {
+        return categoryScheme;
+    }
+
+    /**
+     * @param categoryScheme the categoryScheme to set
+     */
+    public void setCategoryScheme(String categoryScheme) {
+        this.categoryScheme = categoryScheme;
+    }
+
+    /**
+     * @return the content
+     */
+    public String getContent() {
+        return content;
+    }
+
+    /**
+     * @param content the content to set
+     */
+    public void setContent(String content) {
+        this.content = content;
+    }
+
+    /**
+     * @return the editUri
+     */
+    public String getEditUri() {
+        return editUri;
+    }
+
+    /**
+     * @param editUri the editUri to set
+     */
+    public void setEditUri(String editUri) {
+        this.editUri = editUri;
+    }
+
+    /**
+     * @return The uri for the HTML version of this entry.
+     */
+    public String getHtmlUri() {
+        return htmlUri;
+    }
+
+    /**
+     * Set the uri for the HTML version of this entry.
+     * @param htmlUri The uri for the HTML version of this entry.
+     */
+    public void setHtmlUri(String htmlUri) {
+        this.htmlUri = htmlUri;
+    }
+
+    /**
+     * @return the id
+     */
+    public String getId() {
+        return id;
+    }
+
+    /**
+     * @param id the id to set
+     */
+    public void setId(String id) {
+        this.id = id;
+    }
+
+    /**
+     * @return the publicationDate
+     */
+    public String getPublicationDate() {
+        return publicationDate;
+    }
+
+    /**
+     * @param publicationDate the publicationDate to set
+     */
+    public void setPublicationDate(String publicationDate) {
+        this.publicationDate = publicationDate;
+    }
+
+    /**
+     * @return the summary
+     */
+    public String getSummary() {
+        return summary;
+    }
+
+    /**
+     * @param summary the summary to set
+     */
+    public void setSummary(String summary) {
+        this.summary = summary;
+    }
+
+    /**
+     * @return the title
+     */
+    public String getTitle() {
+        return title;
+    }
+
+    /**
+     * @param title the title to set
+     */
+    public void setTitle(String title) {
+        this.title = title;
+    }
+
+    /**
+     * @return the updateDate
+     */
+    public String getUpdateDate() {
+        return updateDate;
+    }
+
+    /**
+     * @param updateDate the updateDate to set
+     */
+    public void setUpdateDate(String updateDate) {
+        this.updateDate = updateDate;
+    }
+
+    /**
+     * @return true if this entry represents a tombstone
+     */
+    public boolean isDeleted() {
+        return deleted;
+    }
+
+    /**
+     * @param isDeleted true if the entry is deleted
+     */
+    public void setDeleted(boolean isDeleted) {
+        deleted = isDeleted;
+    }
+ 
+    /**
+     * Appends the name and value to this StringBuffer, if value is not null.
+     * Uses the format: "<NAME>: <VALUE>\n"
+     * @param sb The StringBuffer in which the name and value should be
+     * appended.
+     * @param name The name that should be appended.
+     * @param value The value that should be appended.
+     */
+    protected void appendIfNotNull(StringBuffer sb,
+                                   String name, String value) {
+        if (!StringUtils.isEmpty(value)) {
+            sb.append(name);
+            sb.append(": ");
+            sb.append(value);
+            sb.append("\n");
+        }
+    }
+
+    /**
+     * Helper method that creates the String representation of this Entry.
+     * Called by {@link #toString()}.
+     * Subclasses can add additional data to the StringBuffer.
+     * @param sb The StringBuffer that should be modified to add to the String
+     * representation of this Entry.
+     */
+    protected void toString(StringBuffer sb) {
+        appendIfNotNull(sb, "ID", id);
+        appendIfNotNull(sb, "TITLE", title);
+        appendIfNotNull(sb, "EDIT URI", editUri);
+        appendIfNotNull(sb, "HTML URI", htmlUri);        
+        appendIfNotNull(sb, "SUMMARY", summary);
+        appendIfNotNull(sb, "CONTENT", content);
+        appendIfNotNull(sb, "AUTHOR", author);
+        appendIfNotNull(sb, "CATEGORY", category);
+        appendIfNotNull(sb, "CATEGORY SCHEME", categoryScheme);
+        appendIfNotNull(sb, "PUBLICATION DATE", publicationDate);
+        appendIfNotNull(sb, "UPDATE DATE", updateDate);
+        appendIfNotNull(sb, "DELETED", String.valueOf(deleted));
+    }
+
+    /**
+     * Creates a StringBuffer and calls {@link #toString(StringBuffer)}.  The
+     * return value for this method is simply the result of calling
+     * {@link StringBuffer#toString()} on this StringBuffer.  Mainly used for
+     * debugging.
+     */
+    public String toString() {
+        StringBuffer sb = new StringBuffer();
+        toString(sb);
+        return sb.toString();
+    }
+
+    /**
+     * @return the email
+     */
+    public String getEmail() {
+        return email;
+    }
+
+    /**
+     * @param email the email to set
+     */
+    public void setEmail(String email) {
+        this.email = email;
+    }
+
+    public void validate() throws ParseException {
+    }
+}
diff --git a/src/com/google/wireless/gdata2/data/ExtendedProperty.java b/src/com/google/wireless/gdata2/data/ExtendedProperty.java
new file mode 100644
index 0000000..09b4a4b
--- /dev/null
+++ b/src/com/google/wireless/gdata2/data/ExtendedProperty.java
@@ -0,0 +1,53 @@
+package com.google.wireless.gdata2.data;
+
+import com.google.wireless.gdata2.parser.ParseException;
+
+/**
+ * The extendedProperty gdata type
+ */
+public class ExtendedProperty {
+  private String name;
+  private String value;
+  private String xmlBlob;
+
+  public String getXmlBlob() {
+    return xmlBlob;
+  }
+
+  public void setXmlBlob(String xmlBlob) {
+    this.xmlBlob = xmlBlob;
+  }
+
+  public String getName() {
+    return name;
+  }
+
+  public void setName(String name) {
+    this.name = name;
+  }
+
+  public String getValue() {
+    return value;
+  }
+
+  public void setValue(String value) {
+    this.value = value;
+  }
+
+  public void toString(StringBuffer sb) {
+    sb.append("ExtendedProperty");
+    if (name != null) sb.append(" name:").append(name);
+    if (value != null) sb.append(" value:").append(value);
+    if (xmlBlob != null) sb.append(" xmlBlob:").append(xmlBlob);
+  }
+
+  public void validate() throws ParseException {
+    if (name == null) {
+      throw new ParseException("name must not be null");
+    }
+
+    if ((value == null && xmlBlob == null) || (value != null && xmlBlob != null)) {
+      throw new ParseException("exactly one of value and xmlBlob must be present");
+    }
+  }
+}
diff --git a/src/com/google/wireless/gdata2/data/Feed.java b/src/com/google/wireless/gdata2/data/Feed.java
new file mode 100644
index 0000000..201a134
--- /dev/null
+++ b/src/com/google/wireless/gdata2/data/Feed.java
@@ -0,0 +1,122 @@
+// Copyright 2007 The Android Open Source Project
+
+package com.google.wireless.gdata2.data;
+
+/**
+ * Class containing information about a GData feed.  Note that this feed does
+ * not contain any of the entries in that feed -- the entries are yielded
+ * separately from this Feed.
+ */
+// TODO: add a createEntry method?
+// TODO: comment that setters are only used for parsing code.
+public class Feed {
+    private int totalResults;
+    private int startIndex;
+    private int itemsPerPage;
+    private String title;
+    private String id;
+    private String lastUpdated;
+    private String category;
+    private String categoryScheme;
+
+    /**
+     * Creates a new, empty feed.
+     */
+    public Feed() {
+    }
+
+    public int getTotalResults() {
+        return totalResults;
+    }
+
+    public void setTotalResults(int totalResults) {
+        this.totalResults = totalResults;
+    }
+
+    public int getStartIndex() {
+        return startIndex;
+    }
+
+    public void setStartIndex(int startIndex) {
+        this.startIndex = startIndex;
+    }
+
+    public int getItemsPerPage() {
+        return itemsPerPage;
+    }
+
+    public void setItemsPerPage(int itemsPerPage) {
+        this.itemsPerPage = itemsPerPage;
+    }
+
+    /**
+     * @return the category
+     */
+    public String getCategory() {
+        return category;
+    }
+
+    /**
+     * @param category the category to set
+     */
+    public void setCategory(String category) {
+        this.category = category;
+    }
+
+    /**
+     * @return the categoryScheme
+     */
+    public String getCategoryScheme() {
+        return categoryScheme;
+    }
+
+    /**
+     * @param categoryScheme the categoryScheme to set
+     */
+    public void setCategoryScheme(String categoryScheme) {
+        this.categoryScheme = categoryScheme;
+    }
+
+    /**
+     * @return the id
+     */
+    public String getId() {
+        return id;
+    }
+
+    /**
+     * @param id the id to set
+     */
+    public void setId(String id) {
+        this.id = id;
+    }
+
+    /**
+     * @return the lastUpdated
+     */
+    public String getLastUpdated() {
+        return lastUpdated;
+    }
+
+    /**
+     * @param lastUpdated the lastUpdated to set
+     */
+    public void setLastUpdated(String lastUpdated) {
+        this.lastUpdated = lastUpdated;
+    }
+
+    /**
+     * @return the title
+     */
+    public String getTitle() {
+        return title;
+    }
+
+    /**
+     * @param title the title to set
+     */
+    public void setTitle(String title) {
+        this.title = title;
+    }
+
+}
diff --git a/src/com/google/wireless/gdata2/data/MediaEntry.java b/src/com/google/wireless/gdata2/data/MediaEntry.java
new file mode 100644
index 0000000..fefe001
--- /dev/null
+++ b/src/com/google/wireless/gdata2/data/MediaEntry.java
@@ -0,0 +1,10 @@
+package com.google.wireless.gdata2.data;
+
+/**
+ * Entry containing information about media entries
+ */
+public class MediaEntry extends Entry {
+  public MediaEntry() {
+    super();
+  }
+}
diff --git a/src/com/google/wireless/gdata2/data/StringUtils.java b/src/com/google/wireless/gdata2/data/StringUtils.java
new file mode 100644
index 0000000..c6a125e
--- /dev/null
+++ b/src/com/google/wireless/gdata2/data/StringUtils.java
@@ -0,0 +1,54 @@
+// Copyright 2007 The Android Open Source Project
+
+package com.google.wireless.gdata2.data;
+
+/**
+ * Utility class for working with and manipulating Strings.
+ */
+public final class StringUtils {
+    // utility class
+    private StringUtils() {
+    }
+    
+    /**
+     * Returns whether or not the String is empty.  A String is considered to
+     * be empty if it is null or if it has a length of 0.
+     * @param string The String that should be examined.
+     * @return Whether or not the String is empty.
+     */
+    public static boolean isEmpty(String string) {
+        return ((string == null) || (string.length() == 0));
+    }
+
+    /**
+     * Returns {@code true} if the given string is null, empty, or comprises only
+     * whitespace characters, as defined by {@link Character#isWhitespace(char)}.
+     *
+     * @param string The String that should be examined.
+     * @return {@code true} if {@code string} is null, empty, or consists of
+     *     whitespace characters only
+     */
+    public static boolean isEmptyOrWhitespace(String string) {
+        if (string == null) {
+            return true;
+        }
+        int length = string.length();
+        for (int i = 0; i < length; i++) {
+            if (!Character.isWhitespace(string.charAt(i))) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    public static int parseInt(String string, int defaultValue) {
+        if (string != null) {
+            try {
+                return Integer.parseInt(string);
+            } catch (NumberFormatException nfe) {
+                // ignore
+            }
+        }
+        return defaultValue;
+    }
+}
diff --git a/src/com/google/wireless/gdata2/data/XmlUtils.java b/src/com/google/wireless/gdata2/data/XmlUtils.java
new file mode 100644
index 0000000..5224e47
--- /dev/null
+++ b/src/com/google/wireless/gdata2/data/XmlUtils.java
@@ -0,0 +1,133 @@
+// Copyright 2007 The Android Open Source Project
+
+package com.google.wireless.gdata2.data;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.IOException;
+
+/**
+ * Utility class for working with an XmlPullParser.
+ */
+public final class XmlUtils {
+    // utility class
+    private XmlUtils() {
+    }
+
+    /**
+     * Extracts the child text for the current element in the pull parser.
+     * @param parser The XmlPullParser parsing an XML document. 
+     * @return The child text for the current element.  May be null, if there
+     * is no child text.
+     * @throws XmlPullParserException Thrown if the child text could not be
+     * parsed.
+     * @throws IOException Thrown if the InputStream behind the parser cannot
+     * be read.
+     */
+    public static String extractChildText(XmlPullParser parser) 
+        throws XmlPullParserException, IOException {
+        // TODO: check that the current node is an element?
+        int eventType = parser.next();
+        if (eventType != XmlPullParser.TEXT) {
+            return null;
+        }
+        return parser.getText();
+    }
+
+    public static String extractFirstChildTextIgnoreRest(XmlPullParser parser)
+            throws XmlPullParserException, IOException {
+        int parentDepth = parser.getDepth();
+        int eventType = parser.next();
+        String child = null;
+        while (eventType != XmlPullParser.END_DOCUMENT) {
+            int depth = parser.getDepth();
+
+            if (eventType == XmlPullParser.TEXT) {
+                if (child == null) {
+                    child = parser.getText();
+                }
+            } else if (eventType == XmlPullParser.END_TAG && depth == parentDepth) {
+                return child;
+            }
+            eventType = parser.next();
+        }
+        throw new XmlPullParserException("End of document reached; never saw expected end tag at "
+                + "depth " + parentDepth);
+    }
+
+    public static String nextDirectChildTag(XmlPullParser parser, int parentDepth)
+            throws XmlPullParserException, IOException {
+        int targetDepth = parentDepth + 1;
+        int eventType = parser.next();
+        while (eventType != XmlPullParser.END_DOCUMENT) {
+            int depth = parser.getDepth();
+
+            if (eventType == XmlPullParser.START_TAG && depth == targetDepth) {
+                return parser.getName();
+            }
+
+            if (eventType == XmlPullParser.END_TAG && depth == parentDepth) {
+                return null;
+            }
+            eventType = parser.next();            
+        }
+        throw new XmlPullParserException("End of document reached; never saw expected end tag at "
+                + "depth " + parentDepth);
+    }
+
+//    public static void parseChildrenToSerializer(XmlPullParser parser, XmlSerializer serializer)
+//            throws XmlPullParserException, IOException {
+//        int parentDepth = parser.getDepth();
+//        int eventType = parser.getEventType();
+//        while (eventType != XmlPullParser.END_DOCUMENT) {
+//            // TODO: call parser.nextToken(), so we get all entities, comments, whitespace, etc.?
+//            // find out if this is necessary.
+//            eventType = parser.next();
+//            int depth = parser.getDepth();
+//            String name;
+//            String ns;
+//            switch (eventType) {
+//                case XmlPullParser.START_TAG:
+//                    name = parser.getName();
+//                    ns = parser.getNamespace();
+//                    // grab all of the namespace definitions between the previous depth and the
+//                    // current depth (e.g., what was just defined in the start tag).
+//                    int nstackBegin = parser.getNamespaceCount(depth - 1);
+//                    int nstackEnd = parser.getNamespaceCount(depth);
+//                    for (int i = nstackBegin; i < nstackEnd; ++i) {
+//                        serializer.setPrefix(parser.getNamespacePrefix(i),
+//                                parser.getNamespaceUri(i));
+//                    }
+//                    serializer.startTag(ns, name);
+//
+//                    int numAttrs = parser.getAttributeCount();
+//                    for (int i = 0; i < numAttrs; ++i) {
+//                        String attrNs = parser.getAttributeNamespace(i);
+//                        String attrName = parser.getAttributeName(i);
+//                        String attrValue = parser.getAttributeValue(i);
+//                        serializer.attribute(attrNs, attrName, attrValue);
+//                    }
+//                    break;
+//                case XmlPullParser.END_TAG:
+//                    if (depth == parentDepth) {
+//                        // we're done.
+//                        return;
+//                    }
+//                    name = parser.getName();
+//                    ns = parser.getNamespace();
+//                    serializer.endTag(ns, name);
+//                    break;
+//                case XmlPullParser.TEXT:
+//                    serializer.text(parser.getText());
+//                    break;
+//                default:
+//                    // ignore the rest.
+//                    break;
+//            }
+//        }
+//        throw new XmlPullParserException("End of document reached; never saw expected end tag "
+//            + "at depth " + parentDepth);
+//    }
+}
diff --git a/src/com/google/wireless/gdata2/data/package.html b/src/com/google/wireless/gdata2/data/package.html
new file mode 100644
index 0000000..1c9bf9d
--- /dev/null
+++ b/src/com/google/wireless/gdata2/data/package.html
@@ -0,0 +1,5 @@
+<html>
+<body>
+    {@hide}
+</body>
+</html>
diff --git a/src/com/google/wireless/gdata2/package.html b/src/com/google/wireless/gdata2/package.html
new file mode 100644
index 0000000..1c9bf9d
--- /dev/null
+++ b/src/com/google/wireless/gdata2/package.html
@@ -0,0 +1,5 @@
+<html>
+<body>
+    {@hide}
+</body>
+</html>
diff --git a/src/com/google/wireless/gdata2/parser/GDataParser.java b/src/com/google/wireless/gdata2/parser/GDataParser.java
new file mode 100644
index 0000000..87c9ec3
--- /dev/null
+++ b/src/com/google/wireless/gdata2/parser/GDataParser.java
@@ -0,0 +1,65 @@
+// Copyright 2008 The Android Open Source Project
+
+package com.google.wireless.gdata2.parser;
+
+import com.google.wireless.gdata2.data.Entry;
+import com.google.wireless.gdata2.data.Feed;
+
+import java.io.IOException;
+
+/**
+ * Interface for parsing GData feeds.  Uses a &quot;pull&quot; model, where
+ * entries are not read or parsed until {@link #readNextEntry}
+ * is called.
+ */
+public interface GDataParser {
+
+    /**
+     * Starts parsing the feed, returning a {@link Feed} containing information
+     * about the feed.  Note that the {@link Feed} does not contain any
+     * information about any entries, as the entries have not yet been parsed.
+     *
+     * @return The {@link Feed} containing information about the parsed feed.
+     * @throws ParseException Thrown if the feed cannot be parsed.
+     */
+    // TODO: rename to parseFeed?  need to make the API clear.
+    Feed init() throws ParseException;
+
+    /**
+     * Parses a GData entry.  You can either call {@link #init()} or
+     * {@link #parseStandaloneEntry()} for a given feed.
+     *
+     * @return The parsed entry.
+     * @throws ParseException Thrown if the entry could not be parsed.
+     */
+    Entry parseStandaloneEntry() throws ParseException, IOException;
+
+    /**
+     * Returns whether or not there is more data in the feed.
+     */
+    boolean hasMoreData();
+
+    /**
+     * Reads and parses the next entry in the feed.  The {@link Entry} that
+     * should be filled is passed in -- if null, the entry will be created
+     * by the parser; if not null, the entry will be cleared and reused.
+     *
+     * @param entry The entry that should be filled.  Should be null if this is
+     * the first call to this method.  This entry is also returned as the return
+     * value.
+     *
+     * @return The {@link Entry} containing information about the parsed entry.
+     * If entry was not null, returns the same (reused) object as entry, filled
+     * with information about the entry that was just parsed.  If the entry was
+     * null, returns a newly created entry (as appropriate for the type of
+     * feed being parsed).
+     * @throws ParseException Thrown if the entry cannot be parsed.
+     */
+    Entry readNextEntry(Entry entry) throws ParseException, IOException;
+
+    /**
+     * Cleans up any state in the parser.  Should be called when caller is
+     * finished parsing a GData feed.
+     */
+    void close();
+}
diff --git a/src/com/google/wireless/gdata2/parser/ParseException.java b/src/com/google/wireless/gdata2/parser/ParseException.java
new file mode 100644
index 0000000..48786b4
--- /dev/null
+++ b/src/com/google/wireless/gdata2/parser/ParseException.java
@@ -0,0 +1,38 @@
+// Copyright 2007 The Android Open Source Project
+
+package com.google.wireless.gdata2.parser;
+
+import com.google.wireless.gdata2.GDataException;
+
+/**
+ * Exception thrown if a GData feed cannot be parsed.
+ */
+public class ParseException extends GDataException {
+    
+    /**
+     * Creates a new empty ParseException.
+     */
+    public ParseException() {
+        super();
+    }
+    
+    /**
+     * Creates a new ParseException with the supplied message.
+     * @param message The message for this ParseException.
+     */
+    public ParseException(String message) {
+        super(message);
+    }
+    
+    /**
+     * Creates a new ParseException with the supplied message and underlying
+     * cause.
+     * 
+     * @param message The message for this ParseException.
+     * @param cause The underlying cause that was caught and wrapped by this
+     * ParseException.
+     */
+    public ParseException(String message, Throwable cause) {
+        super(message, cause);
+    }
+}
diff --git a/src/com/google/wireless/gdata2/parser/package.html b/src/com/google/wireless/gdata2/parser/package.html
new file mode 100644
index 0000000..1c9bf9d
--- /dev/null
+++ b/src/com/google/wireless/gdata2/parser/package.html
@@ -0,0 +1,5 @@
+<html>
+<body>
+    {@hide}
+</body>
+</html>
diff --git a/src/com/google/wireless/gdata2/parser/xml/SimplePullParser.java b/src/com/google/wireless/gdata2/parser/xml/SimplePullParser.java
new file mode 100644
index 0000000..44fb921
--- /dev/null
+++ b/src/com/google/wireless/gdata2/parser/xml/SimplePullParser.java
@@ -0,0 +1,300 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.wireless.gdata2.parser.xml;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+
+/**
+ * This is an abstraction of a pull parser that provides several benefits:<ul>
+ *   <li>it is easier to use robustly because it makes it trivial to handle unexpected tags (which
+ *   might have children)</li>
+ *   <li>it makes the handling of text (cdata) blocks more convenient</li>
+ *   <li>it provides convenient methods for getting a mandatory attribute (and throwing an exception
+ *   if it is missing) or an optional attribute (and using a default value if it is missing)
+ * </ul>
+ */
+public class SimplePullParser {
+  public static final String TEXT_TAG = "![CDATA[";
+
+  private final XmlPullParser mParser;
+  private String mCurrentStartTag;
+
+  /**
+   * Constructs a new SimplePullParser to parse the xml
+   * @param parser the underlying parser to use
+   */
+  public SimplePullParser(XmlPullParser parser) {
+    mParser = parser;
+    mCurrentStartTag = null;
+  }
+
+  /**
+   * Returns the tag of the next element whose depth is parentDepth plus one
+   * or null if there are no more such elements before the next start tag. When this returns,
+   * getDepth() and all methods relating to attributes will refer to the element whose tag is
+   * returned.
+   *
+   * @param parentDepth the depth of the parrent of the item to be returned
+   * @param textBuffer if null then text blocks will be ignored. If
+   *   non-null then text blocks will be added to the builder and TEXT_TAG
+   *   will be returned when one is found
+   * @return the next of the next child element's tag, TEXT_TAG if a text block is found, or null
+   *   if there are no more child elements or DATA blocks
+   * @throws IOException propogated from the underlying parser
+   * @throws ParseException if there was an error parsing the xml.
+   */
+  public String nextTagOrText(int parentDepth, StringBuffer textBuffer)
+      throws IOException, ParseException {
+    while (true) {
+      int eventType = 0;
+      try {
+        eventType = mParser.next();
+      } catch (XmlPullParserException e) {
+        throw new ParseException(e);
+      }
+      int depth = mParser.getDepth();
+      mCurrentStartTag = null;
+
+      if (eventType == XmlPullParser.START_TAG && depth == parentDepth + 1) {
+        mCurrentStartTag = mParser.getName();
+          // TODO: this is an example of how to do logging of the XML
+//                if (mLogTag != null && Log.isLoggable(mLogTag, Log.DEBUG)) {
+//                    StringBuilder sb = new StringBuilder();
+//                    for (int i = 0; i < depth; i++) sb.append("  ");
+//                    sb.append("<").append(mParser.getName());
+//                    int count = mParser.getAttributeCount();
+//                    for (int i = 0; i < count; i++) {
+//                        sb.append(" ");
+//                        sb.append(mParser.getAttributeName(i));
+//                        sb.append("=\"");
+//                        sb.append(mParser.getAttributeValue(i));
+//                        sb.append("\"");
+//                    }
+//                    sb.append(">");
+//                    Log.d(mLogTag, sb.toString());
+//                }
+        return mParser.getName();
+      }
+
+      if (eventType == XmlPullParser.END_TAG && depth == parentDepth) {
+          // TODO: this is an example of how to do logging of the XML
+//                if (mLogTag != null && Log.isLoggable(mLogTag, Log.DEBUG)) {
+//                    StringBuilder sb = new StringBuilder();
+//                    for (int i = 0; i < depth; i++) sb.append("  ");
+//                    sb.append("</>"); // Not quite valid xml but it gets the job done.
+//                    Log.d(mLogTag, sb.toString());
+//                }
+        return null;
+      }
+
+      if (eventType == XmlPullParser.END_DOCUMENT && parentDepth == 0) {
+        return null;
+      }
+
+      if (eventType == XmlPullParser.TEXT && depth == parentDepth) {
+        if (textBuffer == null) {
+          continue;
+        }
+        String text = mParser.getText();
+        textBuffer.append(text);
+        return TEXT_TAG;
+      }
+    }
+  }
+
+  /**
+   * The same as nextTagOrText(int, StringBuilder) but ignores text blocks.
+   */
+  public String nextTag(int parentDepth) throws IOException, ParseException {
+    return nextTagOrText(parentDepth, null /* ignore text */);
+  }
+
+  /**
+   * Returns the depth of the current element. The depth is 0 before the first
+   * element has been returned, 1 after that, etc.
+   *
+   * @return the depth of the current element
+   */
+  public int getDepth() {
+    return mParser.getDepth();
+  }
+
+  /**
+   * Consumes the rest of the children, accumulating any text at this level into the builder.
+   *
+   * @param textBuffer
+   * @throws IOException propogated from the XmlPullParser
+   * @throws ParseException if there was an error parsing the xml.
+   */
+  public void readRemainingText(int parentDepth, StringBuffer textBuffer)
+      throws IOException, ParseException {
+    while (nextTagOrText(parentDepth, textBuffer) != null) {
+    }
+  }
+
+  /**
+   * Returns the number of attributes on the current element.
+   *
+   * @return the number of attributes on the current element
+   */
+  public int numAttributes() {
+    return mParser.getAttributeCount();
+  }
+
+  /**
+   * Returns the name of the nth attribute on the current element.
+   *
+   * @return the name of the nth attribute on the current element
+   */
+  public String getAttributeName(int i) {
+    return mParser.getAttributeName(i);
+  }
+
+  /**
+   * Returns the namespace of the nth attribute on the current element.
+   *
+   * @return the namespace of the nth attribute on the current element
+   */
+  public String getAttributeNamespace(int i) {
+    return mParser.getAttributeNamespace(i);
+  }
+
+  /**
+   * Returns the string value of the named attribute.
+   *
+   * @param namespace the namespace of the attribute
+   * @param name the name of the attribute
+   * @param defaultValue the value to return if the attribute is not specified
+   * @return the value of the attribute
+   */
+  public String getStringAttribute(
+      String namespace, String name, String defaultValue) {
+    String value = mParser.getAttributeValue(namespace, name);
+    if (null == value) return defaultValue;
+    return value;
+  }
+
+  /**
+   * Returns the string value of the named attribute. An exception will
+   * be thrown if the attribute is not present.
+   *
+   * @param namespace the namespace of the attribute
+   * @param name the name of the attribute @return the value of the attribute
+   * @throws ParseException thrown if the attribute is missing
+   */
+  public String getStringAttribute(String namespace, String name) throws ParseException {
+    String value = mParser.getAttributeValue(namespace, name);
+    if (null == value) {
+      throw new ParseException(
+          "missing '" + name + "' attribute on '" + mCurrentStartTag + "' element");
+    }
+    return value;
+  }
+
+  /**
+   * Returns the string value of the named attribute. An exception will
+   * be thrown if the attribute is not a valid integer.
+   *
+   * @param namespace the namespace of the attribute
+   * @param name the name of the attribute
+   * @param defaultValue the value to return if the attribute is not specified
+   * @return the value of the attribute
+   * @throws ParseException thrown if the attribute not a valid integer.
+   */
+  public int getIntAttribute(String namespace, String name, int defaultValue)
+      throws ParseException {
+    String value = mParser.getAttributeValue(namespace, name);
+    if (null == value) return defaultValue;
+    try {
+      return Integer.parseInt(value);
+    } catch (NumberFormatException e) {
+      throw new ParseException("Cannot parse '" + value + "' as an integer");
+    }
+  }
+
+  /**
+   * Returns the string value of the named attribute. An exception will
+   * be thrown if the attribute is not present or is not a valid integer.
+   *
+   * @param namespace the namespace of the attribute
+   * @param name the name of the attribute @return the value of the attribute
+   * @throws ParseException thrown if the attribute is missing or not a valid integer.
+   */
+  public int getIntAttribute(String namespace, String name)
+      throws ParseException {
+    String value = getStringAttribute(namespace, name);
+    try {
+      return Integer.parseInt(value);
+    } catch (NumberFormatException e) {
+      throw new ParseException("Cannot parse '" + value + "' as an integer");
+    }
+  }
+
+  /**
+   * Returns the string value of the named attribute. An exception will
+   * be thrown if the attribute is not a valid long.
+   *
+   * @param namespace the namespace of the attribute
+   * @param name the name of the attribute @return the value of the attribute
+   * @throws ParseException thrown if the attribute is not a valid long.
+   */
+  public long getLongAttribute(String namespace, String name, long defaultValue)
+      throws ParseException {
+    String value = mParser.getAttributeValue(namespace, name);
+    if (null == value) return defaultValue;
+    try {
+      return Long.parseLong(value);
+    } catch (NumberFormatException e) {
+      throw new ParseException("Cannot parse '" + value + "' as a long");
+    }
+  }
+
+  /**
+   * Returns the string value of the named attribute. An exception will
+   * be thrown if the attribute is not present or is not a valid long.
+   *
+   * @param namespace the namespace of the attribute
+   * @param name the name of the attribute @return the value of the attribute
+   * @throws ParseException thrown if the attribute is missing or not a valid long.
+   */
+  public long getLongAttribute(String namespace, String name)
+      throws ParseException {
+    String value = getStringAttribute(namespace, name);
+    try {
+      return Long.parseLong(value);
+    } catch (NumberFormatException e) {
+      throw new ParseException("Cannot parse '" + value + "' as a long");
+    }
+  }
+
+  public static final class ParseException extends Exception {
+    public ParseException(String message) {
+      super(message);
+    }
+
+    public ParseException(String message, Throwable cause) {
+      super(message, cause);
+    }
+
+    public ParseException(Throwable cause) {
+      super(cause);
+    }
+  }
+}
diff --git a/src/com/google/wireless/gdata2/parser/xml/XmlGDataParser.java b/src/com/google/wireless/gdata2/parser/xml/XmlGDataParser.java
new file mode 100644
index 0000000..9cd3e23
--- /dev/null
+++ b/src/com/google/wireless/gdata2/parser/xml/XmlGDataParser.java
@@ -0,0 +1,530 @@
+// Copyright 2008 The Android Open Source Project
+
+package com.google.wireless.gdata2.parser.xml;
+
+import com.google.wireless.gdata2.data.Entry;
+import com.google.wireless.gdata2.data.Feed;
+import com.google.wireless.gdata2.data.StringUtils;
+import com.google.wireless.gdata2.data.XmlUtils;
+import com.google.wireless.gdata2.parser.GDataParser;
+import com.google.wireless.gdata2.parser.ParseException;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * {@link GDataParser} that uses an {@link XmlPullParser} to parse a GData feed.
+ */
+// NOTE: we do not perform any validity checks on the XML.
+public class XmlGDataParser implements GDataParser {
+
+  /** Namespace URI for Atom */
+  public static final String NAMESPACE_ATOM_URI =
+      "http://www.w3.org/2005/Atom";
+
+  public static final String NAMESPACE_OPENSEARCH = "openSearch";
+
+  public static final String NAMESPACE_OPENSEARCH_URI =
+      "http://a9.com/-/spec/opensearchrss/1.0/";
+
+  /** Namespace prefix for GData */
+  public static final String NAMESPACE_GD = "gd";
+
+  /** Namespace URI for GData */
+  public static final String NAMESPACE_GD_URI =
+      "http://schemas.google.com/g/2005";
+
+  private final InputStream is;
+  private final XmlPullParser parser;
+  private boolean isInBadState;
+
+  /**
+   * Creates a new XmlGDataParser for a feed in the provided InputStream.
+   * @param is The InputStream that should be parsed.
+   * @throws ParseException Thrown if an XmlPullParser could not be created
+   * or set around this InputStream.
+   */
+  public XmlGDataParser(InputStream is, XmlPullParser parser)
+      throws ParseException {
+    this.is = is;
+    this.parser = parser;
+    this.isInBadState = false;
+    if (this.is != null) {
+      try {
+        this.parser.setInput(is, null /* encoding */);
+      } catch (XmlPullParserException e) {
+        throw new ParseException("Could not create XmlGDataParser", e);
+      }
+    }
+  }
+
+  /*
+  * (non-Javadoc)
+  * @see com.google.wireless.gdata2.parser.GDataParser#init()
+  */
+  public final Feed init() throws ParseException {
+    int eventType;
+    try {
+      eventType = parser.getEventType();
+    } catch (XmlPullParserException e) {
+      throw new ParseException("Could not parse GData feed.", e);
+    }
+    if (eventType != XmlPullParser.START_DOCUMENT) {
+      throw new ParseException("Attempting to initialize parsing beyond "
+          + "the start of the document.");
+    }
+
+    try {
+      eventType = parser.next();
+    } catch (XmlPullParserException xppe) {
+      throw new ParseException("Could not read next event.", xppe);
+    } catch (IOException ioe) {
+      throw new ParseException("Could not read next event.", ioe);
+    }
+    while (eventType != XmlPullParser.END_DOCUMENT) {
+      switch (eventType) {
+        case XmlPullParser.START_TAG:
+          String name = parser.getName();
+          if ("feed".equals(name)) {
+            try {
+              return parseFeed();
+            } catch (XmlPullParserException xppe) {
+              throw new ParseException("Unable to parse <feed>.",
+                  xppe);
+            } catch (IOException ioe) {
+              throw new ParseException("Unable to parse <feed>.",
+                  ioe);
+            }
+          }
+          break;
+        default:
+          // ignore
+          break;
+      }
+
+      try {
+        eventType = parser.next();
+      } catch (XmlPullParserException xppe) {
+        throw new ParseException("Could not read next event.", xppe);
+      } catch (IOException ioe) {
+        throw new ParseException("Could not read next event." , ioe);
+      }
+    }
+    throw new ParseException("No <feed> found in document.");
+  }
+
+  /**
+   * Returns the {@link XmlPullParser} being used to parse this feed.
+   */
+  protected final XmlPullParser getParser() {
+    return parser;
+  }
+
+  /**
+   * Creates a new {@link Feed} that should be filled with information about
+   * the feed that will be parsed.
+   * @return The {@link Feed} that should be filled.
+   */
+  protected Feed createFeed() {
+    return new Feed();
+  }
+
+  /**
+   * Creates a new {@link Entry} that should be filled with information about
+   * the entry that will be parsed.
+   * @return The {@link Entry} that should be filled.
+   */
+  protected Entry createEntry() {
+    return new Entry();
+  }
+
+  /**
+   * Parses the feed (but not any entries).
+   *
+   * @return A new {@link Feed} containing information about the feed.
+   * @throws XmlPullParserException Thrown if the XML document cannot be
+   * parsed.
+   * @throws IOException Thrown if the {@link InputStream} behind the feed
+   * cannot be read.
+   */
+  private final Feed parseFeed() throws XmlPullParserException, IOException {
+    Feed feed = createFeed();
+    // parsing <feed>
+    // not interested in any attributes -- move onto the children.
+    int eventType = parser.next();
+    while (eventType != XmlPullParser.END_DOCUMENT) {
+      switch (eventType) {
+        case XmlPullParser.START_TAG:
+          String name = parser.getName();
+          if ("totalResults".equals(name)) {
+            feed.setTotalResults(StringUtils.parseInt(
+                XmlUtils.extractChildText(parser), 0));
+          } else if ("startIndex".equals(name)) {
+            feed.setStartIndex(StringUtils.parseInt(
+                XmlUtils.extractChildText(parser), 0));
+          } else if ("itemsPerPage".equals(name)) {
+            feed.setItemsPerPage(StringUtils.parseInt(
+                XmlUtils.extractChildText(parser), 0));
+          } else if ("title".equals(name)) {
+            feed.setTitle(XmlUtils.extractChildText(parser));
+          } else if ("id".equals(name)) {
+            feed.setId(XmlUtils.extractChildText(parser));
+          } else if ("updated".equals(name)) {
+            feed.setLastUpdated(XmlUtils.extractChildText(parser));
+          } else if ("category".equals(name)) {
+            String category =
+                parser.getAttributeValue(null /* ns */, "term");
+            if (!StringUtils.isEmpty(category)) {
+              feed.setCategory(category);
+            }
+            String categoryScheme =
+                parser.getAttributeValue(null /* ns */, "scheme");
+            if (!StringUtils.isEmpty(categoryScheme)) {
+              feed.setCategoryScheme(categoryScheme);
+            }
+          } else if ("entry".equals(name)) {
+            // stop parsing here.
+            // TODO: pay attention to depth?
+            return feed;
+          } else {
+            handleExtraElementInFeed(feed);
+          }
+          break;
+        default:
+          break;
+      }
+      eventType = parser.next();
+    }
+    // if we get here, we have a feed with no entries.
+    return feed;
+  }
+
+  /**
+   * Hook that allows extra (service-specific) elements in a &lt;feed&gt; to
+   * be parsed.
+   * @param feed The {@link Feed} being filled.
+   */
+  protected void handleExtraElementInFeed(Feed feed)
+      throws XmlPullParserException, IOException {
+    // no-op in this class.
+  }
+
+  /*
+  * (non-Javadoc)
+  * @see com.google.wireless.gdata2.parser.GDataParser#hasMoreData()
+  */
+  public boolean hasMoreData() {
+    if (isInBadState) {
+      return false;
+    }
+    try {
+      int eventType = parser.getEventType();
+      return (eventType != XmlPullParser.END_DOCUMENT);
+    } catch (XmlPullParserException xppe) {
+      return false;
+    }
+  }
+
+  /*
+  * (non-Javadoc)
+  * @see com.google.wireless.gdata2.parser.GDataParser#readNextEntry
+  */
+  public Entry readNextEntry(Entry entry) throws ParseException, IOException {
+    if (!hasMoreData()) {
+      throw new IllegalStateException("you shouldn't call this if hasMoreData() is false");
+    }
+
+    int eventType;
+    try {
+      eventType = parser.getEventType();
+    } catch (XmlPullParserException e) {
+      throw new ParseException("Could not parse entry.", e);
+    }
+
+    if (eventType != XmlPullParser.START_TAG) {
+      throw new ParseException("Expected event START_TAG: Actual event: "
+          + XmlPullParser.TYPES[eventType]);
+    }
+
+    String name = parser.getName();
+    if (!"entry".equals(name)) {
+      throw new ParseException("Expected <entry>: Actual element: "
+          + "<" + name + ">");
+    }
+
+    if (entry == null) {
+      entry = createEntry();
+    } else {
+      entry.clear();
+    }
+
+    try {
+      parser.next();
+      handleEntry(entry);
+      entry.validate();
+    } catch (ParseException xppe1) {
+      try {
+        if (hasMoreData()) skipToNextEntry();
+      } catch (XmlPullParserException xppe2) {
+        // squelch the error -- let the original one stand.
+        // set isInBadState to ensure that the next call to hasMoreData() will return false.
+        isInBadState = true;
+      }
+      throw new ParseException("Could not parse <entry>, " + entry, xppe1);
+    } catch (XmlPullParserException xppe1) {
+      try {
+        if (hasMoreData()) skipToNextEntry();
+      } catch (XmlPullParserException xppe2) {
+        // squelch the error -- let the original one stand.
+        // set isInBadState to ensure that the next call to hasMoreData() will return false.
+        isInBadState = true;
+      }
+      throw new ParseException("Could not parse <entry>, " + entry, xppe1);
+    }
+    return entry;
+  }
+
+  /**
+   * Parses a GData entry.  You can either call {@link #init()} or
+   * {@link #parseStandaloneEntry()} for a given feed.
+   *
+   * @return The parsed entry.
+   * @throws ParseException Thrown if the entry could not be parsed.
+   */
+  public Entry parseStandaloneEntry() throws ParseException, IOException {
+    Entry entry = createEntry();
+
+    int eventType;
+    try {
+      eventType = parser.getEventType();
+    } catch (XmlPullParserException e) {
+      throw new ParseException("Could not parse GData entry.", e);
+    }
+    if (eventType != XmlPullParser.START_DOCUMENT) {
+      throw new ParseException("Attempting to initialize parsing beyond "
+          + "the start of the document.");
+    }
+
+    try {
+      eventType = parser.next();
+    } catch (XmlPullParserException xppe) {
+      throw new ParseException("Could not read next event.", xppe);
+    } catch (IOException ioe) {
+      throw new ParseException("Could not read next event.", ioe);
+    }
+    while (eventType != XmlPullParser.END_DOCUMENT) {
+      switch (eventType) {
+        case XmlPullParser.START_TAG:
+          String name = parser.getName();
+          if ("entry".equals(name)) {
+            try {
+              parser.next();
+              handleEntry(entry);
+              return entry;
+            } catch (XmlPullParserException xppe) {
+              throw new ParseException("Unable to parse <entry>.",
+                  xppe);
+            } catch (IOException ioe) {
+              throw new ParseException("Unable to parse <entry>.",
+                  ioe);
+            }
+          }
+          break;
+        default:
+          // ignore
+          break;
+      }
+
+      try {
+        eventType = parser.next();
+      } catch (XmlPullParserException xppe) {
+        throw new ParseException("Could not read next event.", xppe);
+      }
+    }
+    throw new ParseException("No <entry> found in document.");
+  }
+
+  /**
+   * Skips the rest of the current entry until the parser reaches the next entry, if any.
+   * Does nothing if the parser is already at the beginning of an entry.
+   */
+  protected void skipToNextEntry() throws IOException, XmlPullParserException {
+    if (!hasMoreData()) {
+      throw new IllegalStateException("you shouldn't call this if hasMoreData() is false");
+    }
+
+    int eventType = parser.getEventType();
+
+    // skip ahead until we reach an <entry> tag.
+    while (eventType != XmlPullParser.END_DOCUMENT) {
+      switch (eventType) {
+        case XmlPullParser.START_TAG:
+          if ("entry".equals(parser.getName())) {
+            return;
+          }
+          break;
+      }
+      eventType = parser.next();
+    }
+  }
+
+  /**
+   * Parses the current entry in the XML document.  Assumes that the parser
+   * is currently pointing just after an &lt;entry&gt;.
+   *
+   * @param entry The entry that will be filled.
+   * @throws XmlPullParserException Thrown if the XML cannot be parsed.
+   * @throws IOException Thrown if the underlying inputstream cannot be read.
+   */
+  protected void handleEntry(Entry entry)
+      throws XmlPullParserException, IOException, ParseException {
+    int eventType = parser.getEventType();
+    while (eventType != XmlPullParser.END_DOCUMENT) {
+      switch (eventType) {
+        case XmlPullParser.START_TAG:
+          // TODO: make sure these elements are at the expected depth.
+          String name = parser.getName();
+          if ("entry".equals(name)) {
+            // stop parsing here.
+            return;
+          } else if ("id".equals(name)) {
+            entry.setId(XmlUtils.extractChildText(parser));
+          } else if ("title".equals(name)) {
+            entry.setTitle(XmlUtils.extractChildText(parser));
+          } else if ("link".equals(name)) {
+            String rel =
+                parser.getAttributeValue(null /* ns */, "rel");
+            String type =
+                parser.getAttributeValue(null /* ns */, "type");
+            String href =
+                parser.getAttributeValue(null /* ns */, "href");
+            if ("edit".equals(rel)) {
+              entry.setEditUri(href);
+            } else if (("alternate").equals(rel) && ("text/html".equals(type))) {
+                entry.setHtmlUri(href);
+            } else {
+              handleExtraLinkInEntry(rel,
+                  type,
+                  href,
+                  entry);
+            }
+          } else if ("summary".equals(name)) {
+            entry.setSummary(XmlUtils.extractChildText(parser));
+          } else if ("content".equals(name)) {
+            // TODO: parse the type
+            entry.setContent(XmlUtils.extractChildText(parser));
+          } else if ("author".equals(name)) {
+            handleAuthor(entry);
+          } else if ("category".equals(name)) {
+            String category =
+                parser.getAttributeValue(null /* ns */, "term");
+            if (category != null && category.length() > 0) {
+              entry.setCategory(category);
+            }
+            String categoryScheme =
+                parser.getAttributeValue(null /* ns */, "scheme");
+            if (categoryScheme != null && category.length() > 0) {
+              entry.setCategoryScheme(categoryScheme);
+            }
+          } else if ("published".equals(name)) {
+            entry.setPublicationDate(
+                XmlUtils.extractChildText(parser));
+          } else if ("updated".equals(name)) {
+            entry.setUpdateDate(XmlUtils.extractChildText(parser));
+          } else if ("deleted".equals(name)) {
+            entry.setDeleted(true);
+          } else {
+            handleExtraElementInEntry(entry);
+          }
+          break;
+        default:
+          break;
+      }
+
+      eventType = parser.next();
+    }
+  }
+
+  private void handleAuthor(Entry entry)
+      throws XmlPullParserException, IOException {
+
+    int eventType = parser.getEventType();
+    String name = parser.getName();
+
+    if (eventType != XmlPullParser.START_TAG ||
+        (!"author".equals(parser.getName()))) {
+      // should not happen.
+      throw new
+          IllegalStateException("Expected <author>: Actual element: <"
+          + parser.getName() + ">");
+    }
+
+    eventType = parser.next();
+    while (eventType != XmlPullParser.END_DOCUMENT) {
+      switch (eventType) {
+        case XmlPullParser.START_TAG:
+          name = parser.getName();
+          if ("name".equals(name)) {
+            String authorName = XmlUtils.extractChildText(parser);
+            entry.setAuthor(authorName);
+          } else if ("email".equals(name)) {
+            String email = XmlUtils.extractChildText(parser);
+            entry.setEmail(email);
+          }
+          break;
+        case XmlPullParser.END_TAG:
+          name = parser.getName();
+          if ("author".equals(name)) {
+            return;
+          }
+        default:
+          // ignore
+      }
+
+      eventType = parser.next();
+    }
+  }
+
+  /*
+  * (non-Javadoc)
+  * @see com.google.wireless.gdata2.parser.GDataParser#close()
+  */
+  public void close() {
+    if (is != null) {
+      try {
+        is.close();
+      } catch (IOException ioe) {
+        // ignore
+      }
+    }
+  }
+
+  /**
+   * Hook that allows extra (service-specific) elements in an &lt;entry&gt;
+   * to be parsed.
+   * @param entry The {@link Entry} being filled.
+   */
+  protected void handleExtraElementInEntry(Entry entry)
+      throws XmlPullParserException, IOException, ParseException {
+    // no-op in this class.
+  }
+
+  /**
+   * Hook that allows extra (service-specific) &lt;link&gt;s in an entry to be
+   * parsed.
+   * @param rel The rel attribute value.
+   * @param type The type attribute value.
+   * @param href The href attribute value.
+   * @param entry The {@link Entry} being filled.
+   */
+  protected void handleExtraLinkInEntry(String rel,
+      String type,
+      String href,
+      Entry entry)
+      throws XmlPullParserException, IOException {
+    // no-op in this class.
+  }
+}
diff --git a/src/com/google/wireless/gdata2/parser/xml/XmlMediaEntryGDataParser.java b/src/com/google/wireless/gdata2/parser/xml/XmlMediaEntryGDataParser.java
new file mode 100644
index 0000000..c72d55f
--- /dev/null
+++ b/src/com/google/wireless/gdata2/parser/xml/XmlMediaEntryGDataParser.java
@@ -0,0 +1,42 @@
+package com.google.wireless.gdata2.parser.xml;
+
+import com.google.wireless.gdata2.data.MediaEntry;
+import com.google.wireless.gdata2.data.Entry;
+import com.google.wireless.gdata2.data.Feed;
+import com.google.wireless.gdata2.parser.ParseException;
+
+import org.xmlpull.v1.XmlPullParser;
+
+import java.io.InputStream;
+
+/**
+ * GDataParser for a MediaEntry. This must only be used to parse an entry, not a feed, since
+ * there is no such thing as a feed of media entries.
+ */
+public class XmlMediaEntryGDataParser extends XmlGDataParser {
+  /**
+   * Creates a new XmlMediaEntryGDataParser.
+   * @param is The InputStream that should be parsed.
+   * @param parser the XmlPullParser to use for the xml parsing
+   * @throws com.google.wireless.gdata2.parser.ParseException Thrown if a parser cannot be created.
+   */
+  public XmlMediaEntryGDataParser(InputStream is, XmlPullParser parser) throws ParseException {
+    super(is, parser);
+  }
+
+  /*
+  * (non-Javadoc)
+  * @see com.google.wireless.gdata2.parser.xml.XmlGDataParser#createFeed()
+  */
+  protected Feed createFeed() {
+    throw new UnsupportedOperationException("there is no such thing as a feed of media entries");
+  }
+
+  /*
+  * (non-Javadoc)
+  * @see com.google.wireless.gdata2.parser.xml.XmlGDataParser#createEntry()
+  */
+  protected Entry createEntry() {
+    return new MediaEntry();
+  }
+}
diff --git a/src/com/google/wireless/gdata2/parser/xml/XmlParserFactory.java b/src/com/google/wireless/gdata2/parser/xml/XmlParserFactory.java
new file mode 100644
index 0000000..650db50
--- /dev/null
+++ b/src/com/google/wireless/gdata2/parser/xml/XmlParserFactory.java
@@ -0,0 +1,31 @@
+// Copyright 2007 The Android Open Source Project
+
+package com.google.wireless.gdata2.parser.xml;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlSerializer;
+
+/**
+ * Factory for creating new {@link org.xmlpull.v1.XmlPullParser}s and
+ * {@link org.xmlpull.v1.XmlSerializer}s
+ */
+public interface XmlParserFactory {
+
+    /**
+     * Creates a new {@link XmlPullParser}.
+     *
+     * @return A new {@link XmlPullParser}.
+     * @throws XmlPullParserException Thrown if the parser could not be created.
+     */
+    XmlPullParser createParser() throws XmlPullParserException;
+
+    /**
+     * Creates a new {@link XmlSerializer}.
+     *
+     * @return A new {@link XmlSerializer}.
+     * @throws XmlPullParserException Thrown if the serializer could not be
+     * created.
+     */
+    XmlSerializer createSerializer() throws XmlPullParserException;
+}
diff --git a/src/com/google/wireless/gdata2/parser/xml/package.html b/src/com/google/wireless/gdata2/parser/xml/package.html
new file mode 100644
index 0000000..1c9bf9d
--- /dev/null
+++ b/src/com/google/wireless/gdata2/parser/xml/package.html
@@ -0,0 +1,5 @@
+<html>
+<body>
+    {@hide}
+</body>
+</html>
diff --git a/src/com/google/wireless/gdata2/serializer/GDataSerializer.java b/src/com/google/wireless/gdata2/serializer/GDataSerializer.java
new file mode 100644
index 0000000..3d75485
--- /dev/null
+++ b/src/com/google/wireless/gdata2/serializer/GDataSerializer.java
@@ -0,0 +1,56 @@
+// Copyright 2007 The Android Open Source Project
+
+package com.google.wireless.gdata2.serializer;
+
+import com.google.wireless.gdata2.parser.ParseException;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+/**
+ * Interface for serializing GData entries.
+ */
+public interface GDataSerializer {
+
+    // TODO: I hope the three formats does not bite us.  Each serializer has
+    // to pay attention to what "mode" it is in when serializing.
+    
+    /**
+     * Serialize all data in the entry.  Used for debugging.
+     */
+    public static final int FORMAT_FULL = 0;
+    
+    /**
+     * Serialize only the data necessary for creating a new entry.
+     */
+    public static final int FORMAT_CREATE = 1;
+    
+    /**
+     * Serialize only the data necessary for updating an existing entry.
+     */
+    public static final int FORMAT_UPDATE = 2;
+
+    /**
+     * Returns the Content-Type for this serialization format.
+     * @return The Content-Type for this serialization format.
+     */
+    String getContentType();
+    
+    /**
+     * Serializes a GData entry to the provided {@link OutputStream}, using the
+     * specified serialization format.
+     * 
+     * @see #FORMAT_FULL
+     * @see #FORMAT_CREATE
+     * @see #FORMAT_UPDATE
+     * 
+     * @param out The {@link OutputStream} to which the entry should be 
+     * serialized.
+     * @param format The format of the serialized output.
+     * @throws IOException Thrown if there is an issue writing the serialized 
+     * entry to the provided {@link OutputStream}.
+     * @throws ParseException Thrown if the entry cannot be serialized.
+     */
+    void serialize(OutputStream out, int format)
+        throws IOException, ParseException;
+}
diff --git a/src/com/google/wireless/gdata2/serializer/package.html b/src/com/google/wireless/gdata2/serializer/package.html
new file mode 100644
index 0000000..1c9bf9d
--- /dev/null
+++ b/src/com/google/wireless/gdata2/serializer/package.html
@@ -0,0 +1,5 @@
+<html>
+<body>
+    {@hide}
+</body>
+</html>
diff --git a/src/com/google/wireless/gdata2/serializer/xml/XmlEntryGDataSerializer.java b/src/com/google/wireless/gdata2/serializer/xml/XmlEntryGDataSerializer.java
new file mode 100644
index 0000000..210f76c
--- /dev/null
+++ b/src/com/google/wireless/gdata2/serializer/xml/XmlEntryGDataSerializer.java
@@ -0,0 +1,268 @@
+// Copyright 2007 The Android Open Source Project
+
+package com.google.wireless.gdata2.serializer.xml;
+
+import com.google.wireless.gdata2.data.Entry;
+import com.google.wireless.gdata2.data.ExtendedProperty;
+import com.google.wireless.gdata2.data.StringUtils;
+import com.google.wireless.gdata2.parser.ParseException;
+import com.google.wireless.gdata2.parser.xml.XmlGDataParser;
+import com.google.wireless.gdata2.parser.xml.XmlParserFactory;
+import com.google.wireless.gdata2.serializer.GDataSerializer;
+
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+/**
+ * Serializes GData entries to the Atom XML format.
+ */
+public class XmlEntryGDataSerializer implements GDataSerializer {
+
+  /** The XmlParserFactory that is used to create the XmlSerializer */
+  private final XmlParserFactory factory;
+
+  /** The entry being serialized. */
+  private final Entry entry;
+
+  /**
+   * Creates a new XmlEntryGDataSerializer that will serialize the provided
+   * entry.
+   *
+   * @param entry The entry that should be serialized.
+   */
+  public XmlEntryGDataSerializer(XmlParserFactory factory,
+      Entry entry) {
+    this.factory = factory;
+    this.entry = entry;
+  }
+
+  /**
+   * Returns the entry being serialized.
+   * @return The entry being serialized.
+   */
+  protected Entry getEntry() {
+    return entry;
+  }
+
+  /* (non-Javadoc)
+  * @see GDataSerializer#getContentType()
+  */
+  public String getContentType() {
+    return "application/atom+xml";
+  }
+
+  /* (non-Javadoc)
+  * @see GDataSerializer#serialize(java.io.OutputStream)
+  */
+  public void serialize(OutputStream out, int format)
+      throws IOException, ParseException {
+    XmlSerializer serializer = null;
+    try {
+      serializer = factory.createSerializer();
+    } catch (XmlPullParserException e) {
+      throw new ParseException("Unable to create XmlSerializer.", e);
+    }
+    // TODO: make the output compact
+
+    serializer.setOutput(out, "UTF-8");
+    serializer.startDocument("UTF-8", new Boolean(false));
+
+    declareEntryNamespaces(serializer);
+    serializer.startTag(XmlGDataParser.NAMESPACE_ATOM_URI, "entry");
+
+    serializeEntryContents(serializer, format);
+
+    serializer.endTag(XmlGDataParser.NAMESPACE_ATOM_URI, "entry");
+    serializer.endDocument();
+    serializer.flush();
+  }
+
+  private final void declareEntryNamespaces(XmlSerializer serializer)
+      throws IOException {
+    serializer.setPrefix("" /* default ns */,
+        XmlGDataParser.NAMESPACE_ATOM_URI);
+    serializer.setPrefix(XmlGDataParser.NAMESPACE_GD,
+        XmlGDataParser.NAMESPACE_GD_URI);
+    declareExtraEntryNamespaces(serializer);
+  }
+
+  protected void declareExtraEntryNamespaces(XmlSerializer serializer)
+      throws IOException {
+    // no-op in this class
+  }
+
+  /**
+   * @param serializer
+   * @throws IOException
+   */
+  private final void serializeEntryContents(XmlSerializer serializer,
+      int format)
+      throws ParseException, IOException {
+
+    if (format != FORMAT_CREATE) {
+      serializeId(serializer, entry.getId());
+    }
+
+    serializeTitle(serializer, entry.getTitle());
+
+    if (format != FORMAT_CREATE) {
+      serializeLink(serializer, "edit" /* rel */, entry.getEditUri(), null /* type */);
+      serializeLink(serializer, "alternate" /* rel */, entry.getHtmlUri(), "text/html" /* type */);
+    }
+
+    serializeSummary(serializer, entry.getSummary());
+
+    serializeContent(serializer, entry.getContent());
+
+    serializeAuthor(serializer, entry.getAuthor(), entry.getEmail());
+
+    serializeCategory(serializer,
+        entry.getCategory(), entry.getCategoryScheme());
+
+    if (format == FORMAT_FULL) {
+      serializePublicationDate(serializer,
+          entry.getPublicationDate());
+    }
+
+    if (format != FORMAT_CREATE) {
+      serializeUpdateDate(serializer,
+          entry.getUpdateDate());
+    }
+
+    serializeExtraEntryContents(serializer, format);
+  }
+
+  /**
+   * Hook for subclasses to serialize extra fields within the entry.
+   * @param serializer The XmlSerializer being used to serialize the entry.
+   * @param format The serialization format for the entry.
+   * @throws ParseException Thrown if the entry cannot be serialized.
+   * @throws IOException Thrown if the entry cannot be written to the
+   * underlying {@link OutputStream}.
+   */
+  protected void serializeExtraEntryContents(XmlSerializer serializer,
+      int format)
+      throws ParseException, IOException {
+    // no-op in this class.
+  }
+
+  // TODO: make these helper methods protected so sublcasses can use them?
+
+  private static void serializeId(XmlSerializer serializer,
+      String id) throws IOException {
+    if (StringUtils.isEmpty(id)) {
+      return;
+    }
+    serializer.startTag(null /* ns */, "id");
+    serializer.text(id);
+    serializer.endTag(null /* ns */, "id");
+  }
+
+  private static void serializeTitle(XmlSerializer serializer,
+      String title)
+      throws IOException {
+    if (StringUtils.isEmpty(title)) {
+      return;
+    }
+    serializer.startTag(null /* ns */, "title");
+    serializer.text(title);
+    serializer.endTag(null /* ns */, "title");
+  }
+
+  public static void serializeLink(XmlSerializer serializer, String rel, String href, String type)
+      throws IOException {
+    if (StringUtils.isEmpty(href)) {
+      return;
+    }
+    serializer.startTag(null /* ns */, "link");
+    serializer.attribute(null /* ns */, "rel", rel);
+    serializer.attribute(null /* ns */, "href", href);
+    if (!StringUtils.isEmpty(type)) serializer.attribute(null /* ns */, "type", type);
+    serializer.endTag(null /* ns */, "link");
+  }
+
+  private static void serializeSummary(XmlSerializer serializer,
+      String summary)
+      throws IOException {
+    if (StringUtils.isEmpty(summary)) {
+      return;
+    }
+    serializer.startTag(null /* ns */, "summary");
+    serializer.text(summary);
+    serializer.endTag(null /* ns */, "summary");
+  }
+
+  private static void serializeContent(XmlSerializer serializer,
+      String content)
+      throws IOException {
+    if (content == null) {
+      return;
+    }
+    serializer.startTag(null /* ns */, "content");
+    serializer.attribute(null /* ns */, "type", "text");
+    serializer.text(content);
+    serializer.endTag(null /* ns */, "content");
+  }
+
+  private static void serializeAuthor(XmlSerializer serializer,
+      String author,
+      String email)
+      throws IOException {
+    if (StringUtils.isEmpty(author) || StringUtils.isEmpty(email)) {
+      return;
+    }
+    serializer.startTag(null /* ns */, "author");
+    serializer.startTag(null /* ns */, "name");
+    serializer.text(author);
+    serializer.endTag(null /* ns */, "name");
+    serializer.startTag(null /* ns */, "email");
+    serializer.text(email);
+    serializer.endTag(null /* ns */, "email");
+    serializer.endTag(null /* ns */, "author");
+  }
+
+  private static void serializeCategory(XmlSerializer serializer,
+      String category,
+      String categoryScheme)
+      throws IOException {
+    if (StringUtils.isEmpty(category) &&
+        StringUtils.isEmpty(categoryScheme)) {
+      return;
+    }
+    serializer.startTag(null /* ns */, "category");
+    if (!StringUtils.isEmpty(category)) {
+      serializer.attribute(null /* ns */, "term", category);
+    }
+    if (!StringUtils.isEmpty(categoryScheme)) {
+      serializer.attribute(null /* ns */, "scheme", categoryScheme);
+    }
+    serializer.endTag(null /* ns */, "category");
+  }
+
+  private static void
+  serializePublicationDate(XmlSerializer serializer,
+      String publicationDate)
+      throws IOException {
+    if (StringUtils.isEmpty(publicationDate)) {
+      return;
+    }
+    serializer.startTag(null /* ns */, "published");
+    serializer.text(publicationDate);
+    serializer.endTag(null /* ns */, "published");
+  }
+
+  private static void
+  serializeUpdateDate(XmlSerializer serializer,
+      String updateDate)
+      throws IOException {
+    if (StringUtils.isEmpty(updateDate)) {
+      return;
+    }
+    serializer.startTag(null /* ns */, "updated");
+    serializer.text(updateDate);
+    serializer.endTag(null /* ns */, "updated");
+  }
+}
diff --git a/src/com/google/wireless/gdata2/serializer/xml/package.html b/src/com/google/wireless/gdata2/serializer/xml/package.html
new file mode 100644
index 0000000..1c9bf9d
--- /dev/null
+++ b/src/com/google/wireless/gdata2/serializer/xml/package.html
@@ -0,0 +1,5 @@
+<html>
+<body>
+    {@hide}
+</body>
+</html>
diff --git a/src/com/google/wireless/gdata2/spreadsheets/client/SpreadsheetsClient.java b/src/com/google/wireless/gdata2/spreadsheets/client/SpreadsheetsClient.java
new file mode 100755
index 0000000..585c0a3
--- /dev/null
+++ b/src/com/google/wireless/gdata2/spreadsheets/client/SpreadsheetsClient.java
@@ -0,0 +1,236 @@
+// Copyright 2007 The Android Open Source Project
+package com.google.wireless.gdata2.spreadsheets.client;
+
+import com.google.wireless.gdata2.client.GDataClient;
+import com.google.wireless.gdata2.client.GDataParserFactory;
+import com.google.wireless.gdata2.client.GDataServiceClient;
+import com.google.wireless.gdata2.client.HttpException;
+import com.google.wireless.gdata2.data.Entry;
+import com.google.wireless.gdata2.data.StringUtils;
+import com.google.wireless.gdata2.parser.GDataParser;
+import com.google.wireless.gdata2.parser.ParseException;
+import com.google.wireless.gdata2.serializer.GDataSerializer;
+import com.google.wireless.gdata2.spreadsheets.data.CellEntry;
+import com.google.wireless.gdata2.spreadsheets.data.ListEntry;
+import com.google.wireless.gdata2.spreadsheets.data.SpreadsheetEntry;
+import com.google.wireless.gdata2.spreadsheets.data.WorksheetEntry;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * GDataServiceClient for accessing Google Spreadsheets. This client can
+ * access and parse all of the Spreadsheets feed types: Spreadsheets feed,
+ * Worksheets feed, List feed, and Cells feed. Read operations are supported
+ * on all feed types, but only the List and Cells feeds support write
+ * operations. (This is a limitation of the protocol, not this API. Such write
+ * access may be added to the protocol in the future, requiring changes to
+ * this implementation.)
+ * 
+ * Only 'private' visibility and 'full' projections are currently supported.
+ */
+public class SpreadsheetsClient extends GDataServiceClient {
+    /** The name of the service, dictated to be 'wise' by the protocol. */
+    private static final String SERVICE = "wise";
+
+    /** Standard base feed url for spreadsheets. */
+    public static final String SPREADSHEETS_BASE_FEED_URL =
+            "http://spreadsheets.google.com/feeds/spreadsheets/private/full";
+
+    /** Base feed url for spreadsheets. */
+    private final String baseFeedUrl;
+
+    /**
+     * Create a new SpreadsheetsClient.
+     *
+     * @param client The GDataClient that should be used to authenticate
+     *        requests, retrieve feeds, etc.
+     * @param spreadsheetFactory The GDataParserFactory that should be used to obtain GDataParsers
+     * used by this client.
+     * @param baseFeedUrl The base URL for spreadsheets feeds.
+     */
+    public SpreadsheetsClient(GDataClient client,
+            GDataParserFactory spreadsheetFactory,
+            String baseFeedUrl) {
+        super(client, spreadsheetFactory);
+        this.baseFeedUrl = baseFeedUrl;
+    }
+
+    /**
+     * Create a new SpreadsheetsClient.  Uses the standard base URL for spreadsheets feeds.
+     * 
+     * @param client The GDataClient that should be used to authenticate
+     *        requests, retrieve feeds, etc.
+     */
+    public SpreadsheetsClient(GDataClient client,
+            GDataParserFactory spreadsheetFactory) {
+        this(client, spreadsheetFactory, SPREADSHEETS_BASE_FEED_URL);
+    }
+
+    /* (non-Javadoc)
+     * @see GDataServiceClient#getServiceName
+     */
+    public String getServiceName() {
+        return SERVICE;
+    }
+
+    /**
+     * Returns a parser for the specified feed type.
+     * 
+     * @param feedEntryClass the Class of entry type that will be parsed. This lets this
+     *   method figure out which parser to create.
+     * @param feedUri the URI of the feed to be fetched and parsed
+     * @param authToken the current authToken to use for the request @return a parser for the indicated feed
+     * @throws HttpException if an http error is encountered
+     * @throws ParseException if the response from the server could not be
+     *         parsed
+     */
+    private GDataParser getParserForTypedFeed(Class feedEntryClass, String feedUri,
+            String authToken) throws ParseException, IOException, HttpException {
+        GDataClient gDataClient = getGDataClient();
+        GDataParserFactory gDataParserFactory = getGDataParserFactory();
+
+        InputStream is = gDataClient.getFeedAsStream(feedUri, authToken);
+        return gDataParserFactory.createParser(feedEntryClass, is);
+    }
+
+    /* (non-javadoc)
+     * @see GDataServiceClient#createEntry
+     */
+    public Entry createEntry(String feedUri, String authToken, Entry entry)
+            throws ParseException, IOException, HttpException {
+
+        GDataParserFactory factory = getGDataParserFactory();
+        GDataSerializer serializer = factory.createSerializer(entry);
+
+        InputStream is = getGDataClient().createEntry(feedUri, authToken, serializer);
+        GDataParser parser = factory.createParser(entry.getClass(), is);
+        try {
+            return parser.parseStandaloneEntry();
+        } finally {
+            parser.close();
+        }
+    }
+
+    /**
+     * Returns a parser for a Cells-based feed.
+     *
+     * @param feedUri the URI of the feed to be fetched and parsed
+     * @param authToken the current authToken to use for the request
+     * @return a parser for the indicated feed
+     * @throws HttpException if an http error is encountered
+     * @throws ParseException if the response from the server could not be
+     *         parsed
+     */
+    public GDataParser getParserForCellsFeed(String feedUri, String authToken)
+            throws ParseException, IOException, HttpException {
+        return getParserForTypedFeed(CellEntry.class, feedUri, authToken);
+    }
+
+    /**
+     * Fetches a GDataParser for the indicated feed. The parser can be used to
+     * access the contents of URI. WARNING: because we cannot reliably infer
+     * the feed type from the URI alone, this method assumes the default feed
+     * type! This is probably NOT what you want. Please use the
+     * getParserFor[Type]Feed methods.
+     *
+     * @param feedEntryClass
+     * @param feedUri the URI of the feed to be fetched and parsed
+     * @param authToken the current authToken to use for the request
+     * @return a parser for the indicated feed
+     * @throws HttpException if an http error is encountered
+     * @throws ParseException if the response from the server could not be
+     *         parsed
+     */
+    public GDataParser getParserForFeed(Class feedEntryClass, String feedUri, String authToken)
+            throws ParseException, IOException, HttpException {
+        GDataClient gDataClient = getGDataClient();
+        GDataParserFactory gDataParserFactory = getGDataParserFactory();
+        InputStream is = gDataClient.getFeedAsStream(feedUri, authToken);
+        return gDataParserFactory.createParser(feedEntryClass, is);
+    }
+
+    /**
+     * Returns a parser for a List (row-based) feed.
+     * 
+     * @param feedUri the URI of the feed to be fetched and parsed
+     * @param authToken the current authToken to use for the request
+     * @return a parser for the indicated feed
+     * @throws HttpException if an http error is encountered
+     * @throws ParseException if the response from the server could not be
+     *         parsed
+     */
+    public GDataParser getParserForListFeed(String feedUri, String authToken)
+            throws ParseException, IOException, HttpException {
+        return getParserForTypedFeed(ListEntry.class, feedUri, authToken);
+    }
+
+    /**
+     * Returns a parser for a Spreadsheets meta-feed.
+     * 
+     * @param feedUri the URI of the feed to be fetched and parsed
+     * @param authToken the current authToken to use for the request
+     * @return a parser for the indicated feed
+     * @throws HttpException if an http error is encountered
+     * @throws ParseException if the response from the server could not be
+     *         parsed
+     */
+    public GDataParser getParserForSpreadsheetsFeed(String feedUri, String authToken)
+            throws ParseException, IOException, HttpException {
+        return getParserForTypedFeed(SpreadsheetEntry.class, feedUri, authToken);
+    }
+
+    /**
+     * Returns a parser for a Worksheets meta-feed.
+     * 
+     * @param feedUri the URI of the feed to be fetched and parsed
+     * @param authToken the current authToken to use for the request
+     * @return a parser for the indicated feed
+     * @throws HttpException if an http error is encountered
+     * @throws ParseException if the response from the server could not be
+     *         parsed
+     */
+    public GDataParser getParserForWorksheetsFeed(String feedUri, String authToken)
+            throws ParseException, IOException, HttpException {
+        return getParserForTypedFeed(WorksheetEntry.class, feedUri, authToken);
+    }
+
+    /**
+     * Updates an entry. The URI to be updated is taken from
+     * <code>entry</code>. Note that only entries in List and Cells feeds
+     * can be updated, so <code>entry</code> must be of the corresponding
+     * type; other types will result in an exception.
+     * 
+     * @param entry the entry to be updated; must include its URI
+     * @param authToken the current authToken to be used for the operation
+     * @return An Entry containing the re-parsed version of the entry returned
+     *         by the server in response to the update.
+     * @throws HttpException if an http error is encountered
+     * @throws ParseException if the server returned an error, if the server's
+     *         response was unparseable (unlikely), or if <code>entry</code>
+     *         is of a read-only type
+     * @throws IOException on network error
+     */
+    public Entry updateEntry(Entry entry, String authToken)
+            throws ParseException, IOException, HttpException {
+        GDataParserFactory factory = getGDataParserFactory();
+        GDataSerializer serializer = factory.createSerializer(entry);
+
+        String editUri = entry.getEditUri();
+        if (StringUtils.isEmpty(editUri)) {
+            throw new ParseException("No edit URI -- cannot update.");
+        }
+
+        InputStream is = getGDataClient().updateEntry(editUri, authToken, serializer);
+        GDataParser parser = factory.createParser(entry.getClass(), is);
+        try {
+            return parser.parseStandaloneEntry();
+        } finally {
+            parser.close();
+        }
+    }
+
+    public String getBaseFeedUrl() {
+        return baseFeedUrl;
+    }  
+}
diff --git a/src/com/google/wireless/gdata2/spreadsheets/client/package.html b/src/com/google/wireless/gdata2/spreadsheets/client/package.html
new file mode 100644
index 0000000..1c9bf9d
--- /dev/null
+++ b/src/com/google/wireless/gdata2/spreadsheets/client/package.html
@@ -0,0 +1,5 @@
+<html>
+<body>
+    {@hide}
+</body>
+</html>
diff --git a/src/com/google/wireless/gdata2/spreadsheets/data/CellEntry.java b/src/com/google/wireless/gdata2/spreadsheets/data/CellEntry.java
new file mode 100755
index 0000000..82f8910
--- /dev/null
+++ b/src/com/google/wireless/gdata2/spreadsheets/data/CellEntry.java
@@ -0,0 +1,134 @@
+// Copyright 2007 The Android Open Source Project
+package com.google.wireless.gdata2.spreadsheets.data;
+
+import com.google.wireless.gdata2.data.Entry;
+
+/**
+ * Represents an entry in a GData Spreadsheets Cell-based feed.
+ */
+public class CellEntry extends Entry {
+    /** The spreadsheet column of the cell. */
+    private int col = -1;
+
+    /** The cell entry's inputValue attribute */
+    private String inputValue = null;
+
+    /** The cell entry's numericValue attribute */
+    private String numericValue = null;
+
+    /** The spreadsheet row of the cell */
+    private int row = -1;
+
+    /** The cell entry's text sub-element */
+    private String value = null;
+
+    /** Default constructor. */
+    public CellEntry() {
+        super();
+    }
+    
+    /**
+     * Fetches the cell's spreadsheet column.
+     * 
+     * @return the cell's spreadsheet column
+     */
+    public int getCol() {
+        return col;
+    }
+
+    /**
+     * Fetches the cell's inputValue attribute, which is the actual user input
+     * rather (such as a formula) than computed value of the cell.
+     * 
+     * @return the cell's inputValue
+     */
+    public String getInputValue() {
+        return inputValue;
+    }
+
+    /**
+     * Fetches the cell's numericValue attribute, which is a decimal
+     * representation.
+     * 
+     * @return the cell's numericValue
+     */
+    public String getNumericValue() {
+        return numericValue;
+    }
+
+    /**
+     * Fetches the cell's spreadsheet row.
+     * 
+     * @return the cell's spreadsheet row
+     */
+    public int getRow() {
+        return row;
+    }
+
+    /**
+     * Fetches the cell's contents, after any computation. For example, if the
+     * cell actually contains a formula, this will return the formula's computed
+     * value.
+     * 
+     * @return the computed value of the cell
+     */
+    public String getValue() {
+        return value;
+    }
+
+    /**
+     * Indicates whether the cell's contents are numeric.
+     * 
+     * @return true if the contents are numeric, or false if not
+     */
+    public boolean hasNumericValue() {
+        return numericValue != null;
+    }
+
+    /**
+     * Sets the cell's spreadsheet column.
+     * 
+     * @param col the new spreadsheet column of the cell
+     */
+    public void setCol(int col) {
+        this.col = col;
+    }
+
+    /**
+     * Sets the cell's actual contents (such as a formula, or a raw value.)
+     * 
+     * @param inputValue the new inputValue of the cell
+     */
+    public void setInputValue(String inputValue) {
+        this.inputValue = inputValue;
+    }
+
+    /**
+     * Sets the cell's numeric value. This can be different from the actual
+     * value; for instance, the actual value may be a thousands-delimited pretty
+     * string, while the numeric value could be the raw decimal.
+     * 
+     * @param numericValue the cell's new numericValue
+     */
+    public void setNumericValue(String numericValue) {
+        this.numericValue = numericValue;
+    }
+
+    /**
+     * Sets the cell's spreadsheet row.
+     * 
+     * @param row the new spreadsheet row of the cell
+     */
+    public void setRow(int row) {
+        this.row = row;
+    }
+
+    /**
+     * Sets the cell's computed value.
+     * 
+     * @param value the new value of the cell
+     */
+    public void setValue(String value) {
+        this.value = value;
+    }
+}
diff --git a/src/com/google/wireless/gdata2/spreadsheets/data/CellFeed.java b/src/com/google/wireless/gdata2/spreadsheets/data/CellFeed.java
new file mode 100755
index 0000000..19d6ad9
--- /dev/null
+++ b/src/com/google/wireless/gdata2/spreadsheets/data/CellFeed.java
@@ -0,0 +1,35 @@
+// Copyright 2007 The Android Open Source Project
+package com.google.wireless.gdata2.spreadsheets.data;
+
+import com.google.wireless.gdata2.data.Feed;
+
+/**
+ * A feed handler for GData Spreadsheets cell-based feeds.
+ */
+public class CellFeed extends Feed {
+    private String editUri;
+
+    /** Default constructor. */
+    public CellFeed() {
+        super();
+    }
+
+    /**
+     * Fetches the URI to which edits (such as cell content updates) should
+     * go.
+     * 
+     * @return the edit URI for this feed
+     */
+    public String getEditUri() {
+        return editUri;
+    }
+
+    /**
+     * Sets the URI to which edits (such as cell content updates) should go.
+     * 
+     * @param editUri the new edit URI for this feed
+     */
+    public void setEditUri(String editUri) {
+        this.editUri = editUri;
+    }
+}
diff --git a/src/com/google/wireless/gdata2/spreadsheets/data/ListEntry.java b/src/com/google/wireless/gdata2/spreadsheets/data/ListEntry.java
new file mode 100755
index 0000000..570bd18
--- /dev/null
+++ b/src/com/google/wireless/gdata2/spreadsheets/data/ListEntry.java
@@ -0,0 +1,77 @@
+// Copyright 2007 The Android Open Source Project
+package com.google.wireless.gdata2.spreadsheets.data;
+
+import com.google.wireless.gdata2.data.Entry;
+import com.google.wireless.gdata2.data.StringUtils;
+
+import java.util.Enumeration;
+import java.util.Hashtable;
+import java.util.Vector;
+
+/**
+ * Represents an entry in a GData Spreadsheets List feed.
+ */
+public class ListEntry extends Entry {
+    /** Map containing the values in the row. */
+    private Hashtable values = new Hashtable();
+    
+    /** Caches the list of names, so they don't need to be recomputed. */
+    private Vector names = null;
+
+    /**
+     * Retrieves the column names present in this row.
+     * 
+     * @return a Set of Strings, one per column where data exists
+     */
+    public Vector getNames() {
+        if (names != null) {
+            return names;
+        }
+        names = new Vector();
+        Enumeration e = values.keys();
+        while (e.hasMoreElements()) {
+            names.add(e.nextElement());
+        }
+        return names;
+    }
+
+    /**
+     * Fetches the value for a column. Equivalent to
+     * <code>getValue(name, null)</code>.
+     * 
+     * @param name the name of the column whose row is to be fetched
+     * @return the value of the column, or null if the column is not present
+     */
+    public String getValue(String name) {
+        return getValue(name, null);
+    }
+
+    /**
+     * Fetches the value for a column.
+     * 
+     * @param name the name of the column whose row is to be fetched
+     * @param defaultValue the value to return if the row has no value for the
+     *        requested column; may be null
+     * @return the value of the column, or null if the column is not present
+     */
+    public String getValue(String name, String defaultValue) {
+        if (StringUtils.isEmpty(name)) {
+            return defaultValue;
+        }
+        String val = (String) values.get(name);
+        if (val == null) {
+            return defaultValue;
+        }
+        return val;
+    }
+
+    /**
+     * Sets the value of a column.
+     * 
+     * @param name the name of the column
+     * @param value the value for the column
+     */
+    public void setValue(String name, String value) {
+        values.put(name, value == null ? "" : value);
+    }
+}
diff --git a/src/com/google/wireless/gdata2/spreadsheets/data/ListFeed.java b/src/com/google/wireless/gdata2/spreadsheets/data/ListFeed.java
new file mode 100755
index 0000000..2fcc0a9
--- /dev/null
+++ b/src/com/google/wireless/gdata2/spreadsheets/data/ListFeed.java
@@ -0,0 +1,35 @@
+// Copyright 2007 The Android Open Source Project
+package com.google.wireless.gdata2.spreadsheets.data;
+
+import com.google.wireless.gdata2.data.Feed;
+
+/**
+ * A feed handler for GData Spreadsheets List-based feeds.
+ */
+public class ListFeed extends Feed {
+    private String editUri;
+
+    /** Default constructor. */
+    public ListFeed() {
+        super();
+    }
+
+    /**
+     * Fetches the URI to which edits (such as cell content updates) should
+     * go.
+     * 
+     * @return the edit URI for this feed
+     */
+    public String getEditUri() {
+        return editUri;
+    }
+
+    /**
+     * Sets the URI to which edits (such as cell content updates) should go.
+     * 
+     * @param editUri the new edit URI for this feed
+     */
+    public void setEditUri(String editUri) {
+        this.editUri = editUri;
+    }
+}
diff --git a/src/com/google/wireless/gdata2/spreadsheets/data/SpreadsheetEntry.java b/src/com/google/wireless/gdata2/spreadsheets/data/SpreadsheetEntry.java
new file mode 100755
index 0000000..74c5344
--- /dev/null
+++ b/src/com/google/wireless/gdata2/spreadsheets/data/SpreadsheetEntry.java
@@ -0,0 +1,38 @@
+// Copyright 2007 The Android Open Source Project
+package com.google.wireless.gdata2.spreadsheets.data;
+
+import com.google.wireless.gdata2.GDataException;
+import com.google.wireless.gdata2.data.Entry;
+import com.google.wireless.gdata2.data.StringUtils;
+
+/**
+ * Represents an entry in a GData Spreadsheets meta-feed.
+ */
+public class SpreadsheetEntry extends Entry {
+    /** The URI of the worksheets meta-feed for this spreadsheet */
+    private String worksheetsUri = null;
+
+    /**
+     * Fetches the URI of the worksheets meta-feed (that is, list of worksheets)
+     * for this spreadsheet.
+     * 
+     * @return the worksheets meta-feed URI
+     * @throws GDataException if the unique key is not set
+     */
+    public String getWorksheetFeedUri() throws GDataException {
+        if (StringUtils.isEmpty(worksheetsUri)) {
+            throw new GDataException("worksheet URI is not set");
+        }
+        return worksheetsUri;
+    }
+
+    /**
+     * Sets the URI of the worksheet meta-feed corresponding to this
+     * spreadsheet.
+     * 
+     * @param href
+     */
+    public void setWorksheetFeedUri(String href) {
+        worksheetsUri = href;
+    }
+}
diff --git a/src/com/google/wireless/gdata2/spreadsheets/data/SpreadsheetFeed.java b/src/com/google/wireless/gdata2/spreadsheets/data/SpreadsheetFeed.java
new file mode 100755
index 0000000..ab2cdbb
--- /dev/null
+++ b/src/com/google/wireless/gdata2/spreadsheets/data/SpreadsheetFeed.java
@@ -0,0 +1,14 @@
+// Copyright 2007 The Android Open Source Project
+package com.google.wireless.gdata2.spreadsheets.data;
+
+import com.google.wireless.gdata2.data.Feed;
+
+/**
+ * Feed handler for a GData Spreadsheets meta-feed.
+ */
+public class SpreadsheetFeed extends Feed {
+    /** Default constructor. */
+    public SpreadsheetFeed() {
+        super();
+    }
+}
diff --git a/src/com/google/wireless/gdata2/spreadsheets/data/WorksheetEntry.java b/src/com/google/wireless/gdata2/spreadsheets/data/WorksheetEntry.java
new file mode 100644
index 0000000..9c77221
--- /dev/null
+++ b/src/com/google/wireless/gdata2/spreadsheets/data/WorksheetEntry.java
@@ -0,0 +1,105 @@
+// Copyright 2007 The Android Open Source Project
+package com.google.wireless.gdata2.spreadsheets.data;
+
+import com.google.wireless.gdata2.GDataException;
+import com.google.wireless.gdata2.data.Entry;
+import com.google.wireless.gdata2.data.StringUtils;
+
+/**
+ * Represents an entry in a GData Worksheets meta-feed.
+ */
+public class WorksheetEntry extends Entry {
+    /** The URI to this entry's cells feed. */
+    private String cellsUri = null;
+
+    /** The number of columns in the worksheet. */
+    private int colCount = -1;
+
+    /** The URI to this entry's list feed. */
+    private String listUri = null;
+
+    /** The number of rows in the worksheet. */
+    private int rowCount = -1;
+
+    /**
+     * Fetches the URI of this entry's Cells feed.
+     * 
+     * @return The URI of the entry's Cells feed.
+     */
+    public String getCellFeedUri() {
+        return cellsUri;
+    }
+
+    /**
+     * Fetches the number of columns in the worksheet.
+     * 
+     * @return The number of columns.
+     */
+    public int getColCount() {
+        return colCount;
+    }
+
+    /**
+     * Fetches the URI of this entry's List feed.
+     * 
+     * @return The URI of the entry's List feed.
+     * @throws GDataException If the URI is not set or is invalid.
+     */
+    public String getListFeedUri() {
+        return listUri;
+    }
+
+    /**
+     * Fetches the number of rows in the worksheet.
+     * 
+     * @return The number of rows.
+     */
+    public int getRowCount() {
+        return rowCount;
+    }
+
+    /**
+     * Sets the URI of this entry's Cells feed.
+     * 
+     * @param href The HREF attribute that should be the Cells feed URI.
+     */
+    public void setCellFeedUri(String href) {
+        cellsUri = href;
+    }
+
+    /**
+     * Sets the number of columns in the worksheet.
+     * 
+     * @param colCount The new number of columns.
+     */
+    public void setColCount(int colCount) {
+        this.colCount = colCount;
+    }
+
+    /**
+     * Sets this entry's Atom ID.
+     * 
+     * @param id The new ID value.
+     */
+    public void setId(String id) {
+        super.setId(id);
+    }
+
+    /**
+     * Sets the URI of this entry's List feed.
+     * 
+     * @param href The HREF attribute that should be the List feed URI.
+     */
+    public void setListFeedUri(String href) {
+        listUri = href;
+    }
+
+    /**
+     * Sets the number of rows in the worksheet.
+     * 
+     * @param rowCount The new number of rows.
+     */
+    public void setRowCount(int rowCount) {
+        this.rowCount = rowCount;
+    }
+}
diff --git a/src/com/google/wireless/gdata2/spreadsheets/data/WorksheetFeed.java b/src/com/google/wireless/gdata2/spreadsheets/data/WorksheetFeed.java
new file mode 100755
index 0000000..000655b
--- /dev/null
+++ b/src/com/google/wireless/gdata2/spreadsheets/data/WorksheetFeed.java
@@ -0,0 +1,14 @@
+// Copyright 2007 The Android Open Source Project
+package com.google.wireless.gdata2.spreadsheets.data;
+
+import com.google.wireless.gdata2.data.Feed;
+
+/**
+ * Feed handler for GData Spreadsheets Worksheet meta-feed.
+ */
+public class WorksheetFeed extends Feed {
+    /** Default constructor. */
+    public WorksheetFeed() {
+        super();
+    }
+}
diff --git a/src/com/google/wireless/gdata2/spreadsheets/data/package.html b/src/com/google/wireless/gdata2/spreadsheets/data/package.html
new file mode 100644
index 0000000..1c9bf9d
--- /dev/null
+++ b/src/com/google/wireless/gdata2/spreadsheets/data/package.html
@@ -0,0 +1,5 @@
+<html>
+<body>
+    {@hide}
+</body>
+</html>
diff --git a/src/com/google/wireless/gdata2/spreadsheets/package.html b/src/com/google/wireless/gdata2/spreadsheets/package.html
new file mode 100644
index 0000000..1c9bf9d
--- /dev/null
+++ b/src/com/google/wireless/gdata2/spreadsheets/package.html
@@ -0,0 +1,5 @@
+<html>
+<body>
+    {@hide}
+</body>
+</html>
diff --git a/src/com/google/wireless/gdata2/spreadsheets/parser/package.html b/src/com/google/wireless/gdata2/spreadsheets/parser/package.html
new file mode 100644
index 0000000..1c9bf9d
--- /dev/null
+++ b/src/com/google/wireless/gdata2/spreadsheets/parser/package.html
@@ -0,0 +1,5 @@
+<html>
+<body>
+    {@hide}
+</body>
+</html>
diff --git a/src/com/google/wireless/gdata2/spreadsheets/parser/xml/XmlCellsGDataParser.java b/src/com/google/wireless/gdata2/spreadsheets/parser/xml/XmlCellsGDataParser.java
new file mode 100755
index 0000000..043fe5b
--- /dev/null
+++ b/src/com/google/wireless/gdata2/spreadsheets/parser/xml/XmlCellsGDataParser.java
@@ -0,0 +1,126 @@
+// Copyright 2007 The Android Open Source Project
+package com.google.wireless.gdata2.spreadsheets.parser.xml;
+
+import com.google.wireless.gdata2.data.Entry;
+import com.google.wireless.gdata2.data.Feed;
+import com.google.wireless.gdata2.data.StringUtils;
+import com.google.wireless.gdata2.data.XmlUtils;
+import com.google.wireless.gdata2.parser.ParseException;
+import com.google.wireless.gdata2.parser.xml.XmlGDataParser;
+import com.google.wireless.gdata2.spreadsheets.data.CellEntry;
+import com.google.wireless.gdata2.spreadsheets.data.CellFeed;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Parser for non-Atom data in a GData Spreadsheets Cell-based feed.
+ */
+public class XmlCellsGDataParser extends XmlGDataParser {
+    /**
+     * The rel ID used by the server to identify the URLs for Cell POSTs
+     * (updates)
+     */
+    private static final String CELL_FEED_POST_REL =
+            "http://schemas.google.com/g/2005#post";
+
+    /**
+     * Creates a new XmlCellsGDataParser.
+     * 
+     * @param is the stream from which to read the data
+     * @param xmlParser the XMLPullParser to use for parsing the raw XML
+     * @throws ParseException if the super-class throws one
+     */
+    public XmlCellsGDataParser(InputStream is, XmlPullParser xmlParser)
+            throws ParseException {
+        super(is, xmlParser);
+    }
+
+    /* (non-JavaDoc)
+     * Creates a new Entry that can handle the data parsed by this class.
+     */
+    protected Entry createEntry() {
+        return new CellEntry();
+    }
+
+    /* (non-JavaDoc)
+     * Creates a new Feed that can handle the data parsed by this class.
+     */
+    protected Feed createFeed() {
+        return new CellFeed();
+    }
+
+    /* (non-JavaDoc)
+     * Callback to handle non-Atom data present in an Atom entry tag.
+     */
+    protected void handleExtraElementInEntry(Entry entry)
+            throws XmlPullParserException, IOException {
+        XmlPullParser parser = getParser();
+        if (!(entry instanceof CellEntry)) {
+            throw new IllegalArgumentException("Expected CellEntry!");
+        }
+        CellEntry row = (CellEntry) entry;
+
+        String name = parser.getName();
+        // cells can only have row, col, inputValue, & numericValue attrs
+        if ("cell".equals(name)) {
+            int count = parser.getAttributeCount();
+            String attrName = null;
+            for (int i = 0; i < count; ++i) {
+                attrName = parser.getAttributeName(i);
+                if ("row".equals(attrName)) {
+                    row.setRow(StringUtils.parseInt(parser
+                            .getAttributeValue(i), 0));
+                } else if ("col".equals(attrName)) {
+                    row.setCol(StringUtils.parseInt(parser
+                            .getAttributeValue(i), 0));
+                } else if ("numericValue".equals(attrName)) {
+                    row.setNumericValue(parser.getAttributeValue(i));
+                } else if ("inputValue".equals(attrName)) {
+                    row.setInputValue(parser.getAttributeValue(i));
+                }
+            }
+
+            // also need the data stored in the child text node
+            row.setValue(XmlUtils.extractChildText(parser));
+        }
+    }
+
+    /* (non-JavaDoc)
+     * Callback to handle non-Atom data in the feed.
+     */
+    protected void handleExtraElementInFeed(Feed feed)
+            throws XmlPullParserException, IOException {
+        XmlPullParser parser = getParser();
+        if (!(feed instanceof CellFeed)) {
+            throw new IllegalArgumentException("Expected CellFeed!");
+        }
+        CellFeed cellFeed = (CellFeed) feed;
+
+        String name = parser.getName();
+        if (!"link".equals(name)) {
+            return;
+        }
+
+        int numAttrs = parser.getAttributeCount();
+        String rel = null;
+        String href = null;
+        String attrName = null;
+        for (int i = 0; i < numAttrs; ++i) {
+            attrName = parser.getAttributeName(i);
+            if ("rel".equals(attrName)) {
+                rel = parser.getAttributeValue(i);
+            } else if ("href".equals(attrName)) {
+                href = parser.getAttributeValue(i);
+            }
+        }
+        if (!(StringUtils.isEmpty(rel) || StringUtils.isEmpty(href))) {
+            if (CELL_FEED_POST_REL.equals(rel)) {
+                cellFeed.setEditUri(href);
+            }
+        }
+    }
+}
diff --git a/src/com/google/wireless/gdata2/spreadsheets/parser/xml/XmlListGDataParser.java b/src/com/google/wireless/gdata2/spreadsheets/parser/xml/XmlListGDataParser.java
new file mode 100755
index 0000000..6578a1f
--- /dev/null
+++ b/src/com/google/wireless/gdata2/spreadsheets/parser/xml/XmlListGDataParser.java
@@ -0,0 +1,109 @@
+// Copyright 2007 The Android Open Source Project
+package com.google.wireless.gdata2.spreadsheets.parser.xml;
+
+import com.google.wireless.gdata2.data.Entry;
+import com.google.wireless.gdata2.data.Feed;
+import com.google.wireless.gdata2.data.StringUtils;
+import com.google.wireless.gdata2.data.XmlUtils;
+import com.google.wireless.gdata2.parser.ParseException;
+import com.google.wireless.gdata2.parser.xml.XmlGDataParser;
+import com.google.wireless.gdata2.spreadsheets.data.ListEntry;
+import com.google.wireless.gdata2.spreadsheets.data.ListFeed;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Parser for non-Atom data in a GData Spreadsheets List-based feed.
+ */
+public class XmlListGDataParser extends XmlGDataParser {
+    /**
+     * The rel ID used by the server to identify the URLs for List POSTs
+     * (updates)
+     */
+    private static final String LIST_FEED_POST_REL =
+            "http://schemas.google.com/g/2005#post";
+
+    /**
+     * Creates a new XmlListGDataParser.
+     * 
+     * @param is the stream from which to read the data
+     * @param xmlParser the XmlPullParser to use to parse the raw XML
+     * @throws ParseException if the super-class throws one
+     */
+    public XmlListGDataParser(InputStream is, XmlPullParser xmlParser)
+            throws ParseException {
+        super(is, xmlParser);
+    }
+
+    /* (non-JavaDoc)
+     * Creates a new Entry that can handle the data parsed by this class.
+     */
+    protected Entry createEntry() {
+        return new ListEntry();
+    }
+
+    /* (non-JavaDoc)
+     * Creates a new Feed that can handle the data parsed by this class.
+     */
+    protected Feed createFeed() {
+        return new ListFeed();
+    }
+
+    /* (non-JavaDoc)
+     * Callback to handle non-Atom data present in an Atom entry tag.
+     */
+    protected void handleExtraElementInEntry(Entry entry)
+            throws XmlPullParserException, IOException {
+        XmlPullParser parser = getParser();
+        if (!(entry instanceof ListEntry)) {
+            throw new IllegalArgumentException("Expected ListEntry!");
+        }
+        ListEntry row = (ListEntry) entry;
+
+        String name = parser.getName();
+        row.setValue(name, XmlUtils.extractChildText(parser));
+    }
+
+    /* (non-JavaDoc)
+     * Callback to handle non-Atom data in the feed.
+     */
+    protected void handleExtraElementInFeed(Feed feed)
+            throws XmlPullParserException, IOException {
+        XmlPullParser parser = getParser();
+        if (!(feed instanceof ListFeed)) {
+            throw new IllegalArgumentException("Expected ListFeed!");
+        }
+        ListFeed listFeed = (ListFeed) feed;
+
+        String name = parser.getName();
+        if (!"link".equals(name)) {
+            return;
+        }
+
+        // lists store column data in the gsx namespace:
+        // <gsx:columnheader>data</gsx:columnheader>
+        // The columnheader tag names are the scrubbed values of the first row.
+        // We extract them all and store them as keys in a Map.
+        int numAttrs = parser.getAttributeCount();
+        String rel = null;
+        String href = null;
+        String attrName = null;
+        for (int i = 0; i < numAttrs; ++i) {
+            attrName = parser.getAttributeName(i);
+            if ("rel".equals(attrName)) {
+                rel = parser.getAttributeValue(i);
+            } else if ("href".equals(attrName)) {
+                href = parser.getAttributeValue(i);
+            }
+        }
+        if (!(StringUtils.isEmpty(rel) || StringUtils.isEmpty(href))) {
+            if (LIST_FEED_POST_REL.equals(rel)) {
+                listFeed.setEditUri(href);
+            }
+        }
+    }
+}
diff --git a/src/com/google/wireless/gdata2/spreadsheets/parser/xml/XmlSpreadsheetsGDataParser.java b/src/com/google/wireless/gdata2/spreadsheets/parser/xml/XmlSpreadsheetsGDataParser.java
new file mode 100755
index 0000000..596e624
--- /dev/null
+++ b/src/com/google/wireless/gdata2/spreadsheets/parser/xml/XmlSpreadsheetsGDataParser.java
@@ -0,0 +1,68 @@
+// Copyright 2007 The Android Open Source Project
+package com.google.wireless.gdata2.spreadsheets.parser.xml;
+
+import com.google.wireless.gdata2.data.Entry;
+import com.google.wireless.gdata2.data.Feed;
+import com.google.wireless.gdata2.parser.ParseException;
+import com.google.wireless.gdata2.parser.xml.XmlGDataParser;
+import com.google.wireless.gdata2.spreadsheets.data.SpreadsheetEntry;
+import com.google.wireless.gdata2.spreadsheets.data.SpreadsheetFeed;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Parser helper for non-Atom data in a GData Spreadsheets meta-feed.
+ */
+public class XmlSpreadsheetsGDataParser extends XmlGDataParser {
+    /**
+     * The rel ID used by the server to identify the URLs for the worksheets
+     * feed
+     */
+    protected static final String WORKSHEET_FEED_REL =
+            "http://schemas.google.com/spreadsheets/2006#worksheetsfeed";
+
+    /**
+     * Creates a new XmlSpreadsheetsGDataParser.
+     * 
+     * @param is the stream from which to read the data
+     * @param xmlParser the XmlPullParser to use to parse the raw XML
+     * @throws ParseException if the super-class throws one
+     */
+    public XmlSpreadsheetsGDataParser(InputStream is, XmlPullParser xmlParser)
+            throws ParseException {
+        super(is, xmlParser);
+    }
+
+    /* (non-JavaDoc)
+     * Creates a new Entry that can handle the data parsed by this class.
+     */
+    protected Entry createEntry() {
+        return new SpreadsheetEntry();
+    }
+
+    /* (non-JavaDoc)
+     * Creates a new Feed that can handle the data parsed by this class.
+     */
+    protected Feed createFeed() {
+        return new SpreadsheetFeed();
+    }
+
+    /* (non-JavaDoc)
+     * Callback to handle link elements that are not recognized as Atom links.
+     * Used to pick out the link tag in a Spreadsheet's entry that corresponds
+     * to that spreadsheet's worksheets meta-feed.
+     */
+    protected void handleExtraLinkInEntry(String rel, String type, String href,
+            Entry entry) throws XmlPullParserException, IOException {
+        super.handleExtraLinkInEntry(rel, type, href, entry);
+        if (WORKSHEET_FEED_REL.equals(rel)
+                && "application/atom+xml".equals(type)) {
+            SpreadsheetEntry sheet = (SpreadsheetEntry) entry;
+            sheet.setWorksheetFeedUri(href);
+        }
+    }
+}
diff --git a/src/com/google/wireless/gdata2/spreadsheets/parser/xml/XmlSpreadsheetsGDataParserFactory.java b/src/com/google/wireless/gdata2/spreadsheets/parser/xml/XmlSpreadsheetsGDataParserFactory.java
new file mode 100644
index 0000000..8aaf300
--- /dev/null
+++ b/src/com/google/wireless/gdata2/spreadsheets/parser/xml/XmlSpreadsheetsGDataParserFactory.java
@@ -0,0 +1,99 @@
+// Copyright 2007 The Android Open Source Project
+package com.google.wireless.gdata2.spreadsheets.parser.xml;
+
+import com.google.wireless.gdata2.client.GDataParserFactory;
+import com.google.wireless.gdata2.data.Entry;
+import com.google.wireless.gdata2.parser.GDataParser;
+import com.google.wireless.gdata2.parser.ParseException;
+import com.google.wireless.gdata2.parser.xml.XmlParserFactory;
+import com.google.wireless.gdata2.serializer.GDataSerializer;
+import com.google.wireless.gdata2.spreadsheets.data.CellEntry;
+import com.google.wireless.gdata2.spreadsheets.data.ListEntry;
+import com.google.wireless.gdata2.spreadsheets.data.SpreadsheetEntry;
+import com.google.wireless.gdata2.spreadsheets.data.WorksheetEntry;
+import com.google.wireless.gdata2.spreadsheets.serializer.xml.XmlCellEntryGDataSerializer;
+import com.google.wireless.gdata2.spreadsheets.serializer.xml.XmlListEntryGDataSerializer;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.InputStream;
+
+/**
+ * A GDataParserFactory capable of handling Spreadsheets.
+ */
+public class XmlSpreadsheetsGDataParserFactory implements GDataParserFactory {
+    /*
+     * @see GDataParserFactory
+     */
+    public XmlSpreadsheetsGDataParserFactory(XmlParserFactory xmlFactory) {
+        this.xmlFactory = xmlFactory;
+    }
+
+    /** Intentionally private. */
+    private XmlSpreadsheetsGDataParserFactory() {
+    }
+
+    /*
+     * Creates a parser for the indicated feed, assuming the default feed type.
+     * The default type is specified on {@link SpreadsheetsClient#DEFAULT_FEED}.
+     * 
+     * @param is The stream containing the feed to be parsed.
+     * @return A GDataParser capable of parsing the feed as the default type.
+     * @throws ParseException if the feed could not be parsed for any reason
+     */
+    public GDataParser createParser(InputStream is) throws ParseException {
+        // attempt a default
+        return createParser(SpreadsheetEntry.class, is);
+    }
+
+    /*
+     * Creates a parser of the indicated type for the indicated feed.
+     * 
+     * @param feedType The type of the feed; must be one of the constants on
+     *        {@link SpreadsheetsClient}.
+     * @return A parser capable of parsing the feed as the indicated type.
+     * @throws ParseException if the feed could not be parsed for any reason
+     */
+    public GDataParser createParser(Class entryClass, InputStream is)
+            throws ParseException {
+        try {
+            XmlPullParser xmlParser = xmlFactory.createParser();
+            if (entryClass == SpreadsheetEntry.class) {
+                return new XmlSpreadsheetsGDataParser(is, xmlParser);
+            } else if (entryClass == WorksheetEntry.class) {
+                return new XmlWorksheetsGDataParser(is, xmlParser);
+            } else if (entryClass == CellEntry.class) {
+                return new XmlCellsGDataParser(is, xmlParser);
+            } else if (entryClass == ListEntry.class) {
+                return new XmlListGDataParser(is, xmlParser);
+            } else {
+                throw new ParseException("Unrecognized feed requested.");
+            }
+        } catch (XmlPullParserException e) {
+            throw new ParseException("Failed to create parser", e);
+        }
+    }
+
+    /*
+     * Creates a serializer capable of handling the indicated entry.
+     * 
+     * @param The Entry to be serialized to an XML string.
+     * @return A GDataSerializer capable of handling the indicated entry.
+     * @throws IllegalArgumentException if Entry is not a supported type (which
+     *         currently includes only {@link ListEntry} and {@link CellEntry}.)
+     */
+    public GDataSerializer createSerializer(Entry entry) {
+        if (entry instanceof ListEntry) {
+            return new XmlListEntryGDataSerializer(xmlFactory, entry);
+        } else if (entry instanceof CellEntry) {
+            return new XmlCellEntryGDataSerializer(xmlFactory, entry);
+        } else {
+            throw new IllegalArgumentException(
+                    "Expected a ListEntry or CellEntry");
+        }
+    }
+
+    /** The XmlParserFactory to use to actually process XML streams. */
+    private XmlParserFactory xmlFactory;
+}
diff --git a/src/com/google/wireless/gdata2/spreadsheets/parser/xml/XmlWorksheetsGDataParser.java b/src/com/google/wireless/gdata2/spreadsheets/parser/xml/XmlWorksheetsGDataParser.java
new file mode 100755
index 0000000..9007b6b
--- /dev/null
+++ b/src/com/google/wireless/gdata2/spreadsheets/parser/xml/XmlWorksheetsGDataParser.java
@@ -0,0 +1,98 @@
+// Copyright 2007 The Android Open Source Project
+package com.google.wireless.gdata2.spreadsheets.parser.xml;
+
+import com.google.wireless.gdata2.data.Entry;
+import com.google.wireless.gdata2.data.Feed;
+import com.google.wireless.gdata2.data.StringUtils;
+import com.google.wireless.gdata2.data.XmlUtils;
+import com.google.wireless.gdata2.parser.ParseException;
+import com.google.wireless.gdata2.parser.xml.XmlGDataParser;
+import com.google.wireless.gdata2.spreadsheets.data.WorksheetEntry;
+import com.google.wireless.gdata2.spreadsheets.data.WorksheetFeed;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Parser helper for non-Atom data in a GData Spreadsheets Worksheets meta-feed.
+ */
+public class XmlWorksheetsGDataParser extends XmlGDataParser {
+    /**
+     * The rel ID used by the server to identify the cells feed for a worksheet
+     */
+    protected static final String CELLS_FEED_REL =
+            "http://schemas.google.com/spreadsheets/2006#cellsfeed";
+
+    /**
+     * The rel ID used by the server to identify the list feed for a worksheet
+     */
+    protected static final String LIST_FEED_REL =
+            "http://schemas.google.com/spreadsheets/2006#listfeed";
+
+    /**
+     * Creates a new XmlWorksheetsGDataParser.
+     * 
+     * @param is the stream from which to read the data
+     * @param xmlParser the XmlPullParser to use to parse the raw XML
+     * @throws ParseException if the super-class throws one
+     */
+    public XmlWorksheetsGDataParser(InputStream is, XmlPullParser xmlParser)
+            throws ParseException {
+        super(is, xmlParser);
+    }
+
+    /* (non-JavaDoc)
+     * Creates a new Entry that can handle the data parsed by this class.
+     */
+    protected Entry createEntry() {
+        return new WorksheetEntry();
+    }
+
+    /* (non-JavaDoc)
+     * Creates a new Feed that can handle the data parsed by this class.
+     */
+    protected Feed createFeed() {
+        return new WorksheetFeed();
+    }
+
+    /* (non-JavaDoc)
+     * Callback to handle non-Atom data present in an Atom entry tag.
+     */
+    protected void handleExtraElementInEntry(Entry entry)
+            throws XmlPullParserException, IOException {
+        XmlPullParser parser = getParser();
+        if (!(entry instanceof WorksheetEntry)) {
+            throw new IllegalArgumentException("Expected WorksheetEntry!");
+        }
+        WorksheetEntry worksheet = (WorksheetEntry) entry;
+
+        // the only custom elements are rowCount and colCount
+        String name = parser.getName();
+        if ("rowCount".equals(name)) {
+            worksheet.setRowCount(StringUtils.parseInt(XmlUtils
+                    .extractChildText(parser), 0));
+        } else if ("colCount".equals(name)) {
+            worksheet.setColCount(StringUtils.parseInt(XmlUtils
+                    .extractChildText(parser), 0));
+        }
+    }
+
+    /* (non-JavaDoc)
+     * Callback to handle non-Atom links present in an Atom entry tag. Used to
+     * pick out a worksheet's cells and list feeds.
+     */
+    protected void handleExtraLinkInEntry(String rel, String type, String href,
+            Entry entry) throws XmlPullParserException, IOException {
+        if (LIST_FEED_REL.equals(rel) && "application/atom+xml".equals(type)) {
+            WorksheetEntry sheet = (WorksheetEntry) entry;
+            sheet.setListFeedUri(href);
+        } else if (CELLS_FEED_REL.equals(rel)
+                && "application/atom+xml".equals(type)) {
+            WorksheetEntry sheet = (WorksheetEntry) entry;
+            sheet.setCellFeedUri(href);
+        }
+    }
+}
diff --git a/src/com/google/wireless/gdata2/spreadsheets/parser/xml/package.html b/src/com/google/wireless/gdata2/spreadsheets/parser/xml/package.html
new file mode 100644
index 0000000..1c9bf9d
--- /dev/null
+++ b/src/com/google/wireless/gdata2/spreadsheets/parser/xml/package.html
@@ -0,0 +1,5 @@
+<html>
+<body>
+    {@hide}
+</body>
+</html>
diff --git a/src/com/google/wireless/gdata2/spreadsheets/serializer/package.html b/src/com/google/wireless/gdata2/spreadsheets/serializer/package.html
new file mode 100644
index 0000000..1c9bf9d
--- /dev/null
+++ b/src/com/google/wireless/gdata2/spreadsheets/serializer/package.html
@@ -0,0 +1,5 @@
+<html>
+<body>
+    {@hide}
+</body>
+</html>
diff --git a/src/com/google/wireless/gdata2/spreadsheets/serializer/xml/XmlCellEntryGDataSerializer.java b/src/com/google/wireless/gdata2/spreadsheets/serializer/xml/XmlCellEntryGDataSerializer.java
new file mode 100755
index 0000000..9a382c4
--- /dev/null
+++ b/src/com/google/wireless/gdata2/spreadsheets/serializer/xml/XmlCellEntryGDataSerializer.java
@@ -0,0 +1,83 @@
+package com.google.wireless.gdata2.spreadsheets.serializer.xml;
+
+import com.google.wireless.gdata2.data.Entry;
+import com.google.wireless.gdata2.data.StringUtils;
+import com.google.wireless.gdata2.parser.ParseException;
+import com.google.wireless.gdata2.parser.xml.XmlParserFactory;
+import com.google.wireless.gdata2.serializer.xml.XmlEntryGDataSerializer;
+import com.google.wireless.gdata2.spreadsheets.data.CellEntry;
+
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.IOException;
+
+/**
+ * A serializer for handling GData Spreadsheets Cell entries.
+ */
+public class XmlCellEntryGDataSerializer extends XmlEntryGDataSerializer {
+    /** The namespace to use for the GData Cell attributes */
+    public static final String NAMESPACE_GS = "gs";
+
+    /** The URI of the GData Cell namespace */
+    public static final String NAMESPACE_GS_URI =
+            "http://schemas.google.com/spreadsheets/2006";
+
+    /**
+     * Creates a new XmlCellEntryGDataSerializer.
+     * 
+     * @param entry the entry to be serialized
+     */
+    public XmlCellEntryGDataSerializer(XmlParserFactory xmlFactory,
+            Entry entry) {
+        super(xmlFactory, entry);
+    }
+
+    /**
+     * Sets up the GData Cell namespace.
+     * 
+     * @param serializer the serializer to use
+     */
+    protected void declareExtraEntryNamespaces(XmlSerializer serializer)
+            throws IOException {
+        serializer.setPrefix(NAMESPACE_GS, NAMESPACE_GS_URI);
+    }
+
+    /*
+     * Handles the non-Atom data belonging to the GData Spreadsheets Cell
+     * namespace.
+     * 
+     * @param serializer the XML serializer to use
+     * @param format unused
+     * @throws ParseException if the data could not be serialized
+     * @throws IOException on network error
+     */
+    protected void serializeExtraEntryContents(XmlSerializer serializer,
+            int format) throws ParseException, IOException {
+        CellEntry entry = (CellEntry) getEntry();
+        int row = entry.getRow();
+        int col = entry.getCol();
+        String value = entry.getValue();
+        String inputValue = entry.getInputValue();
+        if (row < 0 || col < 0) {
+            throw new ParseException("Negative row or column value");
+        }
+
+        // cells require row & col attrs, and allow inputValue and
+        // numericValue
+        serializer.startTag(NAMESPACE_GS_URI, "cell");
+        serializer.attribute(null /* ns */, "row", "" + row);
+        serializer.attribute(null /* ns */, "col", "" + col);
+        if (inputValue != null) {
+            serializer.attribute(null /* ns */, "inputValue", inputValue);
+        }
+        if (entry.hasNumericValue()) {
+            serializer.attribute(null /* ns */, "numericValue", entry
+                    .getNumericValue());
+        }
+
+        // set the child text...
+        value = StringUtils.isEmpty(value) ? "" : value;
+        serializer.text(value);
+        serializer.endTag(NAMESPACE_GS_URI, "cell");
+    }
+}
diff --git a/src/com/google/wireless/gdata2/spreadsheets/serializer/xml/XmlListEntryGDataSerializer.java b/src/com/google/wireless/gdata2/spreadsheets/serializer/xml/XmlListEntryGDataSerializer.java
new file mode 100755
index 0000000..3f0c439
--- /dev/null
+++ b/src/com/google/wireless/gdata2/spreadsheets/serializer/xml/XmlListEntryGDataSerializer.java
@@ -0,0 +1,67 @@
+package com.google.wireless.gdata2.spreadsheets.serializer.xml;
+
+import com.google.wireless.gdata2.data.Entry;
+import com.google.wireless.gdata2.parser.ParseException;
+import com.google.wireless.gdata2.parser.xml.XmlParserFactory;
+import com.google.wireless.gdata2.serializer.xml.XmlEntryGDataSerializer;
+import com.google.wireless.gdata2.spreadsheets.data.ListEntry;
+
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.IOException;
+import java.util.Iterator;
+import java.util.Vector;
+
+/**
+ * A serializer for handling GData Spreadsheets List entries.
+ */
+public class XmlListEntryGDataSerializer extends XmlEntryGDataSerializer {
+    /** The prefix to use for the GData Spreadsheets list namespace */
+    public static final String NAMESPACE_GSX = "gsx";
+
+    /** The URI of the GData Spreadsheets list namespace */
+    public static final String NAMESPACE_GSX_URI =
+            "http://schemas.google.com/spreadsheets/2006/extended";
+
+    /**
+     * Creates a new XmlListEntryGDataSerializer.
+     * 
+     * @param entry the entry to be serialized
+     */
+    public XmlListEntryGDataSerializer(XmlParserFactory xmlFactory, Entry entry) {
+        super(xmlFactory, entry);
+    }
+
+    /**
+     * Sets up the GData Spreadsheets list namespace.
+     * 
+     * @param serializer the XML serializer to use
+     * @throws IOException on stream errors.
+     */
+    protected void declareExtraEntryNamespaces(XmlSerializer serializer)
+            throws IOException {
+        serializer.setPrefix(NAMESPACE_GSX, NAMESPACE_GSX_URI);
+    }
+
+    /* (non-JavaDoc)
+     * Handles the non-Atom data belonging to the GData Spreadsheets Cell
+     * namespace.
+     */
+    protected void serializeExtraEntryContents(XmlSerializer serializer,
+            int format) throws ParseException, IOException {
+        ListEntry entry = (ListEntry) getEntry();
+        Vector names = entry.getNames();
+        String name = null;
+        String value = null;
+        Iterator it = names.iterator();
+        while (it.hasNext()) {
+            name = (String) it.next();
+            value = entry.getValue(name);
+            if (value != null) {
+                serializer.startTag(NAMESPACE_GSX_URI, name);
+                serializer.text(value);
+                serializer.endTag(NAMESPACE_GSX_URI, name);
+            }
+        }
+    }
+}
diff --git a/src/com/google/wireless/gdata2/spreadsheets/serializer/xml/package.html b/src/com/google/wireless/gdata2/spreadsheets/serializer/xml/package.html
new file mode 100644
index 0000000..1c9bf9d
--- /dev/null
+++ b/src/com/google/wireless/gdata2/spreadsheets/serializer/xml/package.html
@@ -0,0 +1,5 @@
+<html>
+<body>
+    {@hide}
+</body>
+</html>
diff --git a/src/com/google/wireless/gdata2/subscribedfeeds/client/SubscribedFeedsClient.java b/src/com/google/wireless/gdata2/subscribedfeeds/client/SubscribedFeedsClient.java
new file mode 100644
index 0000000..5b38f40
--- /dev/null
+++ b/src/com/google/wireless/gdata2/subscribedfeeds/client/SubscribedFeedsClient.java
@@ -0,0 +1,36 @@
+// Copyright 2007 The Android Open Source Project
+
+package com.google.wireless.gdata2.subscribedfeeds.client;
+
+import com.google.wireless.gdata2.client.GDataClient;
+import com.google.wireless.gdata2.client.GDataParserFactory;
+import com.google.wireless.gdata2.client.GDataServiceClient;
+
+/**
+ * GDataServiceClient for accessing Subscribed Feeds.  This client can access
+ * subscribed feeds for specific users. The parser this class uses handles
+ * the XML version of feeds.
+ */
+public class SubscribedFeedsClient extends GDataServiceClient {
+
+    /** Service value for contacts. This is only used for downloads; uploads
+     * are done using the service that corresponds to the subscribed feed. */
+    public static final String SERVICE = "mail";
+
+    /**
+     * Create a new SubscribedFeedsClient.
+     * @param client The GDataClient that should be used to authenticate
+     * requests, retrieve feeds, etc.
+     */
+    public SubscribedFeedsClient(GDataClient client, GDataParserFactory factory) {
+        super(client, factory);
+    }
+
+    /*
+     * (non-Javadoc)
+     * @see GDataServiceClient#getServiceName()
+     */
+    public String getServiceName() {
+        return SERVICE;
+    }
+}
diff --git a/src/com/google/wireless/gdata2/subscribedfeeds/client/package.html b/src/com/google/wireless/gdata2/subscribedfeeds/client/package.html
new file mode 100644
index 0000000..1c9bf9d
--- /dev/null
+++ b/src/com/google/wireless/gdata2/subscribedfeeds/client/package.html
@@ -0,0 +1,5 @@
+<html>
+<body>
+    {@hide}
+</body>
+</html>
diff --git a/src/com/google/wireless/gdata2/subscribedfeeds/data/FeedUrl.java b/src/com/google/wireless/gdata2/subscribedfeeds/data/FeedUrl.java
new file mode 100644
index 0000000..523f8ea
--- /dev/null
+++ b/src/com/google/wireless/gdata2/subscribedfeeds/data/FeedUrl.java
@@ -0,0 +1,72 @@
+/*
+** Copyright 2006, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+**     http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** See the License for the specific language governing permissions and
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** limitations under the License.
+*/
+
+package com.google.wireless.gdata2.subscribedfeeds.data;
+
+/**
+ * The FeedUrl GData type.
+ */
+public class FeedUrl {
+    private String feed;
+    private String service;
+    private String authToken;
+
+    public FeedUrl() {
+    }
+
+    public FeedUrl(String feed, String service, String authToken) {
+        setFeed(feed);
+        setService(service);
+        setAuthToken(authToken);
+    }
+
+    public String getFeed() {
+        return feed;
+    }
+
+    public void setFeed(String feed) {
+        this.feed = feed;
+    }
+
+    public String getService() {
+        return service;
+    }
+
+    public void setService(String service) {
+        this.service = service;
+    }
+
+    public String getAuthToken() {
+        return authToken;
+    }
+
+    public void setAuthToken(String authToken) {
+        this.authToken = authToken;
+    }
+
+    public void toString(StringBuffer sb) {
+        sb.append("FeedUrl");
+        sb.append(" url:").append(getFeed());
+        sb.append(" service:").append(getService());
+        sb.append(" authToken:").append(getAuthToken());
+    }
+
+    public String toString() {
+        StringBuffer sb = new StringBuffer();
+        toString(sb);
+        return sb.toString();
+    }    
+}
diff --git a/src/com/google/wireless/gdata2/subscribedfeeds/data/SubscribedFeedsEntry.java b/src/com/google/wireless/gdata2/subscribedfeeds/data/SubscribedFeedsEntry.java
new file mode 100644
index 0000000..319d6fd
--- /dev/null
+++ b/src/com/google/wireless/gdata2/subscribedfeeds/data/SubscribedFeedsEntry.java
@@ -0,0 +1,53 @@
+// Copyright 2007 The Android Open Source Project
+package com.google.wireless.gdata2.subscribedfeeds.data;
+
+import com.google.wireless.gdata2.data.Entry;
+
+/**
+ * Entry containing information about a contact.
+ */
+public class SubscribedFeedsEntry extends Entry {
+    private FeedUrl feedUrl;
+    private String routingInfo;
+    private String clientToken;
+
+    public String getClientToken() {
+        return clientToken;
+    }
+
+    public void setClientToken(String clientToken) {
+        this.clientToken = clientToken;
+    }
+
+    public SubscribedFeedsEntry() {
+        super();
+    }
+
+    public FeedUrl getSubscribedFeed() {
+        return feedUrl;
+    }
+
+    public void setSubscribedFeed(FeedUrl feedUrl) {
+        this.feedUrl = feedUrl;
+    }
+
+    public String getRoutingInfo() {
+        return routingInfo;
+    }
+
+    public void setRoutingInfo(String routingInfo) {
+        this.routingInfo = routingInfo;
+    }
+
+    /*
+     * (non-Javadoc)
+     * @see com.google.wireless.gdata2.data.Entry#clear()
+     */
+    public void clear() {
+        super.clear();
+    }
+
+    public void toString(StringBuffer sb) {
+        super.toString(sb);
+    }
+}
diff --git a/src/com/google/wireless/gdata2/subscribedfeeds/data/SubscribedFeedsFeed.java b/src/com/google/wireless/gdata2/subscribedfeeds/data/SubscribedFeedsFeed.java
new file mode 100644
index 0000000..6eb600e
--- /dev/null
+++ b/src/com/google/wireless/gdata2/subscribedfeeds/data/SubscribedFeedsFeed.java
@@ -0,0 +1,15 @@
+// Copyright 2007 The Android Open Source Project
+package com.google.wireless.gdata2.subscribedfeeds.data;
+
+import com.google.wireless.gdata2.data.Feed;
+
+/**
+ * Feed containing contacts.
+ */
+public class SubscribedFeedsFeed extends Feed {
+    /**
+     * Creates a new empty events feed.
+     */
+    public SubscribedFeedsFeed() {
+    }
+}
diff --git a/src/com/google/wireless/gdata2/subscribedfeeds/data/package.html b/src/com/google/wireless/gdata2/subscribedfeeds/data/package.html
new file mode 100644
index 0000000..1c9bf9d
--- /dev/null
+++ b/src/com/google/wireless/gdata2/subscribedfeeds/data/package.html
@@ -0,0 +1,5 @@
+<html>
+<body>
+    {@hide}
+</body>
+</html>
diff --git a/src/com/google/wireless/gdata2/subscribedfeeds/package.html b/src/com/google/wireless/gdata2/subscribedfeeds/package.html
new file mode 100644
index 0000000..1c9bf9d
--- /dev/null
+++ b/src/com/google/wireless/gdata2/subscribedfeeds/package.html
@@ -0,0 +1,5 @@
+<html>
+<body>
+    {@hide}
+</body>
+</html>
diff --git a/src/com/google/wireless/gdata2/subscribedfeeds/parser/package.html b/src/com/google/wireless/gdata2/subscribedfeeds/parser/package.html
new file mode 100644
index 0000000..1c9bf9d
--- /dev/null
+++ b/src/com/google/wireless/gdata2/subscribedfeeds/parser/package.html
@@ -0,0 +1,5 @@
+<html>
+<body>
+    {@hide}
+</body>
+</html>
diff --git a/src/com/google/wireless/gdata2/subscribedfeeds/parser/xml/XmlSubscribedFeedsGDataParser.java b/src/com/google/wireless/gdata2/subscribedfeeds/parser/xml/XmlSubscribedFeedsGDataParser.java
new file mode 100644
index 0000000..5f5cb6d
--- /dev/null
+++ b/src/com/google/wireless/gdata2/subscribedfeeds/parser/xml/XmlSubscribedFeedsGDataParser.java
@@ -0,0 +1,75 @@
+// Copyright 2007 The Android Open Source Project
+package com.google.wireless.gdata2.subscribedfeeds.parser.xml;
+
+import com.google.wireless.gdata2.data.Entry;
+import com.google.wireless.gdata2.data.Feed;
+import com.google.wireless.gdata2.data.XmlUtils;
+import com.google.wireless.gdata2.parser.ParseException;
+import com.google.wireless.gdata2.parser.xml.XmlGDataParser;
+import com.google.wireless.gdata2.subscribedfeeds.data.FeedUrl;
+import com.google.wireless.gdata2.subscribedfeeds.data.SubscribedFeedsEntry;
+import com.google.wireless.gdata2.subscribedfeeds.data.SubscribedFeedsFeed;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * GDataParser for a subscribed feeds feed.
+ */
+public class XmlSubscribedFeedsGDataParser extends XmlGDataParser {
+    /**
+     * Creates a new XmlSubscribedFeedsGDataParser.
+     * @param is The InputStream that should be parsed.
+     * @throws ParseException Thrown if a parser cannot be created.
+     */
+    public XmlSubscribedFeedsGDataParser(InputStream is, XmlPullParser parser)
+            throws ParseException {
+        super(is, parser);
+    }
+
+    /*
+     * (non-Javadoc)
+     * @see com.google.wireless.gdata2.parser.xml.XmlGDataParser#createFeed()
+     */
+    protected Feed createFeed() {
+        return new SubscribedFeedsFeed();
+    }
+
+    /*
+     * (non-Javadoc)
+     * @see com.google.wireless.gdata2.parser.xml.XmlGDataParser#createEntry()
+     */
+    protected Entry createEntry() {
+        return new SubscribedFeedsEntry();
+    }
+
+  protected void handleExtraElementInEntry(Entry entry)
+      throws XmlPullParserException, IOException {
+        XmlPullParser parser = getParser();
+
+        if (!(entry instanceof SubscribedFeedsEntry)) {
+          throw new IllegalArgumentException("Expected SubscribedFeedsEntry!");
+        }
+        SubscribedFeedsEntry subscribedFeedsEntry =
+                (SubscribedFeedsEntry) entry;
+        String name = parser.getName();
+        if ("feedurl".equals(name)) {
+          FeedUrl feedUrl = new FeedUrl();
+          feedUrl.setFeed(parser.getAttributeValue(null  /* ns */, "value"));
+          feedUrl.setService(parser.getAttributeValue(null  /* ns */, "service"));
+          feedUrl.setAuthToken(parser.getAttributeValue(null  /* ns */, "authtoken"));
+          subscribedFeedsEntry.setSubscribedFeed(feedUrl);
+        }
+        if ("routingInfo".equals(name)) {
+            subscribedFeedsEntry.setRoutingInfo(
+                    XmlUtils.extractChildText(parser));
+        }
+        if ("clientToken".equals(name)) {
+            subscribedFeedsEntry.setClientToken(
+                    XmlUtils.extractChildText(parser));
+        }
+    }
+}
diff --git a/src/com/google/wireless/gdata2/subscribedfeeds/parser/xml/XmlSubscribedFeedsGDataParserFactory.java b/src/com/google/wireless/gdata2/subscribedfeeds/parser/xml/XmlSubscribedFeedsGDataParserFactory.java
new file mode 100644
index 0000000..040b41e
--- /dev/null
+++ b/src/com/google/wireless/gdata2/subscribedfeeds/parser/xml/XmlSubscribedFeedsGDataParserFactory.java
@@ -0,0 +1,81 @@
+package com.google.wireless.gdata2.subscribedfeeds.parser.xml;
+
+import com.google.wireless.gdata2.client.GDataParserFactory;
+import com.google.wireless.gdata2.data.Entry;
+import com.google.wireless.gdata2.parser.GDataParser;
+import com.google.wireless.gdata2.parser.ParseException;
+import com.google.wireless.gdata2.parser.xml.XmlParserFactory;
+import com.google.wireless.gdata2.serializer.GDataSerializer;
+import com.google.wireless.gdata2.subscribedfeeds.data.SubscribedFeedsEntry;
+import com.google.wireless.gdata2.subscribedfeeds.serializer.xml.XmlSubscribedFeedsEntryGDataSerializer;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.InputStream;
+
+/**
+ * GDataParserFactory that creates XML GDataParsers and GDataSerializers for
+ * Subscribed Feeds.
+ */
+public class XmlSubscribedFeedsGDataParserFactory implements
+        GDataParserFactory {
+    private final XmlParserFactory xmlFactory;
+
+    public XmlSubscribedFeedsGDataParserFactory(XmlParserFactory xmlFactory) {
+        this.xmlFactory = xmlFactory;
+    }
+
+    /*
+     * (non-javadoc)
+     * 
+     * @see GDataParserFactory#createParser
+     */
+    public GDataParser createParser(InputStream is) throws ParseException {
+        XmlPullParser xmlParser;
+        try {
+            xmlParser = xmlFactory.createParser();
+        } catch (XmlPullParserException xppe) {
+            throw new ParseException("Could not create XmlPullParser", xppe);
+        }
+        return new XmlSubscribedFeedsGDataParser(is, xmlParser);
+    }
+
+    /*
+     * (non-Javadoc)
+     * 
+     * @see GDataParserFactory#createMetaFeedParser(int, java.io.InputStream)
+     */
+    public GDataParser createParser(Class entryClass, InputStream is)
+            throws ParseException {
+        if (entryClass != SubscribedFeedsEntry.class) {
+            throw new IllegalArgumentException(
+                    "SubscribedFeeds supports only a single feed type");
+        }
+        // we don't have feed sub-types, so just return the default
+        return createParser(is);
+    }
+
+
+    /**
+     * Creates a new {@link GDataSerializer} for the provided entry. The entry
+     * <strong>must</strong> be an instance of {@link SubscribedFeedsEntry}.
+     * 
+     * @param entry The {@link SubscribedFeedsEntry} that should be
+     *        serialized.
+     * @return The {@link GDataSerializer} that will serialize this entry.
+     * @throws IllegalArgumentException Thrown if entry is not a
+     *         {@link SubscribedFeedsEntry}.
+     * @see com.google.wireless.gdata2.client.GDataParserFactory#createSerializer
+     */
+    public GDataSerializer createSerializer(Entry entry) {
+        if (!(entry instanceof SubscribedFeedsEntry)) {
+            throw new IllegalArgumentException(
+                    "Expected SubscribedFeedsEntry!");
+        }
+        SubscribedFeedsEntry subscribedFeedsEntry =
+                (SubscribedFeedsEntry) entry;
+        return new XmlSubscribedFeedsEntryGDataSerializer(xmlFactory,
+                subscribedFeedsEntry);
+    }
+}
diff --git a/src/com/google/wireless/gdata2/subscribedfeeds/parser/xml/package.html b/src/com/google/wireless/gdata2/subscribedfeeds/parser/xml/package.html
new file mode 100644
index 0000000..1c9bf9d
--- /dev/null
+++ b/src/com/google/wireless/gdata2/subscribedfeeds/parser/xml/package.html
@@ -0,0 +1,5 @@
+<html>
+<body>
+    {@hide}
+</body>
+</html>
diff --git a/src/com/google/wireless/gdata2/subscribedfeeds/serializer/package.html b/src/com/google/wireless/gdata2/subscribedfeeds/serializer/package.html
new file mode 100644
index 0000000..1c9bf9d
--- /dev/null
+++ b/src/com/google/wireless/gdata2/subscribedfeeds/serializer/package.html
@@ -0,0 +1,5 @@
+<html>
+<body>
+    {@hide}
+</body>
+</html>
diff --git a/src/com/google/wireless/gdata2/subscribedfeeds/serializer/xml/XmlSubscribedFeedsEntryGDataSerializer.java b/src/com/google/wireless/gdata2/subscribedfeeds/serializer/xml/XmlSubscribedFeedsEntryGDataSerializer.java
new file mode 100644
index 0000000..ed018b2
--- /dev/null
+++ b/src/com/google/wireless/gdata2/subscribedfeeds/serializer/xml/XmlSubscribedFeedsEntryGDataSerializer.java
@@ -0,0 +1,80 @@
+package com.google.wireless.gdata2.subscribedfeeds.serializer.xml;
+
+import com.google.wireless.gdata2.data.StringUtils;
+import com.google.wireless.gdata2.serializer.xml.XmlEntryGDataSerializer;
+import com.google.wireless.gdata2.subscribedfeeds.data.FeedUrl;
+import com.google.wireless.gdata2.subscribedfeeds.data.SubscribedFeedsEntry;
+import com.google.wireless.gdata2.parser.xml.XmlParserFactory;
+
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.IOException;
+
+/**
+ *  Serializes the SubscribedFeedEntry into the Atom XML format.
+ */
+public class XmlSubscribedFeedsEntryGDataSerializer extends
+        XmlEntryGDataSerializer {
+    public static final String NAMESPACE_GSYNC = "gsync";
+    public static final String NAMESPACE_GSYNC_URI =
+        "http://schemas.google.com/gsync/data";
+
+    public XmlSubscribedFeedsEntryGDataSerializer(XmlParserFactory factory,
+                                                  SubscribedFeedsEntry entry) {
+        super(factory, entry);
+    }
+
+    protected SubscribedFeedsEntry getSubscribedFeedsEntry() {
+        return (SubscribedFeedsEntry) getEntry();
+    }
+
+    protected void declareExtraEntryNamespaces(XmlSerializer serializer)
+        throws IOException {
+        serializer.setPrefix(NAMESPACE_GSYNC, NAMESPACE_GSYNC_URI);
+    }
+
+    /* (non-Javadoc)
+     * @see XmlEntryGDataSerializer#serializeExtraEntryContents
+     */
+    protected void serializeExtraEntryContents(XmlSerializer serializer,
+                                               int format)
+        throws IOException {
+        SubscribedFeedsEntry entry = getSubscribedFeedsEntry();
+
+        serializeFeedUrl(serializer,  entry.getSubscribedFeed());
+        serializeClientToken(serializer, entry.getClientToken());
+        serializeRoutingInfo(serializer, entry.getRoutingInfo());
+    }
+
+    private static void serializeFeedUrl(XmlSerializer serializer,
+            FeedUrl feedUrl)
+            throws IOException {
+        serializer.startTag(NAMESPACE_GSYNC_URI, "feedurl");
+        serializer.attribute(null /* ns */, "value", feedUrl.getFeed());
+        serializer.attribute(null /* ns */, "service", feedUrl.getService());
+        serializer.attribute(null /* ns */, "authtoken", feedUrl.getAuthToken());
+        serializer.endTag(NAMESPACE_GSYNC_URI, "feedurl");
+    }
+
+    private static void serializeClientToken(XmlSerializer serializer,
+            String clientToken)
+            throws IOException {
+        if (StringUtils.isEmpty(clientToken)) {
+            clientToken = "";
+        }
+        serializer.startTag(NAMESPACE_GSYNC_URI, "clientToken");
+        serializer.text(clientToken);
+        serializer.endTag(NAMESPACE_GSYNC_URI, "clientToken");
+    }
+
+    private static void serializeRoutingInfo(XmlSerializer serializer,
+            String routingInfo)
+            throws IOException {
+        if (StringUtils.isEmpty(routingInfo)) {
+            routingInfo = "";
+        }
+        serializer.startTag(NAMESPACE_GSYNC_URI, "routingInfo");
+        serializer.text(routingInfo);
+        serializer.endTag(NAMESPACE_GSYNC_URI, "routingInfo");
+    }
+}
diff --git a/src/com/google/wireless/gdata2/subscribedfeeds/serializer/xml/package.html b/src/com/google/wireless/gdata2/subscribedfeeds/serializer/xml/package.html
new file mode 100644
index 0000000..1c9bf9d
--- /dev/null
+++ b/src/com/google/wireless/gdata2/subscribedfeeds/serializer/xml/package.html
@@ -0,0 +1,5 @@
+<html>
+<body>
+    {@hide}
+</body>
+</html>