am 9449b8be: Make name of Calendar Provider localizable.

Merge commit '9449b8be778a2f53361adc81ac38141096c8857c'

* commit '9449b8be778a2f53361adc81ac38141096c8857c':
  Make name of Calendar Provider localizable.
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 438a17e..50e9b97 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -39,6 +39,13 @@
                 android:syncable="true" android:multiprocess="false"
                 android:readPermission="android.permission.READ_CALENDAR"
                 android:writePermission="android.permission.WRITE_CALENDAR" />
+        <service android:name="CalendarSyncAdapterService" android:exported="true">
+            <intent-filter>
+                <action android:name="android.content.SyncAdapter" />
+            </intent-filter>
+            <meta-data android:name="android.content.SyncAdapter"
+                       android:resource="@xml/syncadapter" />
+        </service>
         <activity android:name="CalendarContentProviderTests" android:label="Calendar Content Provider">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
diff --git a/res/xml/syncadapter.xml b/res/xml/syncadapter.xml
new file mode 100644
index 0000000..b5f89cc
--- /dev/null
+++ b/res/xml/syncadapter.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/**
+ * Copyright (c) 2009, 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.
+ */
+-->
+
+<!-- The attributes in this XML file provide configuration information -->
+<!-- for the SyncAdapter. -->
+
+<sync-adapter xmlns:android="http://schemas.android.com/apk/res/android"
+    android:contentAuthority="calendar"
+    android:accountType="com.google.GAIA"
+/>
diff --git a/src/com/android/providers/calendar/CalendarProvider.java b/src/com/android/providers/calendar/CalendarProvider.java
index 59cc896..623757f 100644
--- a/src/com/android/providers/calendar/CalendarProvider.java
+++ b/src/com/android/providers/calendar/CalendarProvider.java
@@ -17,22 +17,6 @@
 
 package com.android.providers.calendar;
 
-import com.google.android.collect.Sets;
-import com.google.android.gdata.client.AndroidGDataClient;
-import com.google.android.gdata.client.AndroidXmlParserFactory;
-import com.google.android.googlelogin.GoogleLoginServiceBlockingHelper;
-import com.google.android.googlelogin.GoogleLoginServiceNotFoundException;
-import com.google.android.providers.AbstractGDataSyncAdapter;
-import com.google.android.providers.AbstractGDataSyncAdapter.GDataSyncData;
-import com.google.wireless.gdata.calendar.client.CalendarClient;
-import com.google.wireless.gdata.calendar.data.CalendarEntry;
-import com.google.wireless.gdata.calendar.data.CalendarsFeed;
-import com.google.wireless.gdata.calendar.parser.xml.XmlCalendarGDataParserFactory;
-import com.google.wireless.gdata.client.HttpException;
-import com.google.wireless.gdata.data.Entry;
-import com.google.wireless.gdata.parser.GDataParser;
-import com.google.wireless.gdata.parser.ParseException;
-
 import android.app.AlarmManager;
 import android.app.PendingIntent;
 import android.content.AbstractSyncableContentProvider;
@@ -45,7 +29,6 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
-import android.content.SyncAdapter;
 import android.content.SyncContext;
 import android.content.UriMatcher;
 import android.database.Cursor;
@@ -53,7 +36,6 @@
 import android.database.SQLException;
 import android.database.sqlite.SQLiteDatabase;
 import android.database.sqlite.SQLiteQueryBuilder;
-import android.graphics.Color;
 import android.net.Uri;
 import android.os.Bundle;
 import android.os.Debug;
@@ -69,34 +51,41 @@
 import android.provider.Calendar.ExtendedProperties;
 import android.provider.Calendar.Instances;
 import android.provider.Calendar.Reminders;
-import android.provider.SyncConstValue;
 import android.text.TextUtils;
 import android.text.format.Time;
 import android.util.Config;
 import android.util.Log;
 import android.util.TimeFormatException;
+import android.accounts.Account;
+import com.google.android.collect.Sets;
+import com.google.android.collect.Maps;
+import com.google.android.gdata.client.AndroidGDataClient;
+import com.google.android.gdata.client.AndroidXmlParserFactory;
+import com.google.android.providers.AbstractGDataSyncAdapter;
+import com.google.android.providers.AbstractGDataSyncAdapter.GDataSyncData;
+import com.google.wireless.gdata.calendar.client.CalendarClient;
+import com.google.wireless.gdata.calendar.parser.xml.XmlCalendarGDataParserFactory;
 
-import java.io.IOException;
 import java.io.UnsupportedEncodingException;
 import java.net.URLDecoder;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
-import java.util.HashSet;
 import java.util.Map;
 import java.util.Set;
 import java.util.TimeZone;
 
 public class CalendarProvider extends AbstractSyncableContentProvider {
-
     private static final boolean PROFILE = false;
     private static final boolean MULTIPLE_ATTENDEES_PER_EVENT = false;
-    private static final String[] ACCOUNTS_PROJECTION = new String[] { Calendars._SYNC_ACCOUNT};
+    private static final String[] ACCOUNTS_PROJECTION =
+            new String[] {Calendars._SYNC_ACCOUNT, Calendars._SYNC_ACCOUNT_TYPE};
 
     private static final String[] EVENTS_PROJECTION = new String[] {
         Events._SYNC_ID,
         Events._SYNC_VERSION,
         Events._SYNC_ACCOUNT,
+        Events._SYNC_ACCOUNT_TYPE,
         Events.CALENDAR_ID,
         Events.RRULE,
         Events.RDATE,
@@ -104,11 +93,12 @@
         };
     private static final int EVENTS_SYNC_ID_INDEX = 0;
     private static final int EVENTS_SYNC_VERSION_INDEX = 1;
-    private static final int EVENTS_SYNC_ACCOUNT_INDEX = 2;
-    private static final int EVENTS_CALENDAR_ID_INDEX = 3;
-    private static final int EVENTS_RRULE_INDEX = 4;
-    private static final int EVENTS_RDATE_INDEX = 5;
-    private static final int EVENTS_ORIGINAL_EVENT_INDEX = 6;
+    private static final int EVENTS_SYNC_ACCOUNT_NAME_INDEX = 2;
+    private static final int EVENTS_SYNC_ACCOUNT_TYPE_INDEX = 3;
+    private static final int EVENTS_CALENDAR_ID_INDEX = 4;
+    private static final int EVENTS_RRULE_INDEX = 5;
+    private static final int EVENTS_RDATE_INDEX = 6;
+    private static final int EVENTS_ORIGINAL_EVENT_INDEX = 7;
 
     private DatabaseUtils.InsertHelper mCalendarsInserter;
     private DatabaseUtils.InsertHelper mEventsInserter;
@@ -235,13 +225,7 @@
 
     // Note: if you update the version number, you must also update the code
     // in upgradeDatabase() to modify the database (gracefully, if possible).
-    private static final int DATABASE_VERSION = 54;
-
-    private static final String EXPECTED_PROJECTION = "/full";
-
-    private static final String DESIRED_PROJECTION = "/full-selfattendance";
-
-    private static final String FEEDS_SUBSTRING = "/feeds/";
+    private static final int DATABASE_VERSION = 55;
 
     // Make sure we load at least two months worth of data.
     // Client apps can load more data in a background thread.
@@ -275,7 +259,6 @@
 
     private AlarmManager mAlarmManager;
 
-    private CalendarSyncAdapter mSyncAdapter;
     private CalendarAppWidgetProvider mAppWidgetProvider = CalendarAppWidgetProvider.getInstance();
 
     /**
@@ -309,6 +292,8 @@
     public boolean onCreate() {
         super.onCreate();
 
+        setTempProviderSyncAdapter(new CalendarSyncAdapter(getContext(), this));
+
         // Register for Intent broadcasts
         IntentFilter filter = new IntentFilter();
 
@@ -566,6 +551,27 @@
             oldVersion += 1;
         }
 
+        if (oldVersion == 54) {
+            db.execSQL("ALTER TABLE Calendars ADD COLUMN _sync_account_type TEXT;");
+            db.execSQL("ALTER TABLE Events ADD COLUMN _sync_account_type TEXT;");
+            db.execSQL("ALTER TABLE DeletedEvents ADD COLUMN _sync_account_type TEXT;");
+            db.execSQL("UPDATE Calendars"
+                    + " SET _sync_account_type='com.google.GAIA'"
+                    + " WHERE _sync_account IS NOT NULL");
+            db.execSQL("UPDATE Events"
+                    + " SET _sync_account_type='com.google.GAIA'"
+                    + " WHERE _sync_account IS NOT NULL");
+            db.execSQL("UPDATE DeletedEvents"
+                    + " SET _sync_account_type='com.google.GAIA'"
+                    + " WHERE _sync_account IS NOT NULL");
+            Log.w(TAG, "re-creating eventSyncAccountAndIdIndex");
+            db.execSQL("DROP INDEX eventSyncAccountAndIdIndex");
+            db.execSQL("CREATE INDEX eventSyncAccountAndIdIndex ON Events ("
+                    + Events._SYNC_ACCOUNT_TYPE + ", " + Events._SYNC_ACCOUNT + ", "
+                    + Events._SYNC_ID + ");");
+            oldVersion += 1;
+        }
+
         return true; // this was lossless
     }
 
@@ -589,6 +595,7 @@
         db.execSQL("CREATE TABLE Calendars (" +
                         "_id INTEGER PRIMARY KEY," +
                         "_sync_account TEXT," +
+                        "_sync_account_type TEXT," +
                         "_sync_id TEXT," +
                         "_sync_version TEXT," +
                         "_sync_time TEXT," +            // UTC
@@ -618,6 +625,7 @@
         db.execSQL("CREATE TABLE Events (" +
                         "_id INTEGER PRIMARY KEY," +
                         "_sync_account TEXT," +
+                        "_sync_account_type TEXT," +
                         "_sync_id TEXT," +
                         "_sync_version TEXT," +
                         "_sync_time TEXT," +            // UTC
@@ -652,7 +660,8 @@
                     ");");
 
         db.execSQL("CREATE INDEX eventSyncAccountAndIdIndex ON Events ("
-                + Events._SYNC_ACCOUNT + ", " + Events._SYNC_ID + ");");
+                + Events._SYNC_ACCOUNT_TYPE + ", " + Events._SYNC_ACCOUNT + ", "
+                + Events._SYNC_ID + ");");
 
         db.execSQL("CREATE INDEX eventsCalendarIdIndex ON Events (" +
                    Events.CALENDAR_ID +
@@ -675,6 +684,7 @@
                         "_sync_id TEXT," +
                         "_sync_version TEXT," +
                         "_sync_account TEXT," +
+                        "_sync_account_type TEXT," +
                         (isTemporary() ? "_sync_local_id INTEGER," : "") + // Used while syncing,
                         "_sync_mark INTEGER," + // To filter out new rows
                         "calendar_id INTEGER" +
@@ -831,18 +841,17 @@
      * syncable.
      */
     @Override
-    protected void onAccountsChanged(String[] accountsArray) {
+    protected void onAccountsChanged(Account[] accountsArray) {
         super.onAccountsChanged(accountsArray);
 
-        Map<String, Boolean> accounts = new HashMap<String, Boolean>();
-        for (String account : accountsArray) {
+        Map<Account, Boolean> accounts = Maps.newHashMap();
+        for (Account account : accountsArray) {
             accounts.put(account, false);
         }
 
         mDb.beginTransaction();
         try {
-            deleteRowsForRemovedAccounts(accounts, "Calendars",
-                    SyncConstValue._SYNC_ACCOUNT);
+            deleteRowsForRemovedAccounts(accounts, "Calendars");
             mDb.setTransactionSuccessful();
         } finally {
             mDb.endTransaction();
@@ -856,28 +865,28 @@
         // If there are no calendars at all for a given account, add the
         // default calendar.
 
-        for (Map.Entry<String, Boolean> entry : accounts.entrySet()) {
+        for (Map.Entry<Account, Boolean> entry : accounts.entrySet()) {
             entry.setValue(false);
             // TODO: remove this break when Calendar supports multiple accounts. Until then
             // pretend that only the first account exists.
             break;
         }
 
-        Set<String> handledAccounts = Sets.newHashSet();
-        ContentResolver cr = getContext().getContentResolver();
+        Set<Account> handledAccounts = Sets.newHashSet();
         if (Config.LOGV) Log.v(TAG, "querying calendars");
-        Cursor c = cr.query(Calendars.CONTENT_URI, ACCOUNTS_PROJECTION, null, null, null);
+        Cursor c = queryInternal(Calendars.CONTENT_URI, ACCOUNTS_PROJECTION, null, null, null);
         try {
             while (c.moveToNext()) {
-                String account = c.getString(0);
+                final String accountName = c.getString(0);
+                final String accountType = c.getString(1);
+                final Account account = new Account(accountName, accountType);
                 if (handledAccounts.contains(account)) {
                     continue;
                 }
                 handledAccounts.add(account);
                 if (accounts.containsKey(account)) {
                     if (Config.LOGV) {
-                        Log.v(TAG, "calendars for account " + account
-                                + " exist");
+                        Log.v(TAG, "calendars for account " + account + " exist");
                     }
                     accounts.put(account, true /* hasCalendar */);
                 }
@@ -890,8 +899,8 @@
         if (Config.LOGV) {
             Log.v(TAG, "scanning over " + accounts.size() + " account(s)");
         }
-        for (Map.Entry<String, Boolean> entry : accounts.entrySet()) {
-            String account = entry.getKey();
+        for (Map.Entry<Account, Boolean> entry : accounts.entrySet()) {
+            final Account account = entry.getKey();
             boolean hasCalendar = entry.getValue();
             if (hasCalendar) {
                 if (Config.LOGV) {
@@ -900,14 +909,15 @@
                 }
                 continue;
             }
-            String feedUrl = mCalendarClient.getDefaultCalendarUrl(account,
+            String feedUrl = mCalendarClient.getDefaultCalendarUrl(account.mName,
                     CalendarClient.PROJECTION_PRIVATE_SELF_ATTENDANCE, null/* query params */);
             feedUrl = CalendarSyncAdapter.rewriteUrlforAccount(account, feedUrl);
             if (Config.LOGV) {
                 Log.v(TAG, "adding default calendar for account " + account);
             }
             ContentValues values = new ContentValues();
-            values.put(Calendars._SYNC_ACCOUNT, account);
+            values.put(Calendars._SYNC_ACCOUNT, account.mName);
+            values.put(Calendars._SYNC_ACCOUNT_TYPE, account.mType);
             values.put(Calendars.URL, feedUrl);
             values.put(Calendars.DISPLAY_NAME, "Default");
             values.put(Calendars.SYNC_EVENTS, 1);
@@ -918,7 +928,7 @@
             // when the user does a sync.
             values.put(Calendars.TIMEZONE, Time.getCurrentTimezone());
             values.put(Calendars.ACCESS_LEVEL, Calendars.OWNER_ACCESS);
-            cr.insert(Calendars.CONTENT_URI, values);
+            insertInternal(Calendars.CONTENT_URI, values);
 
             scheduleSync(account, false /* do a full sync */, null /* no url */);
         }
@@ -957,14 +967,6 @@
                 }
             case CALENDARS:
                 qb.setTables("Calendars");
-                // see if we want to update the list of calendars from the
-                // server.
-                String update = null;
-                update = url.getQueryParameter("update");
-                if ("1".equals(update)) {
-                    fetchCalendarsFromServer();
-                }
-
                 break;
             case CALENDARS_ID:
                 qb.setTables("Calendars");
@@ -1075,279 +1077,6 @@
         return ret;
     }
 
-    private void fetchCalendarsFromServer() {
-        if (mCalendarClient == null) {
-            Log.w(TAG, "Cannot fetch calendars -- calendar url defined.");
-            return;
-        }
-
-        GoogleLoginServiceBlockingHelper loginHelper = null;
-        String username = null;
-        String authToken = null;
-
-        try {
-            loginHelper = new GoogleLoginServiceBlockingHelper(getContext());
-
-            // TODO: allow caller to specify which account's feeds should be updated
-            username = loginHelper.getAccount(false);
-            if (TextUtils.isEmpty(username)) {
-                Log.w(TAG, "Unable to update calendars from server -- "
-                      + "no users configured.");
-                return;
-            }
-
-            try {
-                authToken = loginHelper.getAuthToken(username,
-                                                     mCalendarClient.getServiceName());
-            } catch (GoogleLoginServiceBlockingHelper.AuthenticationException e) {
-                Log.w(TAG, "Unable to update calendars from server -- could not "
-                      + "authenticate user " + username, e);
-                return;
-            }
-        } catch (GoogleLoginServiceNotFoundException e) {
-            Log.e(TAG, "Could not find Google login service", e);
-            return;
-        } finally {
-            if (loginHelper != null) {
-                loginHelper.close();
-            }
-        }
-
-        // get the current set of calendars.  we'll need to pay attention to
-        // which calendars we get back from the server, so we can delete
-        // calendars that have been deleted from the server.
-        Set<Long> existingCalendarIds = new HashSet<Long>();
-
-        final SQLiteDatabase db = getDatabase();
-        db.beginTransaction();
-        try {
-            getCurrentCalendars(existingCalendarIds);
-
-            // get and process the calendars meta feed
-            GDataParser parser = null;
-            try {
-                String feedUrl = mCalendarClient.getUserCalendarsUrl(username);
-                feedUrl = CalendarSyncAdapter.rewriteUrlforAccount(username, feedUrl);
-                parser = mCalendarClient.getParserForUserCalendars(feedUrl, authToken);
-                // process the calendars
-                processCalendars(username, parser, existingCalendarIds);
-            } catch (ParseException pe) {
-                Log.w(TAG, "Unable to process calendars from server -- could not "
-                        + "parse calendar feed.", pe);
-                return;
-            } catch (IOException ioe) {
-                Log.w(TAG, "Unable to process calendars from server -- encountered "
-                        + "i/o error", ioe);
-                return;
-            } catch (HttpException e) {
-                switch (e.getStatusCode()) {
-                    case HttpException.SC_UNAUTHORIZED:
-                        Log.w(TAG, "Unable to process calendars from server -- could not "
-                                + "authenticate user.", e);
-                        return;
-                    case HttpException.SC_GONE:
-                        Log.w(TAG, "Unable to process calendars from server -- encountered "
-                                + "an AllDeletedUnavailableException, this should never happen", e);
-                        return;
-                    default:
-                        Log.w(TAG, "Unable to process calendars from server -- error", e);
-                        return;
-                }
-            } finally {
-                if (parser != null) {
-                    parser.close();
-                }
-            }
-
-            // delete calendars that are no longer sent from the server.
-            final Uri calendarContentUri = Calendars.CONTENT_URI;
-            for (long calId : existingCalendarIds) {
-                // NOTE: triggers delete all events, instances for this calendar.
-                delete(ContentUris.withAppendedId(calendarContentUri, calId),
-                        null /* where */, null /* selectionArgs */);
-            }
-            db.setTransactionSuccessful();
-        } finally {
-            db.endTransaction();
-        }
-    }
-
-    private void getCurrentCalendars(Set<Long> calendarIds) {
-        Cursor cursor = query(Calendars.CONTENT_URI,
-                new String[] { Calendars._ID },
-                null /* selection */,
-                null /* selectionArgs */,
-                null /* sort */);
-        if (cursor != null) {
-            try {
-                while (cursor.moveToNext()) {
-                    calendarIds.add(cursor.getLong(0));
-                }
-            } finally {
-                cursor.close();
-            }
-        }
-    }
-
-    private void processCalendars(String username,
-                                  GDataParser parser,
-                                  Set<Long> existingCalendarIds)
-            throws ParseException, IOException {
-        CalendarsFeed feed = (CalendarsFeed) parser.init();
-        Entry entry = null;
-        ContentValues map = new ContentValues();
-        final Uri calendarContentUri = Calendars.CONTENT_URI;
-        while (parser.hasMoreData()) {
-            entry = parser.readNextEntry(entry);
-            if (Config.LOGV) Log.v(TAG, "Read entry: " + entry.toString());
-            CalendarEntry calendarEntry = (CalendarEntry) entry;
-            String feedUrl = calendarEntryToContentValues(username, feed, calendarEntry, map);
-            if (TextUtils.isEmpty(feedUrl)) {
-                continue;
-            }
-            long calId = -1;
-
-            Cursor c = query(calendarContentUri,
-                    new String[] { Calendars._ID },
-                    Calendars.URL + "='"
-                            + feedUrl + '\'' /* selection */,
-                    null /* selectionArgs */,
-                    null /* sort */);
-            if (c != null) {
-                try {
-                    if (c.moveToFirst()) {
-                        calId = c.getLong(0);
-                        existingCalendarIds.remove(calId);
-                    }
-                } finally {
-                    c.close();
-                }
-            }
-
-            if (calId != -1) {
-                if (Config.LOGV) Log.v(TAG, "Updating calendar " + map);
-                // don't override the existing "selected" or "hidden" settings.
-                map.remove(Calendars.SELECTED);
-                map.remove(Calendars.HIDDEN);
-                // write to db directly, so we don't send a notification.
-                updateInternal(ContentUris.withAppendedId(calendarContentUri, calId), map,
-                       null /* where */, null /* selectionArgs */);
-            } else {
-                // Select this calendar for syncing and display if it is
-                // selected and not hidden.
-                int syncAndDisplay = 0;
-                if (calendarEntry.isSelected() && !calendarEntry.isHidden()) {
-                    syncAndDisplay = 1;
-                }
-                map.put(Calendars.SYNC_EVENTS, syncAndDisplay);
-                map.put(Calendars.SELECTED, syncAndDisplay);
-                map.put(Calendars.HIDDEN, 0);
-                map.put(Calendars._SYNC_ACCOUNT, username);
-                if (Config.LOGV) Log.v(TAG, "Adding calendar " + map);
-                // write to db directly, so we don't send a notification.
-                Uri row = insertInternal(calendarContentUri, map);
-            }
-        }
-    }
-
-    // TODO: unit test.
-    protected static final String convertCalendarIdToFeedUrl(String url) {
-        // id: http://www.google.com/calendar/feeds/<username>/<cal id>
-        // desired feed:
-        //   http://www.google.com/calendar/feeds/<cal id>/<projection>
-        int start = url.indexOf(FEEDS_SUBSTRING);
-        if (start != -1) {
-            // strip out the */ in /feeds/*/
-            start += FEEDS_SUBSTRING.length();
-            int end = url.indexOf('/', start);
-            if (end != -1) {
-                url = url.replace(url.substring(start, end + 1), "");
-            }
-            url = url + "/private" + DESIRED_PROJECTION;
-        }
-        return url;
-    }
-
-    /**
-     * Convert the CalenderEntry to a Bundle that can be inserted/updated into the
-     * Calendars table.
-     */
-    private String calendarEntryToContentValues(String account, CalendarsFeed feed,
-            CalendarEntry entry,
-            ContentValues map) {
-        map.clear();
-
-        String url = entry.getAlternateLink();
-
-        // we only want to fetch the full-selfattendance calendar feeds
-        if (!TextUtils.isEmpty(url)) {
-            if (url.endsWith(EXPECTED_PROJECTION)) {
-                url = url.replace(EXPECTED_PROJECTION, DESIRED_PROJECTION);
-            }
-        } else {
-            // yuck.  the alternate link was not available.  we should
-            // reconstruct from the id.
-            url = entry.getId();
-            if (!TextUtils.isEmpty(url)) {
-                url = convertCalendarIdToFeedUrl(url);
-            } else {
-                if (Config.LOGV) {
-                    Log.v(TAG, "Cannot generate url for calendar feed.");
-                }
-                return null;
-            }
-        }
-
-        url = CalendarSyncAdapter.rewriteUrlforAccount(account, url);
-
-        map.put(Calendars.URL, url);
-        map.put(Calendars.NAME, entry.getTitle());
-
-        // TODO:
-        map.put(Calendars.DISPLAY_NAME, entry.getTitle());
-
-        map.put(Calendars.TIMEZONE, entry.getTimezone());
-
-        String colorStr = entry.getColor();
-        if (!TextUtils.isEmpty(colorStr)) {
-            int color = Color.parseColor(colorStr);
-            // Ensure the alpha is set to max
-            color |= 0xff000000;
-            map.put(Calendars.COLOR, color);
-        }
-
-        map.put(Calendars.SELECTED, entry.isSelected() ? 1 : 0);
-
-        map.put(Calendars.HIDDEN, entry.isHidden() ? 1 : 0);
-
-        int accesslevel;
-        switch (entry.getAccessLevel()) {
-        case CalendarEntry.ACCESS_NONE:
-            accesslevel = Calendars.NO_ACCESS;
-            break;
-        case CalendarEntry.ACCESS_READ:
-            accesslevel = Calendars.READ_ACCESS;
-            break;
-        case CalendarEntry.ACCESS_FREEBUSY:
-            accesslevel = Calendars.FREEBUSY_ACCESS;
-            break;
-        case CalendarEntry.ACCESS_EDITOR:
-            accesslevel = Calendars.EDITOR_ACCESS;
-            break;
-        case CalendarEntry.ACCESS_OWNER:
-            accesslevel = Calendars.OWNER_ACCESS;
-            break;
-        default:
-            accesslevel = Calendars.NO_ACCESS;
-        }
-        map.put(Calendars.ACCESS_LEVEL, accesslevel);
-        // TODO: use the update time, when calendar actually supports this.
-        // right now, calendar modifies the update time frequently.
-        map.put(Calendars._SYNC_TIME, System.currentTimeMillis());
-
-        return url;
-    }
-
     /*
      * Fills the Instances table, if necessary, for the given range and then
      * queries the Instances table.
@@ -2472,7 +2201,10 @@
                 if (!isTemporary()) {
                     Integer syncEvents = initialValues.getAsInteger(Calendars.SYNC_EVENTS);
                     if (syncEvents != null && syncEvents == 1) {
-                        String account = initialValues.getAsString(Calendars._SYNC_ACCOUNT);
+                        String accountName = initialValues.getAsString(Calendars._SYNC_ACCOUNT);
+                        String accountType = initialValues.getAsString(
+                                Calendars._SYNC_ACCOUNT_TYPE);
+                        final Account account = new Account(accountName, accountType);
                         String calendarUrl = initialValues.getAsString(Calendars.URL);
                         scheduleSync(account, false /* two-way sync */, calendarUrl);
                     }
@@ -3083,13 +2815,17 @@
                             String syncId = cursor.getString(EVENTS_SYNC_ID_INDEX);
                             if (!TextUtils.isEmpty(syncId)) {
                                 String syncVersion = cursor.getString(EVENTS_SYNC_VERSION_INDEX);
-                                String syncAccount = cursor.getString(EVENTS_SYNC_ACCOUNT_INDEX);
+                                String syncAccountName =
+                                        cursor.getString(EVENTS_SYNC_ACCOUNT_NAME_INDEX);
+                                String syncAccountType =
+                                        cursor.getString(EVENTS_SYNC_ACCOUNT_TYPE_INDEX);
                                 Long calId = cursor.getLong(EVENTS_CALENDAR_ID_INDEX);
 
                                 ContentValues values = new ContentValues();
                                 values.put(Events._SYNC_ID, syncId);
                                 values.put(Events._SYNC_VERSION, syncVersion);
-                                values.put(Events._SYNC_ACCOUNT, syncAccount);
+                                values.put(Events._SYNC_ACCOUNT, syncAccountName);
+                                values.put(Events._SYNC_ACCOUNT_TYPE, syncAccountType);
                                 values.put(Events.CALENDAR_ID, calId);
                                 mDeletedEventsInserter.insert(values);
 
@@ -3332,12 +3068,12 @@
      * Schedule a calendar sync for the account.
      * @param account the account for which to schedule a sync
      * @param uploadChangesOnly if set, specify that the sync should only send
-     *   up local changes
+ *   up local changes
      * @param url the url feed for the calendar to sync (may be null)
      */
-    private void scheduleSync(String account, boolean uploadChangesOnly, String url) {
+    private void scheduleSync(Account account, boolean uploadChangesOnly, String url) {
         Bundle extras = new Bundle();
-        extras.putString(ContentResolver.SYNC_EXTRAS_ACCOUNT, account);
+        extras.putParcelable(ContentResolver.SYNC_EXTRAS_ACCOUNT, account);
         extras.putBoolean(ContentResolver.SYNC_EXTRAS_UPLOAD, uploadChangesOnly);
         if (url != null) {
             extras.putString("feed", url);
@@ -3350,28 +3086,29 @@
         // get the account, url, and current selected state
         // for this calendar.
         Cursor cursor = query(ContentUris.withAppendedId(Calendars.CONTENT_URI, id),
-                new String[] { Calendars._SYNC_ACCOUNT,
-                               Calendars.URL,
-                               Calendars.SYNC_EVENTS},
+                new String[] {Calendars._SYNC_ACCOUNT, Calendars._SYNC_ACCOUNT_TYPE,
+                        Calendars.URL, Calendars.SYNC_EVENTS},
                 null /* selection */,
                 null /* selectionArgs */,
                 null /* sort */);
 
-        String account = null;
+        Account account = null;
         String calendarUrl = null;
         boolean oldSyncEvents = false;
         if (cursor != null) {
             try {
                 cursor.moveToFirst();
-                account = cursor.getString(0);
-                calendarUrl = cursor.getString(1);
-                oldSyncEvents = (cursor.getInt(2) != 0);
+                final String accountName = cursor.getString(0);
+                final String accountType = cursor.getString(1);
+                account = new Account(accountName, accountType);
+                calendarUrl = cursor.getString(2);
+                oldSyncEvents = (cursor.getInt(3) != 0);
             } finally {
                 cursor.close();
             }
         }
 
-        if (TextUtils.isEmpty(account) || TextUtils.isEmpty(calendarUrl)) {
+        if (account == null || TextUtils.isEmpty(calendarUrl)) {
             // should not happen?
             Log.w(TAG, "Cannot update subscription because account "
             + "or calendar url empty -- should not happen.");
@@ -3422,14 +3159,6 @@
     }
 
     @Override
-    public synchronized SyncAdapter getSyncAdapter() {
-        if (mSyncAdapter == null) {
-            mSyncAdapter = new CalendarSyncAdapter(getContext(), this);
-        }
-        return mSyncAdapter;
-    }
-
-    @Override
     public void onSyncStop(SyncContext context, boolean success) {
         super.onSyncStop(context, success);
         if (Log.isLoggable(TAG, Log.DEBUG)) {
@@ -4031,6 +3760,8 @@
             DatabaseUtils.cursorStringToContentValues(diffsCursor, Events._SYNC_VERSION, values);
             DatabaseUtils.cursorStringToContentValues(diffsCursor, Events._SYNC_DIRTY, values);
             DatabaseUtils.cursorStringToContentValues(diffsCursor, Events._SYNC_ACCOUNT, values);
+            DatabaseUtils.cursorStringToContentValues(diffsCursor,
+                    Events._SYNC_ACCOUNT_TYPE, values);
             DatabaseUtils.cursorStringToContentValues(diffsCursor, Events.HTML_URI, values);
             DatabaseUtils.cursorStringToContentValues(diffsCursor, Events.TITLE, values);
             DatabaseUtils.cursorStringToContentValues(diffsCursor, Events.EVENT_LOCATION, values);
@@ -4151,6 +3882,8 @@
         sEventsProjectionMap.put(Events._SYNC_LOCAL_ID, "Events._sync_local_id AS _sync_local_id");
         sEventsProjectionMap.put(Events._SYNC_DIRTY, "Events._sync_dirty AS _sync_dirty");
         sEventsProjectionMap.put(Events._SYNC_ACCOUNT, "Events._sync_account AS _sync_account");
+        sEventsProjectionMap.put(Events._SYNC_ACCOUNT_TYPE,
+                "Events._sync_account_type AS _sync_account_type");
 
         // Instances columns
         sInstancesProjectionMap.put(Instances.BEGIN, "begin");
diff --git a/src/com/android/providers/calendar/CalendarSyncAdapter.java b/src/com/android/providers/calendar/CalendarSyncAdapter.java
index 0c53c6a..9c18a28 100644
--- a/src/com/android/providers/calendar/CalendarSyncAdapter.java
+++ b/src/com/android/providers/calendar/CalendarSyncAdapter.java
@@ -17,23 +17,11 @@
 
 package com.android.providers.calendar;
 
-import com.google.android.gdata.client.AndroidGDataClient;
-import com.google.android.gdata.client.AndroidXmlParserFactory;
-import com.google.android.providers.AbstractGDataSyncAdapter;
-import com.google.wireless.gdata.calendar.client.CalendarClient;
-import com.google.wireless.gdata.calendar.data.EventEntry;
-import com.google.wireless.gdata.calendar.data.EventsFeed;
-import com.google.wireless.gdata.calendar.data.Reminder;
-import com.google.wireless.gdata.calendar.data.When;
-import com.google.wireless.gdata.calendar.data.Who;
-import com.google.wireless.gdata.calendar.parser.xml.XmlCalendarGDataParserFactory;
-import com.google.wireless.gdata.client.GDataServiceClient;
-import com.google.wireless.gdata.client.QueryParams;
-import com.google.wireless.gdata.data.Entry;
-import com.google.wireless.gdata.data.Feed;
-import com.google.wireless.gdata.data.StringUtils;
-import com.google.wireless.gdata.parser.ParseException;
-
+import android.accounts.Account;
+import android.accounts.AccountManager;
+import android.accounts.AuthenticatorException;
+import android.accounts.Constants;
+import android.accounts.OperationCanceledException;
 import android.content.ContentProvider;
 import android.content.ContentResolver;
 import android.content.ContentUris;
@@ -44,24 +32,50 @@
 import android.content.SyncableContentProvider;
 import android.database.Cursor;
 import android.database.CursorJoiner;
+import android.graphics.Color;
 import android.net.Uri;
 import android.os.Bundle;
 import android.os.SystemProperties;
 import android.pim.ICalendar;
 import android.pim.RecurrenceSet;
 import android.provider.Calendar;
-import android.provider.SubscribedFeeds;
-import android.provider.SyncConstValue;
 import android.provider.Calendar.Calendars;
 import android.provider.Calendar.Events;
+import android.provider.SubscribedFeeds;
+import android.provider.SyncConstValue;
 import android.text.TextUtils;
 import android.text.format.Time;
 import android.util.Config;
 import android.util.Log;
+import com.google.android.gdata.client.AndroidGDataClient;
+import com.google.android.gdata.client.AndroidXmlParserFactory;
+import com.google.android.googlelogin.GoogleLoginServiceConstants;
+import com.google.android.providers.AbstractGDataSyncAdapter;
+import com.google.wireless.gdata.calendar.client.CalendarClient;
+import com.google.wireless.gdata.calendar.data.CalendarEntry;
+import com.google.wireless.gdata.calendar.data.CalendarsFeed;
+import com.google.wireless.gdata.calendar.data.EventEntry;
+import com.google.wireless.gdata.calendar.data.EventsFeed;
+import com.google.wireless.gdata.calendar.data.Reminder;
+import com.google.wireless.gdata.calendar.data.When;
+import com.google.wireless.gdata.calendar.data.Who;
+import com.google.wireless.gdata.calendar.parser.xml.XmlCalendarGDataParserFactory;
+import com.google.wireless.gdata.client.GDataServiceClient;
+import com.google.wireless.gdata.client.HttpException;
+import com.google.wireless.gdata.client.QueryParams;
+import com.google.wireless.gdata.data.Entry;
+import com.google.wireless.gdata.data.Feed;
+import com.google.wireless.gdata.data.StringUtils;
+import com.google.wireless.gdata.parser.GDataParser;
+import com.google.wireless.gdata.parser.ParseException;
 
+import java.io.IOException;
+import java.util.ArrayList;
 import java.util.Enumeration;
+import java.util.HashSet;
 import java.util.Hashtable;
 import java.util.List;
+import java.util.Set;
 import java.util.TimeZone;
 import java.util.Vector;
 
@@ -74,18 +88,27 @@
 
     /* package */ static final String USER_AGENT_APP_VERSION = "Android-GData-Calendar/1.1";
 
-    private static final String SELECT_BY_ACCOUNT = Calendars._SYNC_ACCOUNT + "=?";
+    private static final String SELECT_BY_ACCOUNT =
+            Calendars._SYNC_ACCOUNT + "=? AND " + Calendars._SYNC_ACCOUNT_TYPE + "=?";
     private static final String SELECT_BY_ACCOUNT_AND_FEED =
             SELECT_BY_ACCOUNT + " AND " + Calendars.URL + "=?";
 
     private static final String[] CALENDAR_KEY_COLUMNS =
-            new String[]{Calendars._SYNC_ACCOUNT, Calendars.URL};
+            new String[]{Calendars._SYNC_ACCOUNT, Calendars._SYNC_ACCOUNT_TYPE, Calendars.URL};
     private static final String CALENDAR_KEY_SORT_ORDER =
-            Calendars._SYNC_ACCOUNT + "," + Calendars.URL;
+            Calendars._SYNC_ACCOUNT + "," + Calendars._SYNC_ACCOUNT_TYPE + "," + Calendars.URL;
     private static final String[] FEEDS_KEY_COLUMNS =
-            new String[]{SubscribedFeeds.Feeds._SYNC_ACCOUNT, SubscribedFeeds.Feeds.FEED};
+            new String[]{SubscribedFeeds.Feeds._SYNC_ACCOUNT,
+                    SubscribedFeeds.Feeds._SYNC_ACCOUNT_TYPE, SubscribedFeeds.Feeds.FEED};
     private static final String FEEDS_KEY_SORT_ORDER =
-            SubscribedFeeds.Feeds._SYNC_ACCOUNT + ", " + SubscribedFeeds.Feeds.FEED;
+            SubscribedFeeds.Feeds._SYNC_ACCOUNT + ", " + SubscribedFeeds.Feeds._SYNC_ACCOUNT_TYPE
+                    + ", " + SubscribedFeeds.Feeds.FEED;
+
+    private static final String EXPECTED_PROJECTION = "/full";
+
+    private static final String DESIRED_PROJECTION = "/full-selfattendance";
+
+    private static final String FEEDS_SUBSTRING = "/feeds/";
 
     public static class SyncInfo {
         // public String feedUrl;
@@ -519,14 +542,17 @@
 
     protected boolean handleAllDeletedUnavailable(GDataSyncData syncData, String feed) {
         syncData.feedData.remove(feed);
+        final Account account = getAccount();
         getContext().getContentResolver().delete(Calendar.Calendars.CONTENT_URI,
-                Calendar.Calendars._SYNC_ACCOUNT + "=? AND " + Calendar.Calendars.URL + "=?",
-                new String[]{getAccount(), feed});
+                Calendar.Calendars._SYNC_ACCOUNT + "=? AND "
+                        + Calendar.Calendars._SYNC_ACCOUNT_TYPE + "=? AND "
+                        + Calendar.Calendars.URL + "=?",
+                new String[]{account.mName, account.mType, feed});
         return true;
     }
 
     @Override
-    public void onSyncStarting(SyncContext context, String account, boolean forced,
+    public void onSyncStarting(SyncContext context, Account account, boolean forced,
             SyncResult result) {
         mContentResolver = getContext().getContentResolver();
         mServerDiffs = 0;
@@ -598,10 +624,11 @@
         // Base sync info
         map.put(Events._SYNC_ID, event.getId());
         String version = event.getEditUri();
+        final Account account = getAccount();
         if (!StringUtils.isEmpty(version)) {
             // Always rewrite the edit URL to https for dasher account to avoid
             // redirection.
-            map.put(Events._SYNC_VERSION, rewriteUrlforAccount(getAccount(), version));
+            map.put(Events._SYNC_VERSION, rewriteUrlforAccount(account, version));
         }
 
         // see if this is an exception to an existing event/recurrence.
@@ -804,7 +831,8 @@
             return ENTRY_INVALID;
         }
 
-        map.put(SyncConstValue._SYNC_ACCOUNT, getAccount());
+        map.put(SyncConstValue._SYNC_ACCOUNT, account.mName);
+        map.put(SyncConstValue._SYNC_ACCOUNT_TYPE, account.mType);
         return ENTRY_OK;
     }
 
@@ -1030,22 +1058,43 @@
         final ContentResolver cr = getContext().getContentResolver();
         mServerDiffs++;
         final boolean syncingSingleFeed = (extras != null) && extras.containsKey("feed");
+        final boolean syncingMetafeedOnly = (extras != null) && extras.containsKey("metafeedonly");
+
         if (syncingSingleFeed) {
+            if (syncingMetafeedOnly) {
+                Log.d(TAG, "metafeedonly and feed both set.");
+                return;
+            }
             String feedUrl = extras.getString("feed");
             getServerDiffsForFeed(context, baseSyncData, tempProvider, feedUrl,
                     baseSyncInfo, syncResult);
             return;
         }
 
+        // At this point, either metafeed sync or poll.
+        // For the poll (or metafeed sync), refresh the list of calendars.
+        // we can move away from this when we move to the new allcalendars feed, which is
+        // syncable.  until then, we'll rely on the daily poll to keep the list of calendars
+        // up to date.
+
+        mRefresh++;
+        context.setStatusText("Fetching list of calendars");
+        fetchCalendarsFromServer();
+
+        if (syncingMetafeedOnly) {
+            // If not polling, nothing more to do.
+            return;
+        }
+
         // select the set of calendars for this account.
+        final Account account = getAccount();
+        final String[] accountSelectionArgs = new String[]{account.mName, account.mType};
         Cursor cursor = cr.query(Calendar.Calendars.CONTENT_URI,
                 CALENDARS_PROJECTION, SELECT_BY_ACCOUNT,
-                new String[] { getAccount() }, null /* sort order */);
+                accountSelectionArgs, null /* sort order */);
 
         Bundle syncExtras = new Bundle();
 
-        boolean refreshCalendars = true;
-
         try {
             while (cursor.moveToNext()) {
                 boolean syncEvents = (cursor.getInt(6) == 1);
@@ -1055,24 +1104,6 @@
                     continue;
                 }
 
-                // since this is a poll (no specific feed selected), refresh the list of calendars.
-                // we can move away from this when we move to the new allcalendars feed, which is
-                // syncable.  until then, we'll rely on the daily poll to keep the list of calendars
-                // up to date.
-                if (refreshCalendars) {
-                    mRefresh++;
-                    context.setStatusText("Fetching list of calendars");
-                    // get rid of the current cursor and fetch from the server.
-                    cursor.close();
-                    final String[] accountSelectionArgs = new String[]{getAccount()};
-                    cursor = cr.query(
-                        Calendar.Calendars.LIVE_CONTENT_URI, CALENDARS_PROJECTION,
-                        SELECT_BY_ACCOUNT, accountSelectionArgs, null /* sort order */);
-                    // start over with the loop
-                    refreshCalendars = false;
-                    continue;
-                }
-
                 // schedule syncs for each of these feeds.
                 syncExtras.clear();
                 syncExtras.putAll(extras);
@@ -1091,9 +1122,10 @@
         final SyncInfo syncInfo = (SyncInfo) baseSyncInfo;
         final GDataSyncData syncData = (GDataSyncData) baseSyncData;
 
+        final Account account = getAccount();
         Cursor cursor = getContext().getContentResolver().query(Calendar.Calendars.CONTENT_URI,
                 CALENDARS_PROJECTION, SELECT_BY_ACCOUNT_AND_FEED,
-                new String[] { getAccount(), feed }, null /* sort order */);
+                new String[] { account.mName, account.mType, feed }, null /* sort order */);
 
         ContentValues map = new ContentValues();
         int maxResults = getMaxEntriesPerSync();
@@ -1154,10 +1186,12 @@
         
         // populate temp provider with calendar ids, so joins work.
         ContentValues map = new ContentValues();
+        final Account account = getAccount();
         Cursor c = getContext().getContentResolver().query(
                 Calendar.Calendars.CONTENT_URI,
                 CALENDARS_PROJECTION,
-                SELECT_BY_ACCOUNT, new String[]{getAccount()}, null /* sort order */);
+                SELECT_BY_ACCOUNT, new String[]{account.mName, account.mType},
+                null /* sort order */);
         final int idIndex = c.getColumnIndexOrThrow(Calendars._ID);
         final int urlIndex = c.getColumnIndexOrThrow(Calendars.URL);
         final int timezoneIndex = c.getColumnIndexOrThrow(Calendars.TIMEZONE);
@@ -1171,7 +1205,7 @@
         c.close();
     }
 
-    public void onAccountsChanged(String[] accountsArray) {
+    public void onAccountsChanged(Account[] accountsArray) {
         if (!"yes".equals(SystemProperties.get("ro.config.sync"))) {
             return;
         }
@@ -1188,12 +1222,17 @@
             cursorA = Calendar.Calendars.query(cr, null /* projection */,
                     Calendar.Calendars.SELECTED + "=1", CALENDAR_KEY_SORT_ORDER);
             int urlIndexA = cursorA.getColumnIndexOrThrow(Calendar.Calendars.URL);
-            int accountIndexA = cursorA.getColumnIndexOrThrow(Calendar.Calendars._SYNC_ACCOUNT);
+            int accountNameIndexA = cursorA.getColumnIndexOrThrow(Calendar.Calendars._SYNC_ACCOUNT);
+            int accountTypeIndexA =
+                    cursorA.getColumnIndexOrThrow(Calendar.Calendars._SYNC_ACCOUNT_TYPE);
             cursorB = SubscribedFeeds.Feeds.query(cr, FEEDS_KEY_COLUMNS,
                     SubscribedFeeds.Feeds.AUTHORITY + "=?", new String[]{Calendar.AUTHORITY},
                     FEEDS_KEY_SORT_ORDER);
             int urlIndexB = cursorB.getColumnIndexOrThrow(SubscribedFeeds.Feeds.FEED);
-            int accountIndexB = cursorB.getColumnIndexOrThrow(SubscribedFeeds.Feeds._SYNC_ACCOUNT);
+            int accountNameIndexB =
+                    cursorB.getColumnIndexOrThrow(SubscribedFeeds.Feeds._SYNC_ACCOUNT);
+            int accountTypeIndexB =
+                    cursorB.getColumnIndexOrThrow(SubscribedFeeds.Feeds._SYNC_ACCOUNT_TYPE);
             for (CursorJoiner.Result joinerResult :
                     new CursorJoiner(cursorA, CALENDAR_KEY_COLUMNS, cursorB, FEEDS_KEY_COLUMNS)) {
                 switch (joinerResult) {
@@ -1201,7 +1240,8 @@
                         SubscribedFeeds.addFeed(
                                 cr,
                                 cursorA.getString(urlIndexA),
-                                cursorA.getString(accountIndexA),
+                                new Account(cursorA.getString(accountNameIndexA),
+                                        cursorA.getString(accountTypeIndexA)),
                                 Calendar.AUTHORITY,
                                 CalendarClient.SERVICE);
                         break;
@@ -1209,7 +1249,8 @@
                         SubscribedFeeds.deleteFeed(
                                 cr,
                                 cursorB.getString(urlIndexB),
-                                cursorB.getString(accountIndexB),
+                                new Account(cursorB.getString(accountNameIndexB),
+                                        cursorB.getString(accountTypeIndexB)),
                                 Calendar.AUTHORITY);
                         break;
                     case BOTH:
@@ -1229,7 +1270,7 @@
      * to/from the device, and thus is determined and passed around as a local variable, where
      * appropriate.
      */
-    protected String getFeedUrl(String account) {
+    protected String getFeedUrl(Account account) {
         throw new UnsupportedOperationException("getFeedUrl() should not get called.");
     }
 
@@ -1279,4 +1320,286 @@
             sb.append("s").append(mServerDiffs);
         }
     }
+
+    private void fetchCalendarsFromServer() {
+        if (mCalendarClient == null) {
+            Log.w(TAG, "Cannot fetch calendars -- calendar url defined.");
+            return;
+        }
+
+        Account account = null;
+        String authToken = null;
+
+
+        try {
+            // TODO: allow caller to specify which account's feeds should be updated
+            Account[] accounts = AccountManager.get(getContext())
+                    .blockingGetAccountsWithTypeAndFeatures(
+                            GoogleLoginServiceConstants.ACCOUNT_TYPE,
+                            new String[]{GoogleLoginServiceConstants.FEATURE_GOOGLE_OR_DASHER});
+            if (accounts.length == 0) {
+                Log.w(TAG, "Unable to update calendars from server -- no users configured.");
+                return;
+            }
+
+            account = accounts[0];
+
+            Bundle bundle = AccountManager.get(getContext()).getAuthToken(
+                    account, mCalendarClient.getServiceName(),
+                    true /* notifyAuthFailure */, null /* callback */, null /* handler */)
+                    .getResult();
+            authToken = bundle.getString(Constants.AUTHTOKEN_KEY);
+            if (authToken == null) {
+                Log.w(TAG, "Unable to update calendars from server -- could not "
+                      + "authenticate user " + account);
+                return;
+            }
+        } catch (IOException e) {
+            Log.w(TAG, "Unable to update calendars from server -- could not "
+                  + "authenticate user " + account, e);
+            return;
+        } catch (AuthenticatorException e) {
+            Log.w(TAG, "Unable to update calendars from server -- could not "
+                  + "authenticate user " + account, e);
+            return;
+        } catch (OperationCanceledException e) {
+            Log.w(TAG, "Unable to update calendars from server -- could not "
+                  + "authenticate user " + account, e);
+            return;
+        }
+
+        // get the current set of calendars.  we'll need to pay attention to
+        // which calendars we get back from the server, so we can delete
+        // calendars that have been deleted from the server.
+        Set<Long> existingCalendarIds = new HashSet<Long>();
+
+        getCurrentCalendars(existingCalendarIds);
+
+        // get and process the calendars meta feed
+        GDataParser parser = null;
+        try {
+            String feedUrl = mCalendarClient.getUserCalendarsUrl(account.mName);
+            feedUrl = CalendarSyncAdapter.rewriteUrlforAccount(account, feedUrl);
+            parser = mCalendarClient.getParserForUserCalendars(feedUrl, authToken);
+            // process the calendars
+            processCalendars(account, parser, existingCalendarIds);
+        } catch (ParseException pe) {
+            Log.w(TAG, "Unable to process calendars from server -- could not "
+                    + "parse calendar feed.", pe);
+            return;
+        } catch (IOException ioe) {
+            Log.w(TAG, "Unable to process calendars from server -- encountered "
+                    + "i/o error", ioe);
+            return;
+        } catch (HttpException e) {
+            switch (e.getStatusCode()) {
+                case HttpException.SC_UNAUTHORIZED:
+                    Log.w(TAG, "Unable to process calendars from server -- could not "
+                            + "authenticate user.", e);
+                    return;
+                case HttpException.SC_GONE:
+                    Log.w(TAG, "Unable to process calendars from server -- encountered "
+                            + "an AllDeletedUnavailableException, this should never happen", e);
+                    return;
+                default:
+                    Log.w(TAG, "Unable to process calendars from server -- error", e);
+                    return;
+            }
+        } finally {
+            if (parser != null) {
+                parser.close();
+            }
+        }
+
+        // delete calendars that are no longer sent from the server.
+        final Uri calendarContentUri = Calendars.CONTENT_URI;
+        final ContentResolver cr = getContext().getContentResolver();
+        for (long calId : existingCalendarIds) {
+            // NOTE: triggers delete all events, instances for this calendar.
+            cr.delete(ContentUris.withAppendedId(calendarContentUri, calId),
+                    null /* where */, null /* selectionArgs */);
+        }
+    }
+
+    private void getCurrentCalendars(Set<Long> calendarIds) {
+        final ContentResolver cr = getContext().getContentResolver();
+        Cursor cursor = cr.query(Calendars.CONTENT_URI,
+                new String[] { Calendars._ID },
+                null /* selection */,
+                null /* selectionArgs */,
+                null /* sort */);
+        if (cursor != null) {
+            try {
+                while (cursor.moveToNext()) {
+                    calendarIds.add(cursor.getLong(0));
+                }
+            } finally {
+                cursor.close();
+            }
+        }
+    }
+
+    private void processCalendars(Account account,
+            GDataParser parser,
+            Set<Long> existingCalendarIds)
+            throws ParseException, IOException {
+        final ContentResolver cr = getContext().getContentResolver();
+        CalendarsFeed feed = (CalendarsFeed) parser.init();
+        Entry entry = null;
+        final Uri calendarContentUri = Calendars.CONTENT_URI;
+        ArrayList<ContentValues> inserts = new ArrayList<ContentValues>();
+        while (parser.hasMoreData()) {
+            entry = parser.readNextEntry(entry);
+            if (Config.LOGV) Log.v(TAG, "Read entry: " + entry.toString());
+            CalendarEntry calendarEntry = (CalendarEntry) entry;
+            ContentValues map = new ContentValues();
+            String feedUrl = calendarEntryToContentValues(account, feed, calendarEntry, map);
+            if (TextUtils.isEmpty(feedUrl)) {
+                continue;
+            }
+            long calId = -1;
+
+            Cursor c = cr.query(calendarContentUri,
+                    new String[] { Calendars._ID },
+                    Calendars.URL + "='"
+                            + feedUrl + '\'' /* selection */,
+                    null /* selectionArgs */,
+                    null /* sort */);
+            if (c != null) {
+                try {
+                    if (c.moveToFirst()) {
+                        calId = c.getLong(0);
+                        existingCalendarIds.remove(calId);
+                    }
+                } finally {
+                    c.close();
+                }
+            }
+
+            if (calId != -1) {
+                if (Config.LOGV) Log.v(TAG, "Updating calendar " + map);
+                // don't override the existing "selected" or "hidden" settings.
+                map.remove(Calendars.SELECTED);
+                map.remove(Calendars.HIDDEN);
+                cr.update(ContentUris.withAppendedId(calendarContentUri, calId), map,
+                        null /* where */, null /* selectionArgs */);
+            } else {
+                // Select this calendar for syncing and display if it is
+                // selected and not hidden.
+                int syncAndDisplay = 0;
+                if (calendarEntry.isSelected() && !calendarEntry.isHidden()) {
+                    syncAndDisplay = 1;
+                }
+                map.put(Calendars.SYNC_EVENTS, syncAndDisplay);
+                map.put(Calendars.SELECTED, syncAndDisplay);
+                map.put(Calendars.HIDDEN, 0);
+                map.put(Calendars._SYNC_ACCOUNT, account.mName);
+                map.put(Calendars._SYNC_ACCOUNT_TYPE, account.mType);
+                if (Config.LOGV) Log.v(TAG, "Adding calendar " + map);
+                inserts.add(map);
+            }
+        }
+        if (!inserts.isEmpty()) {
+            if (Config.LOGV) Log.v(TAG, "Bulk updating calendar list.");
+            cr.bulkInsert(calendarContentUri, inserts.toArray(new ContentValues[inserts.size()]));
+        }
+    }
+
+    /**
+     * Convert the CalenderEntry to a Bundle that can be inserted/updated into the
+     * Calendars table.
+     */
+    private String calendarEntryToContentValues(Account account, CalendarsFeed feed,
+            CalendarEntry entry,
+            ContentValues map) {
+        map.clear();
+
+        String url = entry.getAlternateLink();
+
+        // we only want to fetch the full-selfattendance calendar feeds
+        if (!TextUtils.isEmpty(url)) {
+            if (url.endsWith(EXPECTED_PROJECTION)) {
+                url = url.replace(EXPECTED_PROJECTION, DESIRED_PROJECTION);
+            }
+        } else {
+            // yuck.  the alternate link was not available.  we should
+            // reconstruct from the id.
+            url = entry.getId();
+            if (!TextUtils.isEmpty(url)) {
+                url = convertCalendarIdToFeedUrl(url);
+            } else {
+                if (Config.LOGV) {
+                    Log.v(TAG, "Cannot generate url for calendar feed.");
+                }
+                return null;
+            }
+        }
+
+        url = rewriteUrlforAccount(account, url);
+
+        map.put(Calendars.URL, url);
+        map.put(Calendars.NAME, entry.getTitle());
+
+        // TODO:
+        map.put(Calendars.DISPLAY_NAME, entry.getTitle());
+
+        map.put(Calendars.TIMEZONE, entry.getTimezone());
+
+        String colorStr = entry.getColor();
+        if (!TextUtils.isEmpty(colorStr)) {
+            int color = Color.parseColor(colorStr);
+            // Ensure the alpha is set to max
+            color |= 0xff000000;
+            map.put(Calendars.COLOR, color);
+        }
+
+        map.put(Calendars.SELECTED, entry.isSelected() ? 1 : 0);
+
+        map.put(Calendars.HIDDEN, entry.isHidden() ? 1 : 0);
+
+        int accesslevel;
+        switch (entry.getAccessLevel()) {
+        case CalendarEntry.ACCESS_NONE:
+            accesslevel = Calendars.NO_ACCESS;
+            break;
+        case CalendarEntry.ACCESS_READ:
+            accesslevel = Calendars.READ_ACCESS;
+            break;
+        case CalendarEntry.ACCESS_FREEBUSY:
+            accesslevel = Calendars.FREEBUSY_ACCESS;
+            break;
+        case CalendarEntry.ACCESS_EDITOR:
+            accesslevel = Calendars.EDITOR_ACCESS;
+            break;
+        case CalendarEntry.ACCESS_OWNER:
+            accesslevel = Calendars.OWNER_ACCESS;
+            break;
+        default:
+            accesslevel = Calendars.NO_ACCESS;
+        }
+        map.put(Calendars.ACCESS_LEVEL, accesslevel);
+        // TODO: use the update time, when calendar actually supports this.
+        // right now, calendar modifies the update time frequently.
+        map.put(Calendars._SYNC_TIME, System.currentTimeMillis());
+
+        return url;
+    }
+
+    // TODO: unit test.
+    protected static final String convertCalendarIdToFeedUrl(String url) {
+        // id: http://www.google.com/calendar/feeds/<username>/<cal id>
+        // desired feed:
+        //   http://www.google.com/calendar/feeds/<cal id>/<projection>
+        int start = url.indexOf(FEEDS_SUBSTRING);
+        if (start != -1) {
+            // strip out the */ in /feeds/*/
+            start += FEEDS_SUBSTRING.length();
+            int end = url.indexOf('/', start);
+            if (end != -1) {
+                url = url.replace(url.substring(start, end + 1), "");
+            }
+            url = url + "/private" + DESIRED_PROJECTION;
+        }
+        return url;
+    }
 }
diff --git a/src/com/android/providers/calendar/CalendarSyncAdapterService.java b/src/com/android/providers/calendar/CalendarSyncAdapterService.java
new file mode 100644
index 0000000..7634f2c
--- /dev/null
+++ b/src/com/android/providers/calendar/CalendarSyncAdapterService.java
@@ -0,0 +1,29 @@
+package com.android.providers.calendar;
+
+import android.app.Service;
+import android.os.IBinder;
+import android.content.Intent;
+import android.content.ContentProviderClient;
+import android.content.ContentProvider;
+import android.content.SyncableContentProvider;
+import android.provider.Calendar;
+
+public class CalendarSyncAdapterService extends Service {
+    private ContentProviderClient mContentProviderClient = null;
+
+    public void onCreate() {
+        mContentProviderClient =
+                getContentResolver().acquireContentProviderClient(Calendar.CONTENT_URI);
+    }
+
+    public void onDestroy() {
+        mContentProviderClient.release();
+    }
+
+    public IBinder onBind(Intent intent) {
+        ContentProvider contentProvider = mContentProviderClient.getLocalContentProvider();
+        if (contentProvider == null) throw new IllegalStateException();
+        SyncableContentProvider syncableContentProvider = (SyncableContentProvider)contentProvider;
+        return syncableContentProvider.getTempProviderSyncAdapter().getISyncAdapter().asBinder();
+    }
+}
diff --git a/tests/src/com/android/providers/calendar/TestCalendarSyncAdapter.java b/tests/src/com/android/providers/calendar/TestCalendarSyncAdapter.java
index 136b382..1fc77e6 100644
--- a/tests/src/com/android/providers/calendar/TestCalendarSyncAdapter.java
+++ b/tests/src/com/android/providers/calendar/TestCalendarSyncAdapter.java
@@ -26,6 +26,7 @@
 import android.net.Uri;
 import android.os.Bundle;
 import android.text.format.DateUtils;
+import android.accounts.Account;
 
 import java.util.Calendar;
 import java.util.TimeZone;
@@ -40,7 +41,7 @@
     }
 
     @Override
-    public void onSyncStarting(SyncContext context, String account, boolean forced,
+    public void onSyncStarting(SyncContext context, Account account, boolean forced,
             SyncResult result)
     {
     }
@@ -162,7 +163,7 @@
         throw new UnsupportedOperationException("not implemented");
     }
 
-    public void onAccountsChanged(String[] accounts) {
+    public void onAccountsChanged(Account[] accounts) {
     }
 }