diff --git a/resources/google/calendar/CMakeLists.txt b/resources/google/calendar/CMakeLists.txt
index 5879cb1..fae4b16 100644
--- a/resources/google/calendar/CMakeLists.txt
+++ b/resources/google/calendar/CMakeLists.txt
@@ -6,6 +6,7 @@ set(calendarresource_SRCS
   calendareditor.cpp
   calendarresource.cpp
   defaultreminderattribute.cpp
+  recurrencehackattribute.cpp
   resource_tasks.cpp
   resource_events.cpp
   settings.cpp
diff --git a/resources/google/calendar/calendarresource.cpp b/resources/google/calendar/calendarresource.cpp
index db33275..f00d89c 100644
--- a/resources/google/calendar/calendarresource.cpp
+++ b/resources/google/calendar/calendarresource.cpp
@@ -19,6 +19,7 @@
 #include "defaultreminderattribute.h"
 #include "settings.h"
 #include "settingsdialog.h"
+#include "recurrencehackattribute.h"
 
 #include <Akonadi/Attribute>
 #include <Akonadi/AttributeFactory>
@@ -29,6 +30,8 @@
 #include <Akonadi/ItemFetchJob>
 #include <Akonadi/ItemFetchScope>
 #include <KCalCore/Calendar>
+#include <KCalCore/Recurrence>
+#include <KCalCore/RecurrenceRule>
 
 #include <KLocalizedString>
 #include <KDialog>
@@ -54,6 +57,11 @@ using namespace KCalCore;
 using namespace Akonadi;
 using namespace KGAPI;
 
+uint qHash( const QDate &date )
+{
+    return (date.year() * 10000) + (date.month() * 100) + date.day();
+}
+
 CalendarResource::CalendarResource( const QString &id ):
   ResourceBase( id ),
   m_account( 0 ),
@@ -63,6 +71,7 @@ CalendarResource::CalendarResource( const QString &id ):
   qRegisterMetaType< KGAPI::Services::Calendar >( "Calendar" );
   qRegisterMetaType< KGAPI::Services::Tasks >( "Tasks" );
   AttributeFactory::registerAttribute< DefaultReminderAttribute >();
+  AttributeFactory::registerAttribute< RecurrenceHackAttribute >();
 
   Auth *auth = Auth::instance();
   auth->init( "Akonadi Google", Settings::self()->clientId(), Settings::self()->clientSecret() );
@@ -83,6 +92,7 @@ CalendarResource::CalendarResource( const QString &id ):
 
   changeRecorder()->itemFetchScope().fetchFullPayload( true );
   changeRecorder()->itemFetchScope().setAncestorRetrieval( ItemFetchScope::All );
+  changeRecorder()->itemFetchScope().fetchAllAttributes( true );
   changeRecorder()->fetchCollection( true );
   changeRecorder()->collectionFetchScope().setAncestorRetrieval( CollectionFetchScope::All );
 
@@ -182,7 +192,8 @@ void CalendarResource::retrieveItems( const Akonadi::Collection &collection )
     connect( fetchJob, SIGNAL(finished(KJob*)),
              fetchJob, SLOT(deleteLater()) );
 
-    fetchJob->fetchScope().fetchFullPayload( false );
+    fetchJob->fetchScope().fetchFullPayload( true );
+    fetchJob->fetchScope().fetchAttribute< RecurrenceHackAttribute >( true );
     fetchJob->setProperty( "collection", qVariantFromValue( collection ) );
     fetchJob->start();
 
@@ -198,7 +209,9 @@ void CalendarResource::cachedItemsRetrieved( KJob *job )
   QString service;
   QString lastSync;
 
-  Collection collection = job->property( "collection" ).value<Collection>();
+  ItemFetchJob *cacheFetchJob = qobject_cast< ItemFetchJob* >( job );
+  Collection collection = cacheFetchJob ->property( "collection" ).value<Collection>();
+  Item::List items = cacheFetchJob ->items();
 
   if ( collection.contentMimeTypes().contains( Event::eventMimeType() ) ) {
 
@@ -236,6 +249,7 @@ void CalendarResource::cachedItemsRetrieved( KJob *job )
 
   FetchListJob *fetchJob = new FetchListJob( url, service, account->accountName() );
   fetchJob->setProperty( "collection", qVariantFromValue( collection ) );
+  fetchJob->setProperty( "existingItems", qVariantFromValue( items ) );
   connect( fetchJob, SIGNAL(finished(KJob*)),
            this, SLOT(itemsReceived(KJob*)) );
   connect( fetchJob, SIGNAL(percent(KJob*,ulong)),
@@ -397,6 +411,13 @@ void CalendarResource::itemChanged( const Akonadi::Item &item,
      * well with it (see bug #298518). */
     kevent.setUid( item.remoteId() );
 
+    if ( item.hasAttribute< RecurrenceHackAttribute >() ) {
+        RecurrenceHackAttribute *attr = item.attribute< RecurrenceHackAttribute >();
+
+        /* Remove all manually added exception dates */
+        removeExDates( kevent.recurrence(), attr->exceptionDates() );
+    }
+
     url = Services::Calendar::updateEventUrl( item.parentCollection().remoteId(), item.remoteId() );
 
     Services::Calendar service;
@@ -433,6 +454,49 @@ void CalendarResource::itemChanged( const Akonadi::Item &item,
   Q_UNUSED( partIdentifiers );
 }
 
+void CalendarResource::removeExDates( Recurrence* recurrence, const QList< QDate > &exDates_ )
+{
+    /* We only need to remove the exDates from the recurrence. Unfortunatelly
+     * there is no KCalCore::Recurrence::removeExDate() method, so we have
+     * copy all rules. exRules, dates and exDates, clear the whole recurrence
+     * and then copy it all back, skipping the unwanted exDates */
+
+    if ( exDates_.isEmpty() ) {
+        return;
+    }
+
+    QSet<QDate> exDatesToRemove = exDates_.toSet();
+    QSet<QDate> exDates = recurrence->exDates().toSet();
+    exDates = exDates.subtract( exDatesToRemove );
+
+    QList<RecurrenceRule> rRules;
+    Q_FOREACH(const RecurrenceRule *rRule, recurrence->rRules()) {
+        rRules << *rRule;
+    }
+    QList<RecurrenceRule> exRules;
+    Q_FOREACH(const RecurrenceRule *exRule, recurrence->exRules()) {
+        exRules << *exRule;
+    }
+
+    QList<KDateTime> exDateTimes = recurrence->exDateTimes();
+    QList<QDate> rDates = recurrence->rDates();
+    QList<KDateTime> rDateTimes = recurrence->rDateTimes();
+
+    recurrence->clear();
+
+    recurrence->setRDates( rDates );
+    recurrence->setRDateTimes( rDateTimes );
+    recurrence->setExDates( exDates.toList() );
+    recurrence->setExDateTimes( exDateTimes );
+
+    Q_FOREACH( const RecurrenceRule &rRule, rRules ) {
+        recurrence->addRRule( new RecurrenceRule( rRule ) );
+    }
+    Q_FOREACH( const RecurrenceRule &exRule, exRules ) {
+        recurrence->addExRule( new RecurrenceRule( exRule ) );
+    }
+}
+
 void CalendarResource::itemRemoved( const Akonadi::Item &item )
 {
   QString service;
diff --git a/resources/google/calendar/calendarresource.h b/resources/google/calendar/calendarresource.h
index 46af9ba..a664f74 100644
--- a/resources/google/calendar/calendarresource.h
+++ b/resources/google/calendar/calendarresource.h
@@ -26,6 +26,10 @@
 #include <libkgapi/common.h>
 #include <libkgapi/account.h>
 
+namespace KCalCore {
+class Recurrence;
+}
+
 namespace KGAPI {
   class AccessManager;
   class Account;
@@ -101,6 +105,7 @@ class CalendarResource : public Akonadi::ResourceBase, public Akonadi::AgentBase
   private:
     void abort();
     Account::Ptr getAccount();
+    void removeExDates( KCalCore::Recurrence *recurrence, const QList<QDate> &exDates );
 
     AccessManager *m_gam;
 
diff --git a/resources/google/calendar/recurrencehackattribute.cpp b/resources/google/calendar/recurrencehackattribute.cpp
new file mode 100644
index 0000000..4f5ff92
--- /dev/null
+++ b/resources/google/calendar/recurrencehackattribute.cpp
@@ -0,0 +1,111 @@
+/*
+    Copyright (C) 2013  Dan Vratil <dan@progdan.cz>
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+#include "recurrencehackattribute.h"
+
+#include <KCalCore/RecurrenceRule>
+#include <KCalCore/ICalFormat>
+#include <KDebug>
+#include <QBuffer>
+#include <kcal/alarm.h>
+
+#define STREAM_VERSION 1
+
+RecurrenceHackAttribute::RecurrenceHackAttribute():
+    Akonadi::Attribute()
+{
+}
+
+RecurrenceHackAttribute::~RecurrenceHackAttribute()
+{
+}
+
+void RecurrenceHackAttribute::setOriginalUID( const QString& uid )
+{
+    m_originalUID = uid;
+}
+
+QString RecurrenceHackAttribute::originalUID() const
+{
+    return m_originalUID;
+}
+
+void RecurrenceHackAttribute::addExceptionDate( const QDate& date )
+{
+    if ( !m_dates.contains( date ) ) {
+        m_dates << date;
+    }
+}
+
+void RecurrenceHackAttribute::setExceptionDates( const QList< QDate > &dates )
+{
+    m_dates = dates;
+}
+
+QList< QDate > RecurrenceHackAttribute::exceptionDates() const
+{
+    return m_dates;
+}
+
+QByteArray RecurrenceHackAttribute::type() const
+{
+    return "RecurrenceHackAttribute";
+}
+
+Akonadi::Attribute* RecurrenceHackAttribute::clone() const
+{
+    RecurrenceHackAttribute *attr = new RecurrenceHackAttribute();
+    attr->setOriginalUID( m_originalUID );
+    attr->setExceptionDates( m_dates );
+
+    return attr;
+}
+
+void RecurrenceHackAttribute::deserialize(const QByteArray& data)
+{
+    QStringList list = QString( data ).split( ',' );
+
+    if (list.length() < 2) {
+        kWarning() << "Invalid data!";
+        return;
+    }
+
+    if ( list.at( 0 ) != QString::number( STREAM_VERSION )) {
+        kWarning() << "Invalid data version";
+        return;
+    }
+
+    m_originalUID = list.at( 1 );
+
+    for (int i = 2; i < list.length(); ++i) {
+        m_dates << QDate::fromString( list.at( i ), Qt::ISODate);
+    }
+}
+
+QByteArray RecurrenceHackAttribute::serialized() const
+{
+    QStringList data;
+
+    data << QString::number( STREAM_VERSION );
+    data << m_originalUID;
+
+    Q_FOREACH( const QDate &date, m_dates ) {
+        data << date.toString( Qt::ISODate );
+    }
+
+    return data.join( "," ).toUtf8();
+}
diff --git a/resources/google/calendar/recurrencehackattribute.h b/resources/google/calendar/recurrencehackattribute.h
new file mode 100644
index 0000000..0b2fcf2
--- /dev/null
+++ b/resources/google/calendar/recurrencehackattribute.h
@@ -0,0 +1,72 @@
+/*
+    Copyright (C) 2013  Dan Vratil <dan@progdan.cz>
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+#ifndef RECURRENCEHACKATTRIBUTE_H
+#define RECURRENCEHACKATTRIBUTE_H
+
+#include <Akonadi/Attribute>
+#include <QString>
+#include <KCalCore/RecurrenceRule>
+
+/**
+ * This attribute stores original event UID and list of it's recurrence exceptions.
+ *
+ * Situation: Have a recurrent event with one recurrence that lasts two hours
+ * instead of one hour. In iCal this is represented by having two events (VEVENT):
+ * the original recurrent event and a second event with different DTEND. Both
+ * events have the same UID (because they represent a single series of events).
+ *
+ * According to RFC it's OK for two events to have the same UID, unfortunatelly
+ * KCalCore does not handle it and one of the events is not displayed in KOrganizer.
+ * To hack around this (until we fix KCalCore), we generate a random UID for
+ * each event and store it's original UID in this attribute.
+ *
+ * We also add the changed instance as an exception to recurrence of the original
+ * recurrent event, because KCalCore would otherwise show the event twice - once
+ * the recurring event and once the changed instance. This is a hack, because
+ * the changed instance is not an exception to the recurrent event, it still
+ * represent a single recurrence in the whole series, it just has different content.
+ * In real world, the exceptions are used only to represent a cancelled recurrence.
+ *
+ * Before uploading the event to Google, we need to remove these manually added
+ * exceptions, but keep the ones that have already been there when synced from
+ * Google, so we store the original exceptions in this atribute.
+ */
+class RecurrenceHackAttribute : public Akonadi::Attribute
+{
+  public:
+    RecurrenceHackAttribute();
+    virtual ~RecurrenceHackAttribute();
+
+    void setOriginalUID( const QString &uid );
+    QString originalUID() const;
+
+    void addExceptionDate( const QDate &date );
+    void setExceptionDates( const QList<QDate> &dates );
+    QList<QDate> exceptionDates() const;
+
+    virtual Attribute* clone() const;
+    virtual QByteArray type() const;
+    virtual void deserialize( const QByteArray& data );
+    virtual QByteArray serialized() const;
+
+  private:
+    QString m_originalUID;
+    QList<QDate> m_dates;
+};
+
+#endif // RECURRENCEHACKATTRIBUTE_H
diff --git a/resources/google/calendar/resource_events.cpp b/resources/google/calendar/resource_events.cpp
index df821dc..07b3b09 100644
--- a/resources/google/calendar/resource_events.cpp
+++ b/resources/google/calendar/resource_events.cpp
@@ -17,6 +17,7 @@
 
 #include "calendarresource.h"
 #include "defaultreminderattribute.h"
+#include "recurrencehackattribute.h"
 #include "settings.h"
 
 #include <libkgapi/fetchlistjob.h>
@@ -150,12 +151,19 @@ void CalendarResource::eventsReceived( KJob *job )
 
   FetchListJob *fetchJob = dynamic_cast< FetchListJob * >( job );
   Collection collection = fetchJob->property( "collection" ).value< Collection >();
+  Item::List existingItemsList = fetchJob->property( "existingItems" ).value< Item::List >();
+
+  QMap< QString, Akonadi::Item > existingItems;
+  Q_FOREACH( const Akonadi::Item &item, existingItemsList ) {
+    existingItems.insert( item.remoteId(), item );
+  }
+
   DefaultReminderAttribute *attr =
     dynamic_cast< DefaultReminderAttribute * >( collection.attribute( "defaultReminders" ) );
 
   Item::List removed;
   Item::List changed;
-  QMap< QString, Objects::Event * > recurrentEvents;
+  QMap< QString, Akonadi::Item > recurrentEvents;
 
   QList< Object *> allData = fetchJob->items();
 
@@ -163,16 +171,37 @@ void CalendarResource::eventsReceived( KJob *job )
   int i = 0;
   while (i < allData.length()) {
     Objects::Event *event = static_cast< Objects::Event * >( allData.at(i) );
+
     if ( event->recurs() && !event->deleted() ) {
-      recurrentEvents.insert( event->uid(), event );
-      allData.removeAt(i);
+      Item item;
+      item.setRemoteId( event->uid() );
+
+      /* If the item already exists and we receive it's update, we must make sure
+       * we preserve the UID (see bug #298518), because KOrganizer can't handle
+       * when UID changes. We use remoteId everywhere to map to Google ID */
+      if ( existingItems.contains( event->uid() ) ) {
+        EventPtr eEvent = existingItems.value( event->uid() ).payload< EventPtr >();
+        event->setUid( eEvent->uid() );
+      }
+
+      item.setRemoteRevision( event->etag() );
+      item.setPayload< Event::Ptr >( EventPtr( event ) );
+      item.setMimeType( Event::eventMimeType() );
+      item.setParentCollection( collection );
+
+      recurrentEvents.insert( item.remoteId(), item );
+
+      allData.removeAt( i );
     } else {
       i++;
     }
   }
 
   /* Step 2: Process all remaining events */
+  Item::List modifiedItems; // modifed existingItems */
   Q_FOREACH( Object *replyData, allData ) {
+    Item newItem;
+
     Objects::Event *event = static_cast< Objects::Event * >( replyData );
 
     if ( event->useDefaultReminders() && attr ) {
@@ -182,40 +211,81 @@ void CalendarResource::eventsReceived( KJob *job )
       }
     }
 
-    /* If current event is related to a recurrent event stored in the map then
-     * take the original recurrent event, set date of the current event as an
-     * exception and continue. We will process content of the map later. */
+
+    /* Find whether this event is a changed instance or exception to any of
+     * the recurrent events */
+    Item recurrentParent;
+    bool hasRecurrentParent = false;
+    bool isExistingItem = false;
     if ( recurrentEvents.contains( event->uid() ) ) {
-      Objects::Event *rEvent = recurrentEvents.value( event->uid() );
+        recurrentParent = recurrentEvents.value( event->uid() );
+        hasRecurrentParent = true;
+    } else if ( existingItems.contains( event->uid() ) ) {
+        recurrentParent = existingItems.value( event->uid() );
+        isExistingItem = true;
+        hasRecurrentParent = true;
+    }
+
+    if ( hasRecurrentParent ) { // recurrentParent.isValid() does not work because recurrentEvents are not yet stored in Akonadi
+      EventPtr rEvent = recurrentParent.payload<EventPtr>();
+
+      if ( event->recurrenceId().isValid() ) {
+        /* Store the original event UID */
+        RecurrenceHackAttribute *attr = newItem.attribute< RecurrenceHackAttribute >( Entity::AddIfMissing );
+        attr->setOriginalUID( event->uid() );
+
+        /* Change the event UID to be unique (see comment in recurrencehackattribute.h for explanation
+         * why we have to do it) */
+        event->setUid( event->uid() + "_" + event->recurrenceId().toString( "%Y%m%dT%H%M%SZ" ) );
+
+        /* Add this event as an exception to the parent recurrent event, so that
+         * they are not displayed twice */
+        rEvent->recurrence()->addExDate( event->dtStart().date() );
+        recurrentParent.setPayload<EventPtr>( rEvent );
+
+        /* Store that we have added this exception rule manually */
+        RecurrenceHackAttribute *rAttr = recurrentParent.attribute< RecurrenceHackAttribute >( Entity::AddIfMissing );
+        rAttr->addExceptionDate( event->dtStart().date() );
+
+        if ( isExistingItem ) {
+            /* Update the item with changed attribute in Akonadi */
+            modifiedItems << recurrentParent;
+        } else {
+            /* Update the item with changed attribute in the list */
+            recurrentEvents.insert( rEvent->uid(), recurrentParent );
+        }
+      }
+    }
+
+    newItem.setRemoteId( event->uid() );
 
-      rEvent->recurrence()->addExDate( event->dtStart().date() );
+    /* If the item already exists and we receive it's update, we must make sure
+     * we preserve the UID (see bug #298518), because KOrganizer can't handle
+     * when UID changes. We use remoteId everywhere to map to Google ID. */
+    if ( existingItems.contains( event->uid() ) ) {
+      EventPtr eEvent = existingItems.value( event->uid() ).payload< EventPtr >();
+      event->setUid( eEvent->uid() );
     }
 
-    Item item;
-    item.setRemoteId( event->uid() );
-    item.setRemoteRevision( event->etag() );
-    item.setPayload< Event::Ptr >( EventPtr( event ) );
-    item.setMimeType( Event::eventMimeType() );
-    item.setParentCollection( collection );
+    newItem.setRemoteRevision( event->etag() );
+    newItem.setPayload< Event::Ptr >( EventPtr( event ) );
+    newItem.setMimeType( Event::eventMimeType() );
+    newItem.setParentCollection( collection );
 
     if ( event->deleted() ) {
-      removed << item;
+      /* Don't try to remove item that is not in Akonadi - messes things up */
+      if ( existingItems.contains( newItem.remoteId() ) ) {
+        removed << newItem;
+      }
     } else {
-      changed << item;
+      changed << newItem;
     }
   }
 
-  /* Step 3: Now process the recurrent events */
-  Q_FOREACH ( Objects::Event * event, recurrentEvents.values() ) {
-
-    Item item;
-    item.setRemoteId( event->uid() );
-    item.setRemoteRevision( event->etag() );
-    item.setPayload< Event::Ptr >( EventPtr( event ) );
-    item.setMimeType( Event::eventMimeType() );
-    item.setParentCollection( collection );
+  changed.append( recurrentEvents.values() );
 
-    changed << item;
+  if ( !modifiedItems.isEmpty() ) {
+    new ItemModifyJob( modifiedItems );
   }
 
   itemsRetrievedIncremental( changed, removed );
@@ -243,7 +313,6 @@ void CalendarResource::eventCreated( KGAPI::Reply *reply )
   Objects::Event *event = static_cast< Objects::Event * >( data.first() );
 
   Item item = reply->request()->property( "Item" ).value<Item>();
-  item.setPayload< Event::Ptr >( EventPtr( event ) );
   item.setRemoteId( event->uid() );
   item.setRemoteRevision( event->etag() );
   item.setMimeType( Event::eventMimeType() );
@@ -295,4 +364,3 @@ void CalendarResource::eventMoved( KGAPI::Reply *reply )
   Item item = reply->request()->property( "Item" ).value<Item>();
   changeCommitted( item );
 }
-
