Fixes: NB#295077 - Birthday plugin only creates events for February 29th in leap...
[qtcontacts-tracker:hasselmms-contactsd.git] / plugins / birthday / cdbirthdaycalendar.cpp
1 /** This file is part of Contacts daemon
2  **
3  ** Copyright (c) 2010-2011 Nokia Corporation and/or its subsidiary(-ies).
4  **
5  ** Contact:  Nokia Corporation (info@qt.nokia.com)
6  **
7  ** GNU Lesser General Public License Usage
8  ** This file may be used under the terms of the GNU Lesser General Public License
9  ** version 2.1 as published by the Free Software Foundation and appearing in the
10  ** file LICENSE.LGPL included in the packaging of this file.  Please review the
11  ** following information to ensure the GNU Lesser General Public License version
12  ** 2.1 requirements will be met:
13  ** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
14  **
15  ** In addition, as a special exception, Nokia gives you certain additional rights.
16  ** These rights are described in the Nokia Qt LGPL Exception version 1.1, included
17  ** in the file LGPL_EXCEPTION.txt in this package.
18  **
19  ** Other Usage
20  ** Alternatively, this file may be used in accordance with the terms and
21  ** conditions contained in a signed written agreement between you and Nokia.
22  **/
23
24 #include <QStringBuilder>
25
26 #include <QContactName>
27 #include <QContactBirthday>
28
29 #include <MLocale>
30
31 #include <recurrencerule.h>
32
33 #include "cdbirthdaycalendar.h"
34 #include "debug.h"
35
36 using namespace Contactsd;
37
38 // A random ID.
39 const QLatin1String calNotebookId("b1376da7-5555-1111-2222-227549c4e570");
40 const QLatin1String calNotebookColor("#e00080"); // Pink
41
42 CDBirthdayCalendar::CDBirthdayCalendar(SyncMode syncMode, QObject *parent) :
43     QObject(parent),
44     mCalendar(0),
45     mStorage(0)
46 {
47     mCalendar = mKCal::ExtendedCalendar::Ptr(new mKCal::ExtendedCalendar(KDateTime::Spec::LocalZone()));
48     mStorage = mKCal::ExtendedCalendar::defaultStorage(mCalendar);
49
50     MLocale * const locale = new MLocale(this);
51
52     if (not locale->isInstalledTrCatalog(QLatin1String("calendar"))) {
53         locale->installTrCatalog(QLatin1String("calendar"));
54     }
55
56     locale->connectSettings();
57     connect(locale, SIGNAL(settingsChanged()), this, SLOT(onLocaleChanged()));
58
59     MLocale::setDefault(*locale);
60
61     mStorage->open();
62
63     mKCal::Notebook::Ptr notebook = mStorage->notebook(calNotebookId);
64
65     if (notebook.isNull()) {
66         notebook = createNotebook();
67         mStorage->addNotebook(notebook);
68     } else {
69         // Clear the calendar database if and only if restoring from a backup.
70         switch(syncMode) {
71         case Incremental:
72             // Force calendar name update, if a locale change happened while contactsd was not running.
73             onLocaleChanged();
74             break;
75
76         case FullSync:
77             mStorage->deleteNotebook(notebook);
78             notebook = createNotebook();
79             mStorage->addNotebook(notebook);
80             break;
81         }
82     }
83 }
84
85 CDBirthdayCalendar::~CDBirthdayCalendar()
86 {
87     if (mStorage) {
88         mStorage->close();
89     }
90
91     debug() << "Destroyed birthday calendar";
92 }
93
94 mKCal::Notebook::Ptr CDBirthdayCalendar::createNotebook()
95 {
96     return mKCal::Notebook::Ptr(new mKCal::Notebook(calNotebookId,
97                                                     qtTrId("qtn_caln_birthdays"),
98                                                     QLatin1String(""),
99                                                     calNotebookColor,
100                                                     false, // Not shared.
101                                                     true, // Is master.
102                                                     false, // Not synced to Ovi.
103                                                     false, // Writable.
104                                                     true, // Visible.
105                                                     QLatin1String("Birthday-Nokia"),
106                                                     QLatin1String(""),
107                                                     0));
108 }
109
110 void CDBirthdayCalendar::updateBirthday(const QContact &contact)
111 {
112     // Retrieve contact details.
113     const QContactDisplayLabel displayName = contact.detail<QContactDisplayLabel>();
114     const QDate contactBirthday = contact.detail<QContactBirthday>().date();
115
116     if (displayName.isEmpty() || contactBirthday.isNull()) {
117         warning() << Q_FUNC_INFO << "Contact without name or birthday, local ID: "
118                   << contact.localId();
119         return;
120     }
121
122     // Retrieve birthday event.
123     if (not mStorage->isValidNotebook(calNotebookId)) {
124         warning() << Q_FUNC_INFO << "Invalid notebook ID: " << calNotebookId;
125         return;
126     }
127
128     KCalCore::Event::Ptr event = calendarEvent(contact.localId());
129
130     if (event.isNull()) {
131         // Add a new event.
132         event = KCalCore::Event::Ptr(new KCalCore::Event());
133         event->startUpdates();
134         event->setUid(calendarEventId(contact.localId()));
135         event->setAllDay(true);
136
137         // Ensure events appear as birthdays in the calendar, NB#259710.
138         event->setCategories(QStringList() << QLatin1String("BIRTHDAY"));
139
140         if (not mCalendar->addEvent(event, calNotebookId)) {
141             warning() << Q_FUNC_INFO << "Failed to add event to calendar";
142             return;
143         }
144     } else {
145         // Update the existing event.
146         event->setReadOnly(false);
147         event->startUpdates();
148     }
149
150     // Transfer birthday details from contact to calendar event.
151     event->setSummary(displayName.label());
152
153     // Event has only date information, no time.
154     event->setDtStart(KDateTime(contactBirthday, QTime(), KDateTime::ClockTime));
155     event->setDtEnd(KDateTime(contactBirthday.addDays(1), QTime(), KDateTime::ClockTime));
156
157     // Must always set the recurrence as it depends on the event date.
158     KCalCore::Recurrence *const recurrence = event->recurrence();
159
160     if (contactBirthday.month() != 2 || contactBirthday.day() < 29) {
161         // Simply setup yearly recurrence for trivial dates.
162         recurrence->setStartDateTime(event->dtStart());
163         recurrence->setYearly(1); /* every year */
164     } else {
165         // For birthdays on February 29th the event shall occur on the
166         // last day of February. This is February 29th in leap years,
167         // and February 28th in all other years.
168         //
169         // NOTE: Actually this recurrence pattern will fail badly for
170         // people born on February 29th of the years 2100, 2200, 2300,
171         // 2500, ... - but I seriously doubt we care.
172         //
173         // NOTE2: Using setByYearDays() instead of just setting proper
174         // start dates, since libmkcal fails to store the start dates
175         // of recurrence rules.
176         KCalCore::RecurrenceRule *rule;
177
178         // 1. Include February 29th in leap years.
179         rule = new KCalCore::RecurrenceRule;
180         rule->setStartDt(event->dtStart());
181         rule->setByYearDays(QList<int>() << 60); // Feb 29th
182         rule->setRecurrenceType(KCalCore::RecurrenceRule::rYearly);
183         rule->setFrequency(4); // every 4th year
184         recurrence->addRRule(rule);
185
186         // 2. Include February 28th starting from year after birth.
187         rule = new KCalCore::RecurrenceRule;
188         rule->setStartDt(event->dtStart());
189         rule->setByYearDays(QList<int>() << 59); // Feb 28th
190         rule->setRecurrenceType(KCalCore::RecurrenceRule::rYearly);
191         rule->setFrequency(1); // every year
192         recurrence->addRRule(rule);
193
194         // 3. Exclude February 28th in leap years.
195         rule = new KCalCore::RecurrenceRule;
196         rule->setStartDt(event->dtStart());
197         rule->setByYearDays(QList<int>() << 59); // Feb 28th
198         rule->setRecurrenceType(KCalCore::RecurrenceRule::rYearly);
199         rule->setFrequency(4); // every 4th year
200         recurrence->addExRule(rule);
201
202     }
203
204     // Set the alarms on the event
205     event->clearAlarms();
206     KCalCore::Alarm::Ptr alarm = event->newAlarm();
207     alarm->setType(KCalCore::Alarm::Audio);
208     alarm->setEnabled(true);
209     alarm->setDisplayAlarm(event->summary());
210     alarm->setStartOffset(KCalCore::Duration(-36 * 3600 /* seconds */));
211
212     event->setReadOnly(true);
213     event->endUpdates();
214
215     debug() << "Updated birthday event in calendar, local ID: " << contact.localId();
216 }
217
218 void CDBirthdayCalendar::deleteBirthday(QContactLocalId contactId)
219 {
220     KCalCore::Event::Ptr event = calendarEvent(contactId);
221
222     if (event.isNull()) {
223         debug() << Q_FUNC_INFO << "Not found in calendar:" << contactId;
224         return;
225     }
226
227     mCalendar->deleteEvent(event);
228
229     debug() << "Deleted birthday event in calendar, local ID: " << event->uid();
230 }
231
232 void CDBirthdayCalendar::save()
233 {
234     if (not mStorage->save()) {
235         warning() << Q_FUNC_INFO << "Failed to update birthdays in calendar";
236     }
237 }
238
239 QDate CDBirthdayCalendar::birthdayDate(QContactLocalId contactId)
240 {
241     KCalCore::Event::Ptr event = calendarEvent(contactId);
242
243     if (event.isNull()) {
244         return QDate();
245     }
246
247     return event->dtStart().date();
248 }
249
250 QString CDBirthdayCalendar::summary(QContactLocalId contactId)
251 {
252     KCalCore::Event::Ptr event = calendarEvent(contactId);
253
254     if (event.isNull()) {
255         return QString();
256     }
257
258     return event->summary();
259 }
260
261 QString CDBirthdayCalendar::calendarEventId(QContactLocalId contactId)
262 {
263     static const QLatin1String calIdExtension("com.nokia.birthday/");
264     return calIdExtension + QString::number(contactId);
265 }
266
267 KCalCore::Event::Ptr CDBirthdayCalendar::calendarEvent(QContactLocalId contactId)
268 {
269     const QString eventId = calendarEventId(contactId);
270
271     if (not mStorage->load(eventId)) {
272         warning() << Q_FUNC_INFO << "Unable to load event from calendar";
273         return KCalCore::Event::Ptr();
274     }
275
276     KCalCore::Event::Ptr event = mCalendar->event(eventId);
277
278     if (event.isNull()) {
279         debug() << Q_FUNC_INFO << "Not found in calendar:" << contactId;
280     }
281
282     return event;
283 }
284
285 void CDBirthdayCalendar::onLocaleChanged()
286 {
287     mKCal::Notebook::Ptr notebook = mStorage->notebook(calNotebookId);
288
289     if (notebook.isNull()) {
290         warning() << Q_FUNC_INFO << "Calendar not found while changing locale";
291         return;
292     }
293
294     const QString name = qtTrId("qtn_caln_birthdays");
295
296     debug() << Q_FUNC_INFO << "Updating calendar name to" << name;
297     notebook->setName(name);
298
299     if (not mStorage->updateNotebook(notebook)) {
300         warning() << Q_FUNC_INFO << "Could not save calendar";
301     }
302 }