Fix bug 697553 - Adapt to the calIChangeLog interface changes (gdata offline support).,a=philipp
authorPhilipp Kewisch <mozilla@kewis.ch>
Wed, 02 Nov 2011 23:41:02 +0100
changeset 8978 053fae2d6e0fc03a002be11a620336250bae3dee
parent 8977 29722e076d509d1638e28d05125d9b2728c574e9
child 8979 cefa766b7b7f07692ed69a289bf312624f056157
push id220
push usermozilla@kewis.ch
push dateWed, 02 Nov 2011 23:07:37 +0000
treeherdercomm-beta@faca33ff533f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersphilipp
bugs697553
Fix bug 697553 - Adapt to the calIChangeLog interface changes (gdata offline support).,a=philipp
calendar/base/content/dialogs/calendar-conflicts-dialog.xul
calendar/base/src/calCachedCalendar.js
calendar/providers/gdata/components/calGoogleCalendar.js
calendar/providers/gdata/components/calGoogleCalendarModule.js
calendar/providers/gdata/components/calGoogleRequest.js
calendar/providers/gdata/components/calGoogleSession.js
calendar/providers/gdata/components/calGoogleUtils.js
calendar/providers/gdata/public/calIGoogleRequest.idl
--- a/calendar/base/content/dialogs/calendar-conflicts-dialog.xul
+++ b/calendar/base/content/dialogs/calendar-conflicts-dialog.xul
@@ -61,17 +61,17 @@
         // should be reworked!
         docEl.title =  cal.calGetString("calendar", "itemModifiedOnServerTitle");
         descr.textContent = cal.calGetString("calendar", "itemModifiedOnServer");
 
         if (window.arguments[0].mode == "modify") {
             descr.textContent += cal.calGetString("calendar", "modifyWillLoseData");
             docEl.getButton("accept").setAttribute("label", cal.calGetString("calendar", "proceedModify"));
         } else {
-            promptMessage += cal.calGetString("calendar", "deleteWillLoseData");
+            descr.textContent += cal.calGetString("calendar", "deleteWillLoseData");
             docEl.getButton("accept").setAttribute("label", cal.calGetString("calendar", "proceedDelete"));
         }
 
         docEl.getButton("cancel").setAttribute("label", cal.calGetString("calendar", "updateFromServer"));
 
         window.sizeToContent();
     }
 
--- a/calendar/base/src/calCachedCalendar.js
+++ b/calendar/base/src/calCachedCalendar.js
@@ -448,17 +448,17 @@ calCachedCalendar.prototype = {
             itemCount: 0,
 
             onGetResult: function(calendar, status, itemType, detail, count, items) {
             },
             onOperationComplete: function(calendar, status, opType, id, detail) {
                 if (Components.isSuccessCode(status)) {
                     storage.resetItemOfflineFlag(detail, resetListener);
                 } else {
-                    cal.LOG("[calCachedCalendar.js] Unable to playback the items to the server. Will try back again. Aborting\n");
+                    cal.LOG("[calCachedCalendar.js] Unable to playback the items to the server. Will try again later. Aborting\n");
                     // TODO does something need to be called back?
                 }
 
                 this.itemCount--;
                 if (this.itemCount == 0) {
                     this_.playbackModifiedItems(callbackFunc);
                 }
             }
@@ -473,22 +473,31 @@ calCachedCalendar.prototype = {
             onOperationComplete: function(calendar, status, opType, id, detail) {
                 if (this_.offline) {
                     cal.LOG("[calCachedCalendar] back to offline mode, reconciliation aborted");
                     callbackFunc();
                 } else {
                     cal.LOG("[calCachedCalendar] Adding "  + this.items.length + " items to " + this_.name);
                     if (this.items.length > 0) {
                         addListener.itemCount = this.items.length;
-                        for each (var aItem in this.items) {
-                            if (this_.supportsChangeLog) {
-                                this_.mUncachedCalendar.addItemOrUseCache(aItem, false, addListener);
-                            } else {
-                                // default mechanism for providers not implementing calIChangeLog
-                                this_.mUncachedCalendar.adoptItem(aItem.clone(), addListener);
+                        for each (let item in this.items) {
+                            try {
+                                if (this_.supportsChangeLog) {
+                                    this_.mUncachedCalendar.addItemOrUseCache(item, false, addListener);
+                                } else {
+                                    // default mechanism for providers not implementing calIChangeLog
+                                    this_.mUncachedCalendar.adoptItem(item.clone(), addListener);
+                                }
+                            } catch (e) {
+                                cal.ERROR("[calCachedCalendar] Could not playback added item " + item.title + ": " + e);
+                                addListener.onOperationComplete(thisCalendar,
+                                                                e.result,
+                                                                Components.interfaces.calIOperationListener.ADD,
+                                                                item.id,
+                                                                e.message);
                             }
                         }
                     } else {
                         this_.playbackModifiedItems(callbackFunc);
                     }
                 }
                 delete this.items;
             }
@@ -532,23 +541,32 @@ calCachedCalendar.prototype = {
                 if (this_.offline) {
                     cal.LOG("[calCachedCalendar] Returning to offline mode, reconciliation aborted");
                     callbackFunc();
                 } else {
                     cal.LOG("[calCachedCalendar] Modifying " + this.items.length + " items in " + this_.name);
                     if (this.items.length > 0) {
                         modifyListener.itemCount = this.items.length;
                         for each (let item in this.items) {
-                            if (this_.supportsChangeLog) {
-                                // The calendar supports the changelog functions, let it modify the item
-                                // TODO is it ok to not have the old item here? Pass null or the new item?
-                                this_.mUncachedCalendar.modifyItemOrUseCache(item, item, false, modifyListener);
-                            } else {
-                                // Default strategy for providers not implementing calIChangeLog
-                                this_.mUncachedCalendar.modifyItem(item, null, modifyListener);
+                            try {
+                                if (this_.supportsChangeLog) {
+                                    // The calendar supports the changelog functions, let it modify the item
+                                    // TODO is it ok to not have the old item here? Pass null or the new item?
+                                    this_.mUncachedCalendar.modifyItemOrUseCache(item, item, false, modifyListener);
+                                } else {
+                                    // Default strategy for providers not implementing calIChangeLog
+                                    this_.mUncachedCalendar.modifyItem(item, null, modifyListener);
+                                }
+                            } catch (e) {
+                                cal.ERROR("[calCachedCalendar] Could not playback modified item " + item.title + ": " + e);
+                                modifyListener.onOperationComplete(thisCalendar,
+                                                                   e.result,
+                                                                   Components.interfaces.calIOperationListener.MODIFY,
+                                                                   item.id,
+                                                                   e.message);
                             }
                         }
                     } else {
                         this_.playbackDeletedItems(callbackFunc);
                     }
                 }
                 delete this.items;
             }
@@ -590,22 +608,31 @@ calCachedCalendar.prototype = {
             onOperationComplete: function(calendar, status, opType, id, detail) {
                 if (this_.offline) {
                     cal.LOG("[calCachedCalendar] Returning to offline mode, reconciliation aborted");
                     callbackFunc();
                 } else {
                     cal.LOG("[calCachedCalendar] Deleting "  + this.items.length + " items from " + this_.name);
                     if (this.items.length > 0) {
                         deleteListener.itemCount = this.items.length;
-                        for each (var aItem in this.items) {
-                            if (this_.supportsChangeLog) {
-                                this_.mUncachedCalendar.deleteItemOrUseCache(aItem, false, deleteListener);
-                            } else {
-                                //Default strategy for providers not implementing calIChangeLog
-                                this_.mUncachedCalendar.deleteItem(aItem, deleteListener);
+                        for each (let item in this.items) {
+                            try {
+                                if (this_.supportsChangeLog) {
+                                    this_.mUncachedCalendar.deleteItemOrUseCache(item, false, deleteListener);
+                                } else {
+                                    // Default strategy for providers not implementing calIChangeLog
+                                    this_.mUncachedCalendar.deleteItem(item, deleteListener);
+                                }
+                            } catch (e) {
+                                cal.ERROR("[calCachedCalendar] Could not playback deleted item " + item.title + ": " + e);
+                                deleteListener.onOperationComplete(thisCalendar,
+                                                                   e.result,
+                                                                   Components.interfaces.calIOperationListener.MODIFY,
+                                                                   item.id,
+                                                                   e.message);
                             }
                         }
                     } else if (callbackFunc) {
                         callbackFunc();
                     }
                 }
                 delete this.items;
             }
--- a/calendar/providers/gdata/components/calGoogleCalendar.js
+++ b/calendar/providers/gdata/components/calGoogleCalendar.js
@@ -34,16 +34,18 @@
  * the provisions above, a recipient may use your version of this file under
  * the terms of any one of the MPL, the GPL or the LGPL.
  *
  * ***** END LICENSE BLOCK ***** */
 
 Components.utils.import("resource://calendar/modules/calProviderUtils.jsm");
 Components.utils.import("resource://calendar/modules/calUtils.jsm");
 
+const cICL = Components.interfaces.calIChangeLog;
+
 /**
  * calGoogleCalendar
  * This Implements a calICalendar Object adapted to the Google Calendar
  * Provider.
  *
  * @class
  * @constructor
  */
@@ -322,17 +324,21 @@ calGoogleCalendar.prototype = {
         return this.__proto__.__proto__.getProperty.apply(this, arguments);
     },
 
     get canRefresh() {
         return true;
     },
 
     adoptItem: function cGC_adoptItem(aItem, aListener) {
-        LOG("Adding item " + aItem.title);
+        return this.adoptItemOrUseCache(aItem, !!this.mOfflineStorage, aListener);
+    },
+
+    adoptItemOrUseCache: function adoptItemOrUseCache(aItem, useCache, aListener) {
+        cal.LOG("[calGoogleCalendar] Adding item " + aItem.title);
 
         try {
             // Check if calendar is readonly
             if (this.readOnly) {
                 throw new Components.Exception("",
                                                Components.interfaces.calIErrors.CAL_IS_READONLY);
             }
 
@@ -351,30 +357,25 @@ calGoogleCalendar.prototype = {
                                           this.session.userName,
                                           this.session.fullName);
 
             request.type = request.ADD;
             request.uri = this.fullUri.spec;
             request.setUploadData("application/atom+xml; charset=UTF-8", xmlEntry);
             request.operationListener = aListener;
             request.calendar = this;
-
-            var calendar = this;
-            request.responseListener = {
-                onResult: function cGC_addItem_response_onResult(aOperation, aData) {
-                    calendar.addItem_response(aOperation, aData);
-                }
-            };
-
+            request.newItem = aItem;
+            request.useCache = useCache;
+            request.responseListener = { onResult: this.addItem_response.bind(this) };
             request.addQueryParameter("ctz", calendarDefaultTimezone().tzid);
 
             this.session.asyncItemRequest(request);
             return request;
         } catch (e) {
-            LOG("adoptItem failed before request " + aItem.title + "\n:" + e);
+            cal.LOG("[calGoogleCalendar] adoptItem failed before request " + aItem.title + "\n:" + e);
             if (e.result == Components.interfaces.calIErrors.CAL_IS_READONLY) {
                 // The calendar is readonly, make sure this is set and
                 // notify the user. This can come from above or from
                 // mSession.addItem which checks for the editURI
                 this.readOnly = true;
             }
 
             this.notifyOperationComplete(aListener,
@@ -384,54 +385,92 @@ calGoogleCalendar.prototype = {
                                          e.message);
         }
         return null;
     },
 
     addItem: function cGC_addItem(aItem, aListener) {
         // Google assigns an ID to every event added. Any id set here or in
         // adoptItem will be overridden.
-        return this.adoptItem( aItem.clone(), aListener );
+        return this.addItemOrUseCache(aItem, !!this.mOfflineStorage, aListener);
+    },
+
+    addItemOrUseCache: function addItemOrUseCache(aItem, useCache, aListener) {
+        return this.adoptItemOrUseCache(aItem.clone(), useCache, aListener);
     },
 
     modifyItem: function cGC_modifyItem(aNewItem, aOldItem, aListener) {
-        LOG("Modifying item " + aOldItem.title);
+        if (this.mOfflineStorage) {
+            return this.modifyItemOrUseCache(aNewItem, aOldItem, true, aListener);
+        } else {
+            return this.doModifyItem(aNewItem, aOldItem, false, aListener);
+        }
+    },
+
+    modifyItemOrUseCache: function modifyItemOrUseCache(aNewItem, aOldItem, useCache, aListener) {
+        let thisCalendar = this;
+        let storage = this.mOfflineStorage.QueryInterface(Components.interfaces.calIOfflineStorage);
+        let modifyOfflineListener = {
+            onGetResult: function(calendar, status, itemType, detail, count, items) {},
+            onOperationComplete: function(calendar, status, opType, id, detail) {
+                storage.modifyOfflineItem(detail, aListener);
+            }
+        };
+
+        let offlineFlagListener = {
+            onGetResult: function(calendar, status, itemType, detail, count, items) {},
+            onOperationComplete: function(calendar, status, opType, id, detail) {
+                let offline_flag = detail;
+                if ((offline_flag == cICL.OFFLINE_FLAG_CREATED_RECORD ||
+                     offline_flag == cICL.OFFLINE_FLAG_MODIFIED_RECORD) && useCache) {
+                    storage.modifyItem(aNewItem, aOldItem, modifyOfflineListener);
+                } else {
+                    thisCalendar.doModifyItem(aNewItem, aOldItem, useCache, aListener, false);
+                }
+            }
+        };
+        storage.getItemOfflineFlag(aOldItem, offlineFlagListener);
+    },
+
+    doModifyItem: function doModifyItem(aNewItem, aOldItem, useCache, aListener) {
+        cal.LOG("[calGoogleCalendar] Modifying item " + aOldItem.title);
 
         try {
             if (this.readOnly) {
                 throw new Components.Exception("",
                                                Components.interfaces.calIErrors.CAL_IS_READONLY);
             }
 
             // Check if we have a session. If not, then the user has canceled
             // the login prompt.
             this.ensureSession();
 
             // Check if enough fields have changed to warrant sending the event
-            // to google. This saves network traffic.
-            if (relevantFieldsMatch(aOldItem, aNewItem)) {
-                LOG("Not requesting item modification for " + aOldItem.id +
-                    "(" + aOldItem.title + "), relevant fields match");
+            // to google. This saves network traffic. Also check if the item isn't
+            // the same to work around a bug in the cache layer.
+            if (aOldItem != aNewItem && relevantFieldsMatch(aOldItem, aNewItem)) {
+                cal.LOG("[calGoogleCalendar] Not requesting item modification for " + aOldItem.id +
+                        "(" + aOldItem.title + "), relevant fields match");
 
                 this.notifyOperationComplete(aListener,
                                              Components.results.NS_OK,
                                              Components.interfaces.calIOperationListener.MODIFY,
                                              aNewItem.id,
                                              aNewItem);
                 this.mObservers.notify("onModifyItem", [aNewItem, aOldItem]);
                 return null;
             }
 
             // Set up the request
             var request = new calGoogleRequest(this.session);
 
             // We need to clone the new item, its possible that ItemToXMLEntry
             // will modify the item. For example, if the item is organized by
             // someone else, we cannot save alarms on it and they should
-            // therefore not be added in the returned item. 
+            // therefore not be added in the returned item.
             var newItem = aNewItem.clone();
 
             var xmlEntry = ItemToXMLEntry(newItem, this,
                                           this.session.userName,
                                           this.session.fullName);
 
             if (aOldItem.parentItem != aOldItem &&
                 !aOldItem.parentItem.recurrenceInfo.getExceptionFor(aOldItem.startDate)) {
@@ -441,36 +480,29 @@ calGoogleCalendar.prototype = {
                 request.uri = this.fullUri.spec;
             } else {
                 // We are  making a negative exception or modifying a parent item
                 request.type = request.MODIFY;
                 request.uri = getItemEditURI(aOldItem);
             }
 
             request.setUploadData("application/atom+xml; charset=UTF-8", xmlEntry);
-            request.responseListener = this.modifyItem_response,
+            request.responseListener = { onResult: this.modifyItem_response.bind(this) };
             request.operationListener = aListener;
             request.newItem = newItem;
             request.oldItem = aOldItem;
             request.calendar = this;
-
-            var calendar = this;
-            request.responseListener = {
-                onResult: function cGC_modifyItem_response_onResult(aOperation, aData) {
-                    calendar.modifyItem_response(aOperation, aData);
-                }
-            };
-
+            request.useCache = useCache;
             request.addQueryParameter("ctz", calendarDefaultTimezone().tzid);
 
             this.session.asyncItemRequest(request);
             return request;
         } catch (e) {
-            LOG("modifyItem failed before request " +
-                aNewItem.title + "(" + aNewItem.id + "):\n" + e);
+            cal.LOG("[calGoogleCalendar] modifyItem failed before request " +
+                    aNewItem.title + "(" + aNewItem.id + "):\n" + e);
 
             if (e.result == Components.interfaces.calIErrors.CAL_IS_READONLY) {
                 // The calendar is readonly, make sure this is set and
                 // notify the user. This can come from above or from
                 // mSession.modifyItem which checks for the editURI
                 this.readOnly = true;
             }
 
@@ -478,18 +510,55 @@ calGoogleCalendar.prototype = {
                                          e.result,
                                          Components.interfaces.calIOperationListener.MODIFY,
                                          null,
                                          e.message);
         }
         return null;
     },
 
+
     deleteItem: function cGC_deleteItem(aItem, aListener) {
-        LOG("Deleting item " + aItem.title + "(" + aItem.id + ")");
+        if (this.mOfflineStorage) {
+            return this.deleteItemOrUseCache(aItem, true, aListener);
+        } else {
+            return this.doDeleteItem(aItem, false, aListener);
+        }
+    },
+
+    deleteItemOrUseCache: function deleteItemOrUseCache(aItem, useCache, aListener) {
+        let thisCalendar = this;
+        let storage = this.mOfflineStorage.QueryInterface(Components.interfaces.calIOfflineStorage);
+        let deleteOfflineListener = {
+            onGetResult: function(calendar, status, itemType, detail, count, items) {},
+            onOperationComplete: function(calendar, status, opType, id, detail) {
+                if (aListener) {
+                    aListener.onOperationComplete(calendar, status, opType, aItem.id, aItem);
+                }
+            }
+        };
+
+        let offlineFlagListener = {
+            onGetResult: function(calendar, status, itemType, detail, count, items) {},
+            onOperationComplete: function(calendar, status, opType, id, detail) {
+                let offline_flag = detail;
+                if ((offline_flag == cICL.OFFLINE_FLAG_CREATED_RECORD ||
+                     offline_flag == cICL.OFFLINE_FLAG_MODIFIED_RECORD) && useCache) {
+                    /* We do not delete the item from the cache, but mark it deleted */
+                    storage.deleteOfflineItem(aItem, aListener);
+                } else {
+                    thisCalendar.doDeleteItem(aItem, useCache, aListener);
+                }
+            }
+        };
+        storage.getItemOfflineFlag(aItem, offlineFlagListener);
+    },
+
+    doDeleteItem: function doDeleteItem(aItem, useCache, aListener) {
+        cal.LOG("[calGoogleCalendar] Deleting item " + aItem.title + "(" + aItem.id + ")");
 
         try {
             if (this.readOnly) {
                 throw new Components.Exception("",
                                                Components.interfaces.calIErrors.CAL_IS_READONLY);
             }
 
             // Check if we have a session. If not, then the user has canceled
@@ -500,29 +569,24 @@ calGoogleCalendar.prototype = {
             // item XML data on delete, and we need to call the observers.
             var request = new calGoogleRequest(this);
 
             request.type = request.DELETE;
             request.uri = getItemEditURI(aItem);
             request.operationListener = aListener;
             request.oldItem = aItem;
             request.calendar = this;
-
-            var calendar = this;
-            request.responseListener = {
-                onResult: function cGC_deleteItem_response_onResult(aOperation, aData) {
-                    calendar.deleteItem_response(aOperation, aData);
-                }
-            };
+            request.useCache = useCache;
+            request.responseListener = { onResult: this.deleteItem_response.bind(this) };
 
             this.session.asyncItemRequest(request);
             return request;
         } catch (e) {
-            LOG("deleteItem failed before request for " +
-                aItem.title + "(" + aItem.id + "):\n" + e);
+            cal.LOG("[calGoogleCalendar] deleteItem failed before request for " +
+                    aItem.title + "(" + aItem.id + "):\n" + e);
 
             if (e.result == Components.interfaces.calIErrors.CAL_IS_READONLY) {
                 // The calendar is readonly, make sure this is set and
                 // notify the user. This can come from above or from
                 // mSession.deleteItem which checks for the editURI
                 this.readOnly = true;
             }
 
@@ -532,49 +596,43 @@ calGoogleCalendar.prototype = {
                                          null,
                                          e.message);
         }
         return null;
     },
 
     getItem: function cGC_getItem(aId, aListener) {
         // This function needs a test case using mechanisms in bug 365212
-        LOG("Getting item with id " + aId);
+        cal.LOG("[calGoogleCalendar] Getting item with id " + aId);
         try {
 
             // Check if we have a session. If not, then the user has canceled
             // the login prompt.
             this.ensureSession();
 
             // Set up the request
 
             var request = new calGoogleRequest(this);
 
             request.itemId = aId;
             request.type = request.GET;
             request.uri = this.fullUri.spec;
             request.operationListener = aListener;
+            request.responseListener = { onResult: this.getItem_response.bind(this) };
             request.calendar = this;
 
-            var calendar = this;
-            request.responseListener = {
-                onResult: function cGC_getItem_response_onResult(aOperation, aData) {
-                    calendar.getItem_response(aOperation, aData);
-                }
-            };
-
             // Request Parameters
             request.addQueryParameter("ctz", calendarDefaultTimezone().tzid);
             request.addQueryParameter("max-results", kMANY_EVENTS);
             request.addQueryParameter("singleevents", "false");
 
             this.session.asyncItemRequest(request);
             return request;
         } catch (e) {
-            LOG("getItem failed before request " + aId + "):\n" + e);
+            cal.LOG("[calGoogleCalendar] getItem failed before request " + aId + "):\n" + e);
 
             this.notifyOperationComplete(aListener,
                                          e.result,
                                          Components.interfaces.calIOperationListener.GET,
                                          null,
                                          e.message);
         }
         return null;
@@ -632,24 +690,17 @@ calGoogleCalendar.prototype = {
 
             // Request Parameters
             request.addQueryParameter("ctz", calendarDefaultTimezone().tzid);
             request.addQueryParameter("max-results",
                                       aCount ? aCount : kMANY_EVENTS);
             request.addQueryParameter("singleevents", "false");
             request.addQueryParameter("start-min", rfcRangeStart);
             request.addQueryParameter("start-max", rfcRangeEnd);
-
-            var calendar = this;
-            request.responseListener = {
-                onResult: function cGC_getItems_response_onResult(aOperation, aData) {
-                    calendar.getItems_response(aOperation, aData);
-                }
-            };
-
+            request.responseListener = { onResult: this.getItems_response.bind(this) };
             this.session.asyncItemRequest(request);
             return request;
         } catch (e) {
             this.notifyOperationComplete(aListener,
                                          e.result,
                                          Components.interfaces.calIOperationListener.GET,
                                          null,
                                          e.message);
@@ -669,86 +720,246 @@ calGoogleCalendar.prototype = {
      * addItem_response
      * Response function, called by the session object when an item was added
      *
      * @param aOperation The calIGoogleRequest processing the request
      * @param aData      In case of an error, this is the error string, otherwise
      *                     an XML representation of the added item.
      */
     addItem_response: function cGC_addItem_response(aOperation, aData) {
-        var item = this.general_response(aOperation, aData);
+        // First, have the general response retrieve the item
+        let item, e;
+        try {
+            item = DataToItem(aOperation, aData, this, null);
+            if (!resolveConflicts(aOperation, item)) {
+                // If a conflict occurred and the user wants to overwrite, a new
+                // request will be sent. bail out here, this method will be
+                // called again
+                return;
+            }
+        } catch (exp) {
+            item = null;
+            e = exp;
+        }
 
-        if (item) {
-            this.mObservers.notify("onAddItem", [item]);
+        // Now we can do some processing on it. If the cache is used, then we
+        // ultimately need to add the item to the cache, either as offline item
+        // or normal item. We will also have to notify the listener and the
+        // observers sooner or later.
+        if (aOperation.useCache && this.mOfflineStorage && (!e || isCacheException(e))) {
+            let listener;
+            if (item) {
+                // If we have an item, then we can directly notify the target
+                // listener
+                listener = aOperation.operationListener;
+                cal.LOG("[calGoogleCalendar] Adding item " + aOperation.newItem.title + " successful");
+            } else {
+                // Otherwise we create an intermediate listener that also sets
+                // the offline flag
+                let storage = this.mOfflineStorage.QueryInterface(Components.interfaces.calIOfflineStorage);
+                let self = this;
+                item = aOperation.newItem;
+                listener = {
+                    onGetResult: function(calendar, status, itemType, detail, count, items) {},
+                    onOperationComplete: function(calendar, status, opType, id, detail) {
+                        if (Components.isSuccessCode(status)) {
+                            // On success, the addOfflineItem call will notify the listener
+                            cal.LOG("[calGoogleCalendar] Adding item " + aOperation.newItem.title + " failed, but the operation will be retried (status: " + status + ", exception: " + e + ")");
+                            storage.addOfflineItem(detail, aOperation.operationListener);
+                            self.mObservers.notify("onAddItem", [aOperation.newItem]);
+                        } else if (aOperation.operationListener) {
+                            cal.ERROR("[calGoogleCalendar] Could not add item " + aOperation.newItem.title + " to the offline cache:" +
+                                      new Components.Exception(detail, status));
+                            // Otherwise we have to do it ourselves.
+                            self.notifyOperationComplete(aOperation.operationListener,
+                                                         status,
+                                                         Components.interfaces.calIOperationListener.ADD,
+                                                         null,
+                                                         null);
+                        }
+                    }
+                };
+            }
+
+            // Now send the item to the offline storage
+            this.mOfflineStorage.adoptItem(item, listener);
+        } else {
+            // When not using the cache, we merely have to notify onAddItem and
+            // tell the operation listener we are done (error or not)
+            if (item) {
+                cal.LOG("[calGoogleCalendar] Adding item " + item.title + " successful");
+                this.mObservers.notify("onAddItem", [item]);
+            } else {
+                cal.LOG("[calGoogleCalendar] Adding item " + aOperation.newItem.id + " failed, status " + aOperation.status + ", Exception: " + e);
+            }
+            this.notifyOperationComplete(aOperation.operationListener,
+                                         (item ? Components.results.NS_OK : e.result),
+                                         Components.interfaces.calIOperationListener.ADD,
+                                         (item ? item.id : null),
+                                         (item ? item : e.message));
         }
     },
 
     /**
      * modifyItem_response
      * Response function, called by the session object when an item was modified
      *
      * @param aOperation The calIGoogleRequest processing the request
      * @param aData      In case of an error, this is the error string, otherwise
      *                     an XML representation of the added item.
      */
     modifyItem_response: function cGC_modifyItem_response_onResult(aOperation,
                                                                    aData) {
-        var item = this.general_response(aOperation,
-                                         aData,
-                                         aOperation.newItem);
-        // Notify Observers
-        if (item) {
-            var oldItem = aOperation.oldItem;
-            if (item.parentItem != item) {
+        let self = this;
+        function notifyObserver(item, oldItem) {
+            if (item && item.parentItem != item) {
                 item.parentItem.recurrenceInfo.modifyException(item, false);
                 item = item.parentItem;
                 oldItem = oldItem.parentItem;
             }
-            this.mObservers.notify("onModifyItem", [item, oldItem]);
+            // Notify Observers
+            if (item) {
+                self.mObservers.notify("onModifyItem", [item, oldItem]);
+            }
+        }
+
+        // First, convert the data to an item and make sure no conflicts occurred.
+        let newItem, e;
+        try {
+            newItem = DataToItem(aOperation, aData, this, aOperation.newItem);
+            if (!resolveConflicts(aOperation, newItem)) {
+                // If a conflict occurred and the user wants to overwrite, a new
+                // request will be sent. bail out here, this method will be
+                // called again
+                return;
+            }
+        } catch (exp) {
+            newItem = null;
+            e = exp;
+        }
+
+        // Now we can do some processing on it. If the cache is used, then we
+        // ultimately need to modify the item in the cache, either as offline item
+        // or normal item. We will also have to notify the listener and the
+        // observers sooner or later.
+        if (aOperation.useCache && this.mOfflineStorage && (!e || isCacheException(e))) {
+            let listener;
+            if (newItem) {
+                // If we have an item, then we can directly notify the target
+                // listener
+                listener = aOperation.operationListener;
+                cal.LOG("[calGoogleCalendar] Modifying item " + aOperation.oldItem.id + " successful");
+            } else {
+                let storage = this.mOfflineStorage.QueryInterface(Components.interfaces.calIOfflineStorage);
+                newItem = aOperation.newItem;
+                listener = {
+                    onGetResult: function(calendar, status, itemType, detail, count, items) {},
+                    onOperationComplete: function(calendar, status, opType, id, detail) {
+                        if (Components.isSuccessCode(status)) {
+                            cal.LOG("[calGoogleCalendar] Modifying item " + aOperation.oldItem.id + " failed, but the operation will be retried (status: " + status + ", exception: " + e + ")");
+                            // On success, the modifyOfflineItem call will notify the listener
+                            storage.modifyOfflineItem(detail, aOperation.operationListener);
+                            notifyObserver(newItem, aOperation.oldItem);
+                        } else if (aOperation.operationListener) {
+                            cal.ERROR("[calGoogleCalendar] Could not modify item " + aOperation.newItem.id + " in the offline cache:" +
+                                      new Components.Exception(detail, status));
+                            // Otherwise we have to do it ourselves.
+                            self.notifyOperationComplete(aOperation.operationListener,
+                                                         status,
+                                                         Components.interfaces.calIOperationListener.MODIFY,
+                                                         null,
+                                                         null);
+                        }
+                    }
+                };
+            }
+
+            // Now send the item to the offline storage
+            this.mOfflineStorage.modifyItem(newItem, aOperation.oldItem, listener);
+        } else {
+            // When not using the cache, we merely have to notify onModifyItem and
+            // tell the operation listener we are done (error or not)
+            notifyObserver(newItem, aOperation.oldItem);
+            if (newItem) {
+                cal.LOG("[calGoogleCalendar] Modifying item " + newItem.id + " successful");
+            } else {
+                cal.LOG("[calGoogleCalendar] Modifying item " + aOperation.oldItem.id + " failed, status " + aOperation.status + ", Exception: " + e);
+            }
+            this.notifyOperationComplete(aOperation.operationListener,
+                                         (newItem ? Components.results.NS_OK : e.result || Components.results.NS_ERROR_FAILURE),
+                                         Components.interfaces.calIOperationListener.MODIFY,
+                                         (newItem ? newItem.id : null),
+                                         (newItem ? newItem : e.message));
         }
     },
 
     /**
      * deleteItem_response
      * Response function, called by the session object when an Item was deleted
      *
      * @param aOperation The calIGoogleRequest processing the request
      * @param aData      In case of an error, this is the error string, otherwise
      *                     an XML representation of the added item.
      */
     deleteItem_response: function cGC_deleteItem_response_onResult(aOperation,
                                                                    aData) {
-        // The reason we are not using general_response here is because deleted
-        // items are not returned as xml from google. We need to pass the item
-        // we saved with the request.
-
+        let item, e;
         try {
-            // Check if the call succeeded
-            if (aOperation.status != Components.results.NS_OK) {
-                throw new Components.Exception(aData, aOperation.status);
+            item = DataToItem(aOperation, aData, this, aOperation.oldItem);
+            if (!resolveConflicts(aOperation, item)) {
+                // If a conflict occurred and the user wants to overwrite, a new
+                // request will be sent. bail out here, this method will be
+                // called again
+                return;
             }
+        } catch (exp) {
+            item = null;
+            e = exp;
+        }
 
-            // All operations need to call onOperationComplete
-            LOG("Deleting item " + aOperation.oldItem.id + " successful");
+        if (aOperation.useCache && this.mOfflineStorage && (!e || isCacheException(e))) {
+            if (item) {
+                // If we have an item, then we can directly notify the target
+                // listener using the offline storage
+                cal.LOG("[calGoogleCalendar] Deleting item " + aOperation.oldItem.id + " successful");
+                this.mOfflineStorage.deleteItem(item, aOperation.operationListener);
+            } else {
+                // Otherwise we shouldn't remove it from the cache, but set the
+                // offline flag to mark it deleted
+                let self = this;
+                let offlineListener = {
+                    // We should not return a success code since the listeners can delete the physical item in case of success
+                    onGetResult: function(calendar, status, itemType, detail, count, items) {},
+                    onOperationComplete: function(calendar, status, opType, id, detail) {
+                        self.mObservers.notify("onDeleteItem", [aOperation.oldItem]);
+                        cal.LOG("[calGoogleCalendar] Deleting item " + aOperation.oldItem.id + " failed, but the operation will be retried");
+                        self.notifyOperationComplete(aOperation.operationListener,
+                                                     Components.results.NS_ERROR_NOT_AVAILABLE,
+                                                     Components.interfaces.calIOperationListener.GET,
+                                                     aOperation.oldItem.id,
+                                                     aOperation.oldItem);
+                    }
+                };
+                let storage = this.mOfflineStorage.QueryInterface(Components.interfaces.calIOfflineStorage);
+                storage.deleteOfflineItem(aOperation.oldItem, offlineListener);
+            }
+        } else {
+            // When not using the cache, we merely have to notify onDeleteItem and
+            // tell the operation listener we are done (error or not)
+            if (item) {
+                cal.LOG("[calGoogleCalendar] Deleting item " + aOperation.oldItem.id + " successful");
+                this.mObservers.notify("onDeleteItem", [item]);
+            } else {
+                cal.LOG("[calGoogleCalendar] Deleting item " + aOperation.oldItem.id + " failed, status " + aOperation.status + ", Exception: " + e);
+            }
             this.notifyOperationComplete(aOperation.operationListener,
-                                         Components.results.NS_OK,
-                                         Components.interfaces.calIOperationListener.DELETE,
-                                         aOperation.oldItem.id,
-                                         aOperation.oldItem);
-
-            // Notify Observers
-            this.mObservers.notify("onDeleteItem", [aOperation.oldItem]);
-        } catch (e) {
-            LOG("Deleting item " + aOperation.oldItem.id + " failed");
-            // Operation failed
-            this.notifyOperationComplete(aOperation.operationListener,
-                                         e.result,
-                                         Components.interfaces.calIOperationListener.DELETE,
-                                         null,
-                                         e.message);
+                                         (item ? Components.results.NS_OK : e.result || Components.results.NS_ERROR_FAILURE),
+                                         Components.interfaces.calIOperationListener.ADD,
+                                         (item ? item.id : null),
+                                         (item ? item : e.message));
         }
     },
 
     /**
      * getItem_response
      * Response function, called by the session object when a single Item was
      * downloaded.
      *
@@ -787,16 +998,17 @@ calGoogleCalendar.prototype = {
 
             // Get the item entry by id.
             var itemEntry = xml.entry.(id.substring(id.lastIndexOf('/') + 1) == aOperation.itemId ||
                                        gCal::uid.@value == aOperation.itemId);
             if (!itemEntry || !itemEntry.length()) {
                 // Item wasn't found. Skip onGetResult and just complete. Not
                 // finding an item isn't a user-important error, it may be a
                 // wanted case. (i.e itip)
+                cal.LOG("[calGoogleCalendar] Item " + aOperation.itemId + " not found in calendar " + this.name);
                 throw new Components.Exception("Item not found", Components.results.NS_OK);
             }
             var item = XMLEntryToItem(itemEntry, timezone, this);
             item.calendar = this.superCalendar;
 
             if (item.recurrenceInfo) {
                 // If this item is recurring, get all exceptions for this item.
                 for each (var entry in xml.entry.gd::originalEvent.(@id == aOperation.itemId)) {
@@ -809,30 +1021,31 @@ calGoogleCalendar.prototype = {
                     } else {
                         excItem.calendar = this;
                         item.recurrenceInfo.modifyException(excItem, true);
                     }
                 }
             }
             // We are done, notify the listener of our result and that we are
             // done.
+            cal.LOG("[calGoogleCalendar] Item " + aOperation.itemId + " was found in calendar " + this.name);
             aOperation.operationListener.onGetResult(this.superCalendar,
                                                      Components.results.NS_OK,
                                                      Components.interfaces.calIEvent,
                                                      null,
                                                      1,
                                                      [item]);
             this.notifyOperationComplete(aOperation.operationListener,
                                          Components.results.NS_OK,
                                          Components.interfaces.calIOperationListener.GET,
                                          item.id,
                                          null);
         } catch (e) {
             if (!Components.isSuccessCode(e.result)) {
-                LOG("Error getting item " + aOperation.itemId + ":\n" + e);
+                cal.LOG("[calGoogleCalendar] Error getting item " + aOperation.itemId + ":\n" + e);
                 Components.utils.reportError(e);
             }
             this.notifyOperationComplete(aOperation.operationListener,
                                          e.result,
                                          Components.interfaces.calIOperationListener.GET,
                                          null,
                                          e.message);
         }
@@ -848,17 +1061,17 @@ calGoogleCalendar.prototype = {
      *                     an XML representation of the added item.
      */
     getItems_response: function cGC_getItems_response(aOperation, aData) {
         // To simplify code, provide a one-stop function to call, independant of
         // if and what type of listener was passed.
         var listener = aOperation.operationListener ||
             { onGetResult: function() {}, onOperationComplete: function() {} };
 
-        LOG("Recieved response for " + aOperation.uri);
+        cal.LOG("[calGoogleCalendar] Recieved response for " + aOperation.uri);
         try {
             // Check if the call succeeded
             if (!Components.isSuccessCode(aOperation.status)) {
                 throw new Components.Exception(aData, aOperation.status);
             }
 
             // Prepare Namespaces
             var gCal = new Namespace("gCal",
@@ -866,17 +1079,17 @@ calGoogleCalendar.prototype = {
             var gd = new Namespace("gd", "http://schemas.google.com/g/2005");
             var atom = new Namespace("", "http://www.w3.org/2005/Atom");
             default xml namespace = atom;
 
             // A feed was passed back, parse it.
             var xml = cal.safeNewXML(aData);
             var timezoneString = xml.gCal::timezone.@value.toString() || "UTC";
             var timezone = gdataTimezoneService.getTimezone(timezoneString);
-            
+
             // This line is needed, otherwise the for each () block will never
             // be entered. It may seem strange, but if you don't believe me, try
             // it!
             xml.link.(@rel);
 
             // We might be able to get the full name through this feed's author
             // tags. We need to make sure we have a session for that.
             this.ensureSession();
@@ -911,31 +1124,31 @@ calGoogleCalendar.prototype = {
                     var att = item.getAttendeeById("mailto:" + this.session.userName);
                     if (!this.isInvitation(item) ||
                         !att ||
                         att.participationStatus != "NEEDS-ACTION") {
                         continue;
                     }
                 }
 
-                LOG("Parsing entry:\n" + entry + "\n");
+                cal.LOG("[calGoogleCalendar] Parsing entry:\n" + entry + "\n");
 
                 if (item.recurrenceInfo) {
                     // If we are doing an uncached operation, then we need to
                     // gather all exceptions and put them into the item.
                     // Otherwise, our listener will take care of mapping the
                     // exception to the base item.
                     for each (var oid in xml.entry.gd::originalEvent.(@id == item.id)) {
                         // Get specific fields so we can speed up the parsing process
                         var status = oid.parent().gd::eventStatus.@value.toString().substring(39);
 
                         if (status == "canceled") {
                             let rId = oid.gd::when.@startTime.toString();
                             let rDate = cal.fromRFC3339(rId, timezone);
-                            LOG("Negative exception " + rId + "/" + rDate);
+                            cal.LOG("[calGoogleCalendar] Negative exception " + rId + "/" + rDate);
                             item.recurrenceInfo.removeOccurrenceAt(rDate);
                         } else {
                             // Parse the exception and modify the current item
                             var excItem = XMLEntryToItem(oid.parent(),
                                                          timezone,
                                                          this);
                             if (excItem) {
                                 // Google uses the status field to reflect negative
@@ -962,112 +1175,42 @@ calGoogleCalendar.prototype = {
             }
             // Operation Completed successfully.
             this.notifyOperationComplete(listener,
                                          Components.results.NS_OK,
                                          Components.interfaces.calIOperationListener.GET,
                                          null,
                                          null);
         } catch (e) {
-            LOG("Error getting items:\n" + e);
+            cal.LOG("[calGoogleCalendar] Error getting items:\n" + e);
             this.notifyOperationComplete(listener,
                                          e.result,
                                          Components.interfaces.calIOperationListener.GET,
                                          null,
                                          e.message);
         }
     },
 
     /**
-     * general_response
-     * Handles common actions for multiple response types. This does not notify
-     * observers.
-     *
-     * @param aOperation        The calIGoogleRequest that initiated the request.
-     * @param aData             The string represenation of the item
-     * @param aReferenceItem    The item to apply the information from the xml
-     *                            to. If null, a new item will be used.
-     * @return                  The Item as a calIEvent, or null if an error
-     *                            happened
+     * Implement calIChangeLog
      */
-    general_response: function cGC_general_response(aOperation,
-                                                    aData,
-                                                    aReferenceItem) {
-
-        try {
-            // Check if the call succeeded, if not then aData is an error
-            // message
-
-            if (!Components.isSuccessCode(aOperation.status)) {
-                throw new Components.Exception(aData, aOperation.status);
-            }
-
-            // An Item was passed back, parse it.
-            var xml = cal.safeNewXML(aData);
-
-            // Get the local timezone from the preferences
-            var timezone = calendarDefaultTimezone();
-
-            // Parse the Item with the given timezone
-            var item = XMLEntryToItem(xml,
-                                      timezone,
-                                      this,
-                                      aReferenceItem);
-
-            LOGitem(item);
-            item.calendar = this.superCalendar;
-
-            // GET operations need to call onGetResult
-            if (aOperation.type == aOperation.GET) {
-                aOperation.operationListener
-                          .onGetResult(this.superCalendar,
-                                       Components.results.NS_OK,
-                                       Components.interfaces.calIEvent,
-                                       null,
-                                       1,
-                                       [item]);
-            }
-
-            // All operations need to call onOperationComplete
-            // calIGoogleRequest's type corresponds to calIOperationListener's
-            // constants, so we can use them here.
-            this.notifyOperationComplete(aOperation.operationListener,
-                                         Components.results.NS_OK,
-                                         aOperation.type,
-                                         (item ? item.id : null),
-                                         item);
-            return item;
-        } catch (e) {
-            LOG("General response failed: " + e);
-
-            if (e.result == Components.interfaces.calIErrors.CAL_IS_READONLY) {
-                // The calendar is readonly, make sure this is set and
-                // notify the user.
-                this.readOnly = true;
-            }
-
-            // Operation failed
-            this.notifyOperationComplete(aOperation.operationListener,
-                                         e.result,
-                                         aOperation.type,
-                                         null,
-                                         e.message);
-        }
-        return null;
+    get offlineStorage() {
+        return this.mOfflineStorage;
     },
 
-    /**
-     * Implement calIChangeLog
-     */
+    set offlineStorage(val) {
+        return (this.mOfflineStorage = val);
+    },
+
     resetLog: function cGC_resetLog() {
-        LOG("Resetting last updated counter for " + this.name);
+        cal.LOG("[calGoogleCalendar] Resetting last updated counter for " + this.name);
         this.deleteProperty("google.lastUpdated");
     },
 
-    replayChangesOn: function cGC_replayChangesOn(aDestination, aListener) {
+    replayChangesOn: function cGC_replayChangesOn(aListener) {
         var lastUpdate = this.getProperty("google.lastUpdated");
         var lastUpdateDateTime;
         if (lastUpdate) {
             // Set up the last sync stamp
             lastUpdateDateTime = createDateTime();
             lastUpdateDateTime.icalString = lastUpdate;
 
             // Set up last week
@@ -1075,33 +1218,27 @@ calGoogleCalendar.prototype = {
             lastWeek.day -= 7;
             if (lastWeek.compare(lastUpdateDateTime) >= 0) {
                 // The last sync was longer than a week ago. Google requires a full
                 // sync in that case. This call also takes care of calling
                 // resetLog().
                 this.superCalendar.wrappedJSObject.setupCachedCalendar();
                 lastUpdateDateTime = null;
             }
-            LOG("The calendar " + this.name + " was last modified: " + lastUpdateDateTime);
-
+            cal.LOG("[calGoogleCalendar] The calendar " + this.name + " was last modified: " + lastUpdateDateTime);
         }
 
         var request = new calGoogleRequest(this.mSession);
 
         request.type = request.GET;
         request.uri = this.fullUri.spec
-        request.destinationCal = aDestination;
+        request.destinationCal = this.mOfflineStorage;
 
         var calendar = this;
-        request.responseListener = {
-            onResult: function cGC_syncItems_response_onResult(aOperation, aData) {
-                calendar.syncItems_response(aOperation, aData, (lastUpdateDateTime == null));
-
-            }
-        };
+        request.responseListener = { onResult: this.syncItems_response.bind(this, (lastUpdateDateTime == null)) }
         request.operationListener = aListener;
         request.calendar = this;
 
         // Request Parameters
         request.addQueryParameter("ctz", calendarDefaultTimezone().tzid);
         request.addQueryParameter("max-results", kMANY_EVENTS);
         request.addQueryParameter("singleevents", "false");
 
@@ -1115,23 +1252,23 @@ calGoogleCalendar.prototype = {
         this.mSession.asyncItemRequest(request);
     },
 
     /**
      * syncItems_response
      * Response function, called by the session object when an Item feed was
      * downloaded.
      *
+     * @param aIsFullSync   If set, this is a full sync rather than an update.
      * @param aOperation    The calIGoogleRequest processing the request
      * @param aData         In case of an error, this is the error string, otherwise
      *                        an XML representation of the added item.
-     * @param aIsFullSync   If set, this is a full sync rather than an update.
      */
-    syncItems_response: function cGC_syncItems_response(aOperation, aData, isFullSync) {
-        LOG("Recieved response for " + aOperation.uri + (isFullSync ? " (full sync)" : ""));
+    syncItems_response: function cGC_syncItems_response(aIsFullSync, aOperation, aData) {
+        cal.LOG("[calGoogleCalendar] Recieved response for " + aOperation.uri + (aIsFullSync ? " (full sync)" : ""));
         try {
             // Check if the call succeeded
             if (!Components.isSuccessCode(aOperation.status)) {
                 throw new Components.Exception(aData, aOperation.status);
             }
 
             // Prepare Namespaces
             var gCal = new Namespace("gCal",
@@ -1161,50 +1298,50 @@ calGoogleCalendar.prototype = {
             }
 
             // This is the calendar we should sync changes into.
             var destinationCal = aOperation.destinationCal;
 
             for each (var entry in xml.entry) {
 
                 var recurrenceId = getRecurrenceIdFromEntry(entry, timezone);
-                if (isFullSync && recurrenceId) {
+                if (aIsFullSync && recurrenceId) {
                     // On a full sync, we parse exceptions different.
                     continue;
                 }
-                LOG("Parsing entry:\n" + entry + "\n");
+                cal.LOG("[calGoogleCalendar] Parsing entry:\n" + entry + "\n");
 
                 var referenceItemObj = {}
                 destinationCal.getItem(getIdFromEntry(entry),
                                        new syncSetter(referenceItemObj));
                 var referenceItem = referenceItemObj.value &&
                                     referenceItemObj.value.clone();
 
                 // Parse the item. If we got a reference item from the storage
                 // calendar, put that in to make sure we get all exceptions and
                 // such.
                 var item = XMLEntryToItem(entry,
                                           timezone,
                                           this,
                                           (recurrenceId && referenceItem ? null : referenceItem));
                 item.calendar = this.superCalendar;
 
-                if (isFullSync && item.recurrenceInfo) {
+                if (aIsFullSync && item.recurrenceInfo) {
                     // On a full synchronization, we can go ahead and pre-parse
                     // all exceptions and then add the item at once. This way we
                     // make sure
                     for each (var oid in xml.entry.gd::originalEvent.(@id == item.id)) {
                         // Get specific fields so we can speed up the parsing process
                         var status = oid.parent().gd::eventStatus.@value.toString().substring(39);
 
                         if (status == "canceled") {
                             let rId = oid.gd::when.@startTime.toString();
                             let rDate = cal.fromRFC3339(rId, timezone);
                             item.recurrenceInfo.removeOccurrenceAt(rDate);
-                            LOG("Negative exception " + rId + "/" + rDate);
+                            cal.LOG("[calGoogleCalendar] Negative exception " + rId + "/" + rDate);
                         } else {
                             // Parse the exception and modify the current item
                             var excItem = XMLEntryToItem(oid.parent(),
                                                          timezone,
                                                          this);
                             if (excItem) {
                                 // Google uses the status field to reflect negative
                                 // exceptions.
@@ -1212,17 +1349,17 @@ calGoogleCalendar.prototype = {
                                 item.recurrenceInfo.modifyException(excItem, true);
                             }
                         }
                     }
                 }
 
                 LOGitem(item);
 
-                if (!isFullSync && item.recurrenceId && referenceItem) {
+                if (!aIsFullSync && item.recurrenceId && referenceItem) {
                     // This is a single occurrence that has been updated.
                     if (item.status == "CANCELED") {
                         // Canceled means the occurrence is an EXDATE.
                         referenceItem.recurrenceInfo.removeOccurrenceAt(item.recurrenceId);
                     } else {
                         // Not canceled means the occurrence was modified.
                         item.parentItem = referenceItem;
                         referenceItem.recurrenceInfo.modifyException(item, true);
@@ -1241,25 +1378,25 @@ calGoogleCalendar.prototype = {
                 } else {
                     // We could not find the parent item for the occurrence in
                     // the feed.
                     WARN("occurrence without parent for item "  + item.id);
                 }
             }
 
             // Set the last updated timestamp to now.
-            LOG("Last sync date for " + this.name + " is now: " +
-                aOperation.requestDate.toString());
+            cal.LOG("[calGoogleCalendar] Last sync date for " + this.name + " is now: " +
+                    aOperation.requestDate.toString());
             this.setProperty("google.lastUpdated",
                              aOperation.requestDate.icalString);
 
             // Tell our listener we are done.
             aOperation.operationListener.onResult(aOperation, null);
         } catch (e) {
-            LOG("Error syncing items:\n" + e);
+            cal.LOG("[calGoogleCalendar] Error syncing items:\n" + e);
             aOperation.operationListener.onResult({ status: e.result }, e.message);
         }
     },
 
     /**
      * Implement calISchedulingSupport. Most is taken care of by the base
      * provider, but we want to advertise that we will always take care of
      * notifications.
--- a/calendar/providers/gdata/components/calGoogleCalendarModule.js
+++ b/calendar/providers/gdata/components/calGoogleCalendarModule.js
@@ -33,19 +33,21 @@
  * the terms of any one of the MPL, the GPL or the LGPL.
  *
  * ***** END LICENSE BLOCK ***** */
 
 Components.utils.import("resource://gre/modules/Services.jsm");
 Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
 Components.utils.import("resource://calendar/modules/calUtils.jsm");
 
-// This constant is used internally to signal a failed login to the login
-// handler's response function.
+// These constants are used internally to signal errors, to avoid the need for
+// our own error range in calIErrors
 const kGOOGLE_LOGIN_FAILED = 1;
+const kGOOGLE_CONFLICT_DELETED = 2;
+const kGOOGLE_CONFLICT_MODIFY = 3
 
 /** Module Registration */
 const calendarScriptLoadOrder = [
     "calUtils.js",
 ];
 
 const gdataScriptLoadOrder = [
     "calGoogleCalendar.js",
--- a/calendar/providers/gdata/components/calGoogleRequest.js
+++ b/calendar/providers/gdata/components/calGoogleRequest.js
@@ -178,23 +178,16 @@ calGoogleRequest.prototype = {
      * The HTTP body data for a POST or PUT request.
      *
      * @param aContentType The Content type of the Data
      * @param aData        The Data to upload
      */
     setUploadData: function cGR_setUploadData(aContentType, aData) {
         this.mUploadContent = aContentType;
         this.mUploadData = aData;
-        if (this.mType == this.LOGIN) {
-            LOG("Setting upload data for login request (hidden)");
-        } else {
-            LOG({action:"Setting Upload Data:",
-                 content:aContentType,
-                 data:aData});
-        }
     },
 
     /**
      * addQueryParameter
      * Adds a query parameter to this request. This will be used in conjunction
      * with the uri.
      *
      * @param aKey      The key of the request parameter.
@@ -237,18 +230,18 @@ calGoogleRequest.prototype = {
 
             this.prepareChannel(channel);
 
             channel = channel.QueryInterface(Components.interfaces.nsIHttpChannel);
             channel.redirectionLimit = 3;
 
             this.mLoader = cal.createStreamLoader();
 
-            LOG("calGoogleRequest: Requesting " + this.method + " " +
-                channel.URI.spec);
+            cal.LOG("[calGoogleCalendar] Requesting " + this.method + " " +
+                    channel.URI.spec);
 
             channel.notificationCallbacks = this;
 
             cal.sendHttpRequest(this.mLoader, channel, this);
         } catch (e) {
             // Let the response function handle the error that happens here
             this.fail(e.result, e.message);
         }
@@ -295,16 +288,23 @@ calGoogleRequest.prototype = {
         if (this.mUploadData) {
             var converter = Components.classes["@mozilla.org/intl/scriptableunicodeconverter"].
                             createInstance(Components.interfaces.nsIScriptableUnicodeConverter);
             converter.charset = "UTF-8";
 
             var stream = converter.convertToInputStream(this.mUploadData);
             aChannel = aChannel.QueryInterface(Components.interfaces.nsIUploadChannel);
             aChannel.setUploadStream(stream, this.mUploadContent, -1);
+
+            if (this.mType == this.LOGIN) {
+                cal.LOG("[calGoogleCalendar] Setting upload data for login request (hidden)");
+            } else {
+                cal.LOG("[calGoogleCalendar] Setting Upload Data (" +
+                        this.mUploadContent + "):\n" + this.mUploadData);
+            }
         }
 
         aChannel  = aChannel.QueryInterface(Components.interfaces.nsIHttpChannel);
 
         // Depending on the preference, we will use X-HTTP-Method-Override to
         // get around some proxies. This will default to true.
         if (getPrefSafe("calendar.google.useHTTPMethodOverride", true) &&
             (this.method == "PUT" || this.method == "DELETE")) {
@@ -393,18 +393,18 @@ calGoogleRequest.prototype = {
             case 201: /* Creation of a resource was successful. */
                 // Everything worked out, we are done
                 this.succeed(result);
                 break;
 
             case 401: /* Authorization required. */
             case 403: /* Unsupported standard parameter, or authentication or
                          Authorization failed. */
-                LOG("Login failed for " + this.mSession.userName +
-                    " HTTP Status " + httpChannel.responseStatus );
+                cal.LOG("[calGoogleCalendar] Login failed for " + this.mSession.userName +
+                        " HTTP Status " + httpChannel.responseStatus);
 
                 // login failed. auth token must be invalid, password too
 
                 if (this.type == this.MODIFY ||
                     this.type == this.DELETE ||
                     this.type == this.ADD) {
                     // Encountering this error on a write request means the
                     // calendar is readonly
@@ -421,39 +421,47 @@ calGoogleRequest.prototype = {
                 } else {
                     // Retry the request. Invalidating the session will trigger
                     // a new login dialog.
                     this.mSession.invalidate();
                     this.mSession.asyncItemRequest(this);
                 }
 
                 break;
+            case 404: /* The resource was not found on the server, which is
+                         also a conflict */
+                //  404 NOT FOUND: Resource (such as a feed or entry) not found.
+                this.fail(kGOOGLE_CONFLICT_DELETED, "");
+                break;
             case 409: /* Specified version number doesn't match resource's
                          latest version number. */
+                this.fail(kGOOGLE_CONFLICT_MODIFY, result);
+                break;
+            case 400:
+                //  400 BAD REQUEST: Invalid request URI or header, or
+                //                   unsupported nonstandard parameter.
 
-                // 409 Conflict. The client should get a newer version of the
-                // event
-                // and edit that one.
+                // HACK Ugh, this sucks. If we send a lower sequence number, Google
+                // will throw a 400 and not a 409, even though both cases are a conflict.
+                if (result == "Cannot decrease the sequence number of an event") {
+                    this.fail(kGOOGLE_CONFLICT_MODIFY, "SEQUENCE-HACK");
+                    break;
+                }
 
-                // TODO Enhancement tracked in bug 362645
-                // Fall through, if 409 is not handled then the event is not
-                // modified/deleted, which is definitly an error.
+                // Otherwise fall through
             default:
                 // The following codes are caught here:
-                //  400 BAD REQUEST: Invalid request URI or header, or
-                //                   unsupported nonstandard parameter.
-                //  404 NOT FOUND: Resource (such as a feed or entry) not found.
                 //  500 INTERNAL SERVER ERROR: Internal error. This is the
                 //                             default code that is used for
                 //                             all unrecognized errors.
                 //
 
                 // Something else went wrong
                 var error = "A request Error Occurred. Status Code: " +
                             httpChannel.responseStatus + " " +
                             httpChannel.responseStatusText + " Body: " +
                             result;
 
-                this.fail(Components.results.NS_ERROR_FAILURE, error);
+                this.fail(Components.results.NS_ERROR_NOT_AVAILABLE, error);
                 break;
         }
     }
 };
--- a/calendar/providers/gdata/components/calGoogleSession.js
+++ b/calendar/providers/gdata/components/calGoogleSession.js
@@ -89,20 +89,20 @@ calGoogleSessionManager.prototype = {
             aUsername += "@gmail.com";
         }
 
         // Check if the session exists
         if (!this.mSessionMap.hasOwnProperty(aUsername)) {
             if (!aCreate) {
                 return null;
             }
-            LOG("Creating session for: " + aUsername);
+            cal.LOG("[calGoogleCalendar] Creating session for: " + aUsername);
             this.mSessionMap[aUsername] = new calGoogleSession(aUsername);
         } else {
-            LOG("Reusing session for: " + aUsername);
+            cal.LOG("[calGoogleCalendar] Reusing session for: " + aUsername);
         }
 
         // XXX What happens if the username is "toSource" :)
         return this.mSessionMap[aUsername];
     }
 };
 
 /**
@@ -256,21 +256,21 @@ calGoogleSession.prototype = {
      * loginAndContinue
      * Prepares a login request, then requests via #asyncRawRequest
      *
      *
      * @param aCalendar    The calendar of the request that initiated the login.
      */
     loginAndContinue: function cGS_loginAndContinue(aCalendar) {
         if (this.mLoggingIn) {
-            LOG("loginAndContinue called while logging in");
+            cal.LOG("[calGoogleCalendar] loginAndContinue called while logging in");
             return;
         }
         try {
-            LOG("Logging in to " + this.mGoogleUser);
+            cal.LOG("[calGoogleCalendar] Logging in to " + this.mGoogleUser);
 
             // We need to have a user and should not be logging in
             ASSERT(!this.mLoggingIn);
             ASSERT(this.mGoogleUser);
 
             // Start logging in
             this.mLoggingIn = true;
 
@@ -305,42 +305,42 @@ calGoogleSession.prototype = {
                                     aCalendar.googleCalendarName :
                                     this.mGoogleUser);
 
                 if (getCalendarCredentials(calendarName,
                                            username,
                                            password,
                                            persist)) {
 
-                    LOG("Got the pw from the calendar credentials: " +
-                        calendarName);
+                    cal.LOG("[calGoogleCalendar] Got the pw from the calendar credentials: " +
+                            calendarName);
 
                     // If a different username was entered, switch sessions
 
                     if (aCalendar && username.value != this.mGoogleUser) {
                         var newSession = getGoogleSessionManager()
                                          .getSessionByUsername(username.value,
                                                                true);
                         newSession.password = password.value;
                         newSession.persist = persist.value;
                         setCalendarPref(aCalendar,
                                         "googleUser",
                                         "CHAR",
                                         username.value);
 
                         // Set the new session for the calendar
                         aCalendar.session = newSession;
-                        LOG("Setting " + aCalendar.name +
-                            "'s Session to " + newSession.userName);
+                        cal.LOG("[calGoogleCalendar] Setting " + aCalendar.name +
+                                "'s Session to " + newSession.userName);
 
                         // Move all requests by this calendar to its new session
                         function cGS_login_moveToSession(element, index, arr) {
                             if (element.calendar == aCalendar) {
-                                LOG("Moving " + element.uri + " to " +
-                                    newSession.userName);
+                                cal.LOG("[calGoogleCalendar] Moving " + element.uri + " to " +
+                                        newSession.userName);
                                 newSession.asyncItemRequest(element);
                                 return false;
                             }
                             return true;
                         }
                         this.mItemQueue = this.mItemQueue
                                           .filter(cGS_login_moveToSession);
 
@@ -351,19 +351,19 @@ calGoogleSession.prototype = {
                     }
 
                     // If we arrive at this point, then the session was not
                     // changed. Just adapt the password from the dialog and
                     // continue.
                     this.mGooglePass = password.value;
                     this.persist = persist.value;
                 } else {
-                    LOG("Could not get any credentials for " +
-                        calendarName + " (" +
-                        this.mGoogleUser + ")");
+                    cal.LOG("[calGoogleCalendar] Could not get any credentials for " +
+                            calendarName + " (" +
+                            this.mGoogleUser + ")");
 
                     if (aCalendar) {
                         // First of all, disable the calendar so no further login
                         // dialogs show up.
                         aCalendar.setProperty("disabled", true);
                         aCalendar.setProperty("auto-enabled", true);
 
                         // Unset the session in the requesting calendar, if the user
@@ -410,17 +410,17 @@ calGoogleSession.prototype = {
                                   "&Passwd=" + encodeURIComponent(this.mGooglePass) +
                                   "&accountType=HOSTED_OR_GOOGLE" +
                                   "&source=" + encodeURIComponent(source) +
                                   "&service=cl");
             this.asyncRawRequest(request);
         } catch (e) {
             // If something went wrong, reset the login state just in case
             this.mLoggingIn = false;
-            LOG("Error Logging In: " + e);
+            cal.LOG("[calGoogleCalendar] Error Logging In: " + e);
 
             // If something went wrong, then this.loginComplete should handle
             // the error. We don't need to take care of the request that
             // initiated the login, since it is also in the item queue.
             this.onResult({ status: e.result}, e.message);
         }
     },
 
@@ -440,25 +440,25 @@ calGoogleSession.prototype = {
      onResult: function cGS_onResult(aOperation, aData) {
         // About mLoggingIn: this should only be set to false when either
         // something went wrong or mAuthToken is set. This avoids redundant
         // logins to Google. Hence mLoggingIn is set three times in the course
         // of this function
 
         if (!aData || !Components.isSuccessCode(aOperation.status)) {
             this.mLoggingIn = false;
-            LOG("Login failed. Status: " + aOperation.status);
+            cal.LOG("[calGoogleCalendar] Login failed. Status: " + aOperation.status);
 
             if (aOperation.status == kGOOGLE_LOGIN_FAILED &&
                 aOperation.reauthenticate) {
                 // If the login failed, then retry the login. This is not an
                 // error that should trigger failing the calICalendar's request.
                 this.loginAndContinue(aOperation.calendar);
             } else {
-                LOG("Failing queue with " + aOperation.status);
+                cal.LOG("[calGoogleCalendar] Failing queue with " + aOperation.status);
                 this.failQueue(aOperation.status);
             }
         } else {
             var start = aData.indexOf("Auth=");
             if (start == -1) {
                 // The Auth token could not be extracted
                 this.mLoggingIn = false;
                 this.invalidate();
@@ -472,25 +472,25 @@ calGoogleSession.prototype = {
 
                 if (this.persist) {
                     try {
                         passwordManagerSave(this.mGoogleUser,
                                             this.mGooglePass);
                     } catch (e) {
                         // This error is non-fatal, but would constrict
                         // functionality
-                        LOG("Error adding password to manager");
+                        cal.LOG("[calGoogleCalendar] Error adding password to manager");
                     }
                 }
 
                 // Process Items that were requested while logging in
                 var request;
                 // Extra parentheses to avoid js strict warning.
                 while ((request = this.mItemQueue.shift())) {
-                    LOG("Processing Queue Item: " + request.uri);
+                    cal.LOG("[calGoogleCalendar] Processing Queue Item: " + request.uri);
                     request.commit(this);
                 }
             }
         }
     },
 
     /**
      * asyncItemRequest
@@ -504,17 +504,17 @@ calGoogleSession.prototype = {
         if (!this.mLoggingIn && this.mAuthToken) {
             // We are not currently logging in and we have an auth token, so
             // directly try the login request
             this.asyncRawRequest(aRequest);
         } else {
             // Push the request in the queue to be executed later
             this.mItemQueue.push(aRequest);
 
-            LOG("Adding item " + aRequest.uri + " to queue");
+            cal.LOG("[calGoogleCalendar] Adding item " + aRequest.uri + " to queue");
 
             // If we are logging in, then we are done since the passed request
             // will be processed when the login is complete. Otherwise start
             // logging in.
             if (!this.mLoggingIn && this.mAuthToken == null) {
                 // We need to do this on a timeout, otherwise the UI thread will
                 // block when the password prompt is shown.
                 setTimeout(function() {
--- a/calendar/providers/gdata/components/calGoogleUtils.js
+++ b/calendar/providers/gdata/components/calGoogleUtils.js
@@ -214,16 +214,134 @@ function passwordManagerGet(aUsername, a
  * @param aUsername     The username to remove.
  * @return              Could the user be removed?
  */
 function passwordManagerRemove(aUsername) {
     return cal.auth.passwordManagerRemove(aUsername, aUsername, "Google Calendar");
 }
 
 /**
+ * If the operation has signaled that a conflict occurred, then prompt the user
+ * to overwrite. If the user chooses to overwrite, restart the request with the
+ * right parameters so the request succeeds.
+ *
+ * @param aOperation        The operation to check
+ * @param aItem             The updated item from the response
+ * @return                  False if further processing should be cancelled
+ */
+function resolveConflicts(aOperation, aItem) {
+    if (aItem && (aOperation.status == kGOOGLE_CONFLICT_DELETED ||
+                  aOperation.status == kGOOGLE_CONFLICT_MODIFY)) {
+        if (aItem == "SEQUENCE-HACK") {
+            // Working around a Google issue here, see what happens on a 400
+            // code in calGoogleRequest.js. This will cause a new request
+            // without the sequence number. In return, we get a new item with
+            // the correct sequence number.
+            let newItem =  aOperation.newItem.clone();
+            let session = aOperation.calendar.session;
+            newItem.deleteProperty("SEQUENCE");
+            let xmlEntry = ItemToXMLEntry(newItem, aOperation.calendar,
+                                          session.userName, session.fullName);
+
+            aOperation.newItem = newItem;
+            aOperation.setUploadData("application/atom+xml; charset=UTF-8", xmlEntry);
+            session.asyncItemRequest(aOperation);
+            return false;
+        } else if (aOperation.status == kGOOGLE_CONFLICT_DELETED &&
+                   aOperation.type == aOperation.DELETE) {
+            // Deleted on the server and deleted locally. Great!
+            return true;
+        } else {
+            // If a conflict occurred, then prompt
+            let method = (aOperation.type == aOperation.DELETE ? "delete" : "modify")
+            let inputItem = aOperation.oldItem || aOperation.newItem;
+            let overwrite = cal.promptOverwrite(method, inputItem);
+            if (overwrite) {
+                if (aOperation.status == kGOOGLE_CONFLICT_DELETED &&
+                    aOperation.type == aOperation.MODIFY) {
+                    // The item was deleted on the server, but modified locally.
+                    // Add it again
+                    aOperation.type = aOperation.ADD;
+                    aOperation.uri = aOperation.calendar.fullUri.spec;
+                    aOperation.calendar.session.asyncItemRequest(aOperation);
+                    return false;
+                } else if (aOperation.status == kGOOGLE_CONFLICT_MODIFY &&
+                           aOperation.type == aOperation.MODIFY) {
+                    // The item was modified in both places, repeat the current
+                    // request with the edit uri of the updated event
+                    aOperation.uri = getItemEditURI(aItem);
+                    aOperation.calendar.session.asyncItemRequest(aOperation);
+                    return false;
+                } else if (aOperation.status == kGOOGLE_CONFLICT_MODIFY &&
+                           aOperation.type == aOperation.DELETE) {
+                    // Modified on the server, deleted locally. Just repeat the
+                    // delete request with the updated edit uri.
+                    aOperation.uri = getItemEditURI(aItem);
+                    aOperation.calendar.session.asyncItemRequest(aOperation);
+                    return false;
+                }
+            }
+        }
+        // Otherwise, we can just continue using the item that was parsed, it
+        // is the newest version on the server.
+    }
+    return true;
+}
+
+/**
+ * Helper function to convert raw data directly into a calIItemBase. If the
+ * passed operation signals an error, then throw an exception
+ *
+ * @param aOperation        The operation to check for errors
+ * @param aData             The result from the response
+ * @param aGoogleCalendar   The calIGoogleCalendar to operate on
+ * @param aReferenceItem    The reference item to apply the information to
+ * @return                  The parsed item
+ * @throws                  An exception on a parsing or request error
+ */
+function DataToItem(aOperation, aData, aGoogleCalendar, aReferenceItem) {
+    if (aOperation.status == kGOOGLE_CONFLICT_DELETED ||
+        aOperation.status == kGOOGLE_CONFLICT_MODIFY ||
+        Components.isSuccessCode(aOperation.status)) {
+
+        let item;
+        if (aData == "SEQUENCE-HACK") {
+            // Working around a Google issue here, see what happens on a 400
+            // code in calGoogleRequest.js. This will be processed in
+            // resolveConflicts().
+            return "SEQUENCE-HACK";
+        }
+
+        if (aData && aData.length) {
+            let xml = cal.safeNewXML(aData);
+            cal.LOG("[calGoogleCalendar] Parsing entry:\n" + xml + "\n");
+
+            // Get the local timezone from the preferences
+            let timezone = calendarDefaultTimezone();
+
+            // Parse the Item with the given timezone
+            item = XMLEntryToItem(xml, timezone,
+                                  aGoogleCalendar,
+                                  aReferenceItem);
+        } else {
+            cal.LOG("[calGoogleCalendar] No content, using reference item instead ");
+            // No data happens for example on delete. Just assume the reference
+            // item.
+            item = aReferenceItem.clone();
+        }
+
+        LOGitem(item);
+        item.calendar = aGoogleCalendar.superCalendar;
+        return item;
+    } else {
+        throw new Components.Exception(aData, aOperation.status);
+    }
+}
+
+/**
  * ItemToXMLEntry
  * Converts a calIEvent to a string of xml data.
  *
  * @param aItem         The item to convert
  * @param aCalendar     The calendar to use, this must be a calIGoogleCalendar
  * @param aAuthorEmail  The email of the author of the event
  * @param aAuthorName   The full name of the author of the event
  * @return              The xml data of the item
@@ -452,18 +570,16 @@ function ItemToXMLEntry(aItem, aCalendar
 
     // gd:visibility
     var privacy = aItem.privacy || "default";
     entry.gd::visibility.@value = kEVENT_SCHEMA + privacy.toLowerCase();
 
     // categories
     // Google does not support categories natively, but allows us to store data
     // as an "extendedProperty", so we do here
-    cal.WARN("CAT: " + aItem.getCategories({}).toSource() + " = " +
-             categoriesArrayToString(aItem.getCategories({})));
     addExtendedProperty("X-MOZ-CATEGORIES",
                         categoriesArrayToString(aItem.getCategories({})));
 
     // gd:recurrence
     if (aItem.recurrenceInfo) {
         try {
             const kNEWLINE = "\r\n";
             var icalString;
@@ -487,30 +603,33 @@ function ItemToXMLEntry(aItem, aCalendar
                     prop.valueAsDatetime = ritem.date.getInTimezone(UTC());
                 }
                 icalString += prop.icalString;
             }
 
             // Put the ical string in a <gd:recurrence> tag
             entry.gd::recurrence = icalString + kNEWLINE;
         } catch (e) {
-            LOG("Error: " + e);
+            cal.ERROR("[calGoogleCalendar] Error: " + e);
         }
     }
 
     // gd:originalEvent
     if (aItem.recurrenceId) {
         entry.gd::originalEvent.@id = aItem.parentItem.id;
         entry.gd::originalEvent.gd::when.@startTime =
             cal.toRFC3339(aItem.recurrenceId.getInTimezone(UTC()));
     }
 
     // While it may sometimes not work out, we can always try to set the uid and
     // sequence properties
-    entry.gCal::sequence.@value = aItem.getProperty("SEQUENCE") || 0;
+    let sequence = aItem.getProperty("SEQUENCE");
+    if (sequence) {
+        entry.gCal::sequence.@value = sequence;
+    }
     entry.gCal::uid.@value = aItem.id || "";
 
     // TODO gd:comments: Enhancement tracked in bug 362653
 
     // XXX Google currently has no priority support. See
     // http://code.google.com/p/google-gdata/issues/detail?id=52
     // for details.
 
@@ -653,17 +772,17 @@ function relevantFieldsMatch(a, b) {
  * @return              The edit URI
  */
 function getItemEditURI(aItem) {
 
     ASSERT(aItem);
     var edituri = aItem.getProperty("X-GOOGLE-EDITURL");
     if (!edituri) {
         // If the item has no edit uri, it is read-only
-        throw new Components.Exception("", Components.interfaces.calIErrors.CAL_IS_READONLY);
+        throw new Components.Exception("The item is readonly", Components.interfaces.calIErrors.CAL_IS_READONLY);
     }
     return edituri;
 }
 
 function getIdFromEntry(aXMLEntry) {
     var gCal = new Namespace("gCal", "http://schemas.google.com/gCal/2005");
     var gd = new Namespace("gd", "http://schemas.google.com/g/2005");
     var id = aXMLEntry.gCal::uid.@value.toString();
@@ -1021,29 +1140,59 @@ function XMLEntryToItem(aXMLEntry, aTime
  * Otherwise returns the item in an array.
  *
  * @param aItem         The item to expand
  * @param aOperation    The calIGoogleRequest that contains the filter and
  *                        ranges.
  * @return              The (possibly expanded) items in an array.
  */
 function expandItems(aItem, aOperation) {
-    var expandedItems;
+    let expandedItems;
     if (aOperation.itemFilter &
         Components.interfaces.calICalendar.ITEM_FILTER_CLASS_OCCURRENCES) {
         expandedItems = aItem.getOccurrencesBetween(aOperation.itemRangeStart,
                                                     aOperation.itemRangeEnd,
                                                     {});
-        LOG("Expanded item " + aItem.title + " to " +
-            expandedItems.length + " items");
+        cal.LOG("[calGoogleCalendar] Expanded item " + aItem.title + " to " +
+                expandedItems.length + " items");
     }
     return expandedItems || [aItem];
 }
 
 /**
+ * Returns true if the exception passed is one that should cause the cache
+ * layer to retry the operation. This is usually a network error or other
+ * temporary error
+ *
+ * @param e     The exception to check
+ */
+function isCacheException(e) {
+    // Stolen from nserror.h
+    const NS_ERROR_MODULE_NETWORK = 6;
+    function NS_ERROR_GET_MODULE(code) {
+        return (((code >> 16) - 0x45) & 0x1fff);
+    }
+
+    if (NS_ERROR_GET_MODULE(e.result) == NS_ERROR_MODULE_NETWORK &&
+        !Components.isSuccessCode(e.result)) {
+        // This is a network error, which most likely means we should
+        // retry it some time.
+        return true;
+    }
+
+    // Other potential errors we want to retry with
+    switch (e.result) {
+        case Components.results.NS_ERROR_NOT_AVAILABLE:
+            return true;
+        default:
+            return false;
+    }
+}
+
+/**
  * Helper prototype to set a certain variable to the first item passed via get
  * listener. Cleans up code.
  */
 function syncSetter(aObj) {
     this.mObj = aObj
 }
 syncSetter.prototype = {
 
@@ -1118,29 +1267,30 @@ function LOGitem(item) {
 
     let astr = "\n";
     let alarms = item.getAlarms({});
     for each (let alarm in alarms) {
         astr += "\t\t" + LOGalarm(alarm) + "\n";
     }
 
 
-    LOG("Logging calIEvent:" +
+    cal.LOG("[calGoogleCalendar] Logging calIEvent:" +
         "\n\tid:" + item.id +
         "\n\tediturl:" + item.getProperty("X-GOOGLE-EDITURL") +
         "\n\tcreated:" + item.getProperty("CREATED") +
         "\n\tupdated:" + item.getProperty("LAST-MODIFIED") +
         "\n\ttitle:" + item.title +
         "\n\tcontent:" + item.getProperty("DESCRIPTION") +
         "\n\ttransparency:" + item.getProperty("TRANSP") +
         "\n\tstatus:" + item.status +
         "\n\tstartTime:" + item.startDate.toString() +
         "\n\tendTime:" + item.endDate.toString() +
         "\n\tlocation:" + item.getProperty("LOCATION") +
         "\n\tprivacy:" + item.privacy +
+        "\n\tsequence:" + item.getProperty("SEQUENCE") +
         "\n\talarmLastAck:" + item.alarmLastAck +
         "\n\tsnoozeTime:" + item.getProperty("X-MOZ-SNOOZE-TIME") +
         "\n\tisOccurrence: " + (item.recurrenceId != null) +
         "\n\tOrganizer: " + LOGattendee(item.organizer) +
         "\n\tAttendees: " + attendeeString +
         "\n\trecurrence: " + (rstr.length > 1 ? "yes: " + rstr : "no") +
         "\n\talarms: " + (astr.length > 1 ? "yes: " + astr : "no"));
 }
@@ -1184,11 +1334,12 @@ function LOGinterval(aInterval) {
     if (aInterval.freeBusyType == fbtypes.FREE) {
         type = "FREE";
     } else if (aInterval.freeBusyType == fbtypes.BUSY) {
         type = "BUSY";
     } else {
         type = aInterval.freeBusyType + "(UNKNOWN)";
     }
 
-    LOG("Interval from " + aInterval.interval.start + " to "
-                         + aInterval.interval.end + " is " + type);
+    cal.LOG("[calGoogleCalendar] Interval from " +
+            aInterval.interval.start + " to " + aInterval.interval.end +
+            " is " + type);
 }
--- a/calendar/providers/gdata/public/calIGoogleRequest.idl
+++ b/calendar/providers/gdata/public/calIGoogleRequest.idl
@@ -102,16 +102,17 @@ interface calIGoogleRequest : calIOperat
      * XXX The corresponding options are not set up automatically just by
      * setting these options. You still need to use addQueryParameter to filter
      * by item range or other property.
      */
     attribute calIDateTime itemRangeStart;
     attribute calIDateTime itemRangeEnd;
     attribute unsigned long itemFilter;
     attribute AUTF8String itemId;
+    attribute boolean useCache;
 
     /**
      * For add/modify/delete item requests, these contain the old and new items.
      */
     attribute calIItemBase newItem;
     attribute calIItemBase oldItem;
 
     /**