Calendar Feed for N9: what it is and how it was developed

    This post is participating in the competition “ Smart Phones for Smart Posts ”.

    The Nokia N9 is a nice device in many ways. But out of the box, it does not have one very important functionality. It is impossible, casting one glance at the phone’s home screen, to understand what events ahead are scheduled on the calendar. To fix this annoying problem, I developed the Calendar Feed application ( OVI Store , source ). Under the cut, I’ll talk in more detail about the application itself (a little) and how it was created (most of the post).

    Caution, there is a lot of text. If you are ready, then ...

    So, the Calendar Feed app. It is fully integrated into the OS and is called again through the OS, and not by the user (although the user can rush the system with updating application data using the Refresh button). Settings are fully integrated into the system and look like part of it. During operation, the application periodically (every 20 minutes) uploads an element with the nearest events from the calendar to Feed (Channel in the Russian version), while deleting the old one. This element is always the top one (its time is set as the current plus one day). The application has a few settings (it is still very young and can do a little), but they are enough for the full functionality of the basic functionality. You can adjust the number of events in the item,
    Screenshots Well, this is all the lyrics and not as interesting as the actual development of this application. In the end, Habr is a technical resource. So we’ll figure out how all this was developed in more detail. The following points will be sorted:

    Disclaimer: The
    documentation for Harmattan is very scarce and does not shine in its entirety, so most of what is written here had to be figured out by typing and digging in binaries. Perhaps some paths are not optimal. If you know an easier way to do something described in this article, write it in the comments.

    Display events

    The most important and basic functionality of the application is adding elements to Feed. There are two and a half ways:
    • simple - through the MEventFeed class;
    • more difficult - through DBus;
    • half - there is a wrapper over DBus written by Johan Paul (Johan Paul), it can be viewed at:

    We will not consider the last option, but consider the first two.

    Class MEventFeed

    This option is suggested by the Harmattan documentation. He is as simple as an ax. There is a MEventFeed class that has 2 methods (although there are three of them according to the online documentation, but the third one is not available in the QtSDK target): addItem and removeItemsBySourceName . The first method adds an element to Feed, the second removes all elements with the same source.
    qlonglong addItem(const QString &icon, // Иконка (отображается слева)
                      const QString &title, // Заголовок (сверху болдом)
                      const QString &body, // Собственно сам текст
                      const QStringList &imageList, // Список изображений, которые будут выведены под текстом (используется например Facebook'ом)
                      const QDateTime ×tamp, // Временная метка (именно через нее осуществляется вечное верхнее положение)
                      const QString &footer, // Футер (отображается снизу, рядом с временем)
                      bool video, // Флаг является ли элемент в imageList ссылкой на видео (в этом случае элемент в этом списке может быть только один, остальные будут проигнорированы)
                      const QUrl &url, // Урл, по которому перейти при нажатии на элемент
                      const QString &sourceName, // Id источника
                      const QString &sourceDisplayName // Название источника (будет выведено на экран в некоторых случаях. Например, при долгом нажатии на элемент)

    The first time I saw THIS, my face was stretched out very much. Is it really impossible to make a small class with getters and setters and pass it? Well, okay, what they gave, we are working with that. The comments indicate what each parameter means. The method returns either the id of the element or -1 (if something went wrong. Most often, these are incorrectly specified parameters).
    void removeItemsBySourceName(const QString &sourceName);

    Well, everything is simple here. We pass the same id that was specified in the previous method, and all elements with this id are deleted.

    This path is simple and convenient, but it limits us in possibilities. The fact is that in addition to navigating through the url, the dbus method can still be called. But with the help of this class you cannot specify the signature of the called method, therefore we will consider a more universal way of working with Feed - via dbus.


    QDBusMessage message = QDBusMessage::createMethodCall(
    QList args;
    QVariantMap itemArgs;
    itemArgs.insert("title", "Calendar");
    itemArgs.insert("icon", icon);
    itemArgs.insert("body", body);
    itemArgs.insert("timestamp", QDateTime::currentDateTime().addDays(1).toString("yyyy-MM-dd hh:mm:ss"));
    itemArgs.insert("sourceName", "SyncFW-calendarfeed");
    itemArgs.insert("sourceDisplayName", "Calendar Feed");
    itemArgs.insert("action", action);
    QDBusConnection bus = QDBusConnection::sessionBus();
    bus.callWithCallback(message, this, SLOT(dbusRequestCompleted(QDBusMessage)));

    There is more code here, but there is the necessary ability for us to set the action property (which stores the signature of the dbus method, but more on that later). You can set all the parameters that were in MEventFeed: addItem and plus action. The only thing to remember is the transfer of the timestamp. It is transmitted in a string in the form of an "ГГГГ-ММ-ДД чч:мм:сс".
    Icon, you can use the system ones, passing its id instead of the icon path (which is what Calendar Feed uses. The icon with id icon-l-calendar-12 is used , where instead of 12 the number of the first event in the list is substituted).

    Also, using DBus makes it possible to delete and update an item by id (which is not available in MEventFeed).

    Harmattan DBus signature storage features

    I had to work on solving this puzzle a couple of nights, but in the end (with the help of hexdump , strings and such and such a mother) I was able to understand how to pass the method signature so that it was correctly called.

    We have the following signature: / 2011 11 25
    When this method is called with these parameters, the device opens a calendar in month view mode with the selected date November 25, 2011.

    To invoke this method, we need to put the following line in the action of the element: / showMonthView AAAAAgAAAAfb AAAAAgAAAAAL AAAAAgAAAAAZ

    Well, with a space between the interface and the method name, everything is clear. In the QDBusMessage constructor , they are also divided into two different parameters. But what are three strange string arguments instead of three numbers? Everything is very simple when you know, hehe (I killed 2 or 3 in the evening digging around in binaries and dbus methods). This is Base64 from a QVariant object serialized into a QDataStream .

    Helper method for generating such lines:
    QString CalendarFeedPlugin::base64SerializedVariant(const QVariant &value) const
        QByteArray ba;
        QDataStream stream(&ba, QIODevice::WriteOnly);
        stream << value;
        return ba.toBase64();

    It is suspected that such a notation is used everywhere in Harmattan, where the signature of the dbus method is stored.


    We learned how to create Feed elements. Now we need to somehow add it there periodically. At the same time, it’s advisable not to fence demons who will eat the battery, but to integrate as much as possible into the system so that it decides when to update.

    For this purpose, Harmattan has a SyncFW framework to which you can write a plugin. This plugin will be polled according to the specified parameters. The plugin is logically divided into two parts: metadata and shared library.

    An important point:in PR1.0 you need to reboot the device after installing a new plugin. In PR1.1, this problem can be circumvented (see the Installation Scripts section). But updating the plugin always requires a reboot. Without it, the old shared library will just be used (maybe somehow it can be unloaded without rebooting, but I don’t know this method) with new metadata.

    Another important point: wherever the calendarfeed name is used in the examples, it is understood that the plugin name should be there (the library should be called, i.e. in our case it is There should not be hyphens in the name of the plugin. Ideally - only Latin letters, I think the use of numbers is also allowed (did not check).


    To describe the plugin, we need the following files:
    • /etc/sync/profiles/client/calendarfeed.xml
    • /etc/sync/profiles/service/calendarfeed.xml
    • /etc/sync/profiles/sync/calendarfeed.xml


    Just create a client side profile (the shared library acts as a client)


    We create a service profile and indicate that it can work offline. That is, our plugin does not need an Internet connection to work successfully (even though the Harmattan documentation states that this is not possible, it actually works). A customer profile is also indicated.


    Create a synchronization profile. Profile node content :
    • displayname - the name that will be displayed in the UI
    • enabled - activated or not profile
    • use_accounts - whether to use the account framework
    • hidden - whether this profile is hidden in the UI
    • profile - service profile

    The most important part here is, of course, the schedule. According to the information from this node, our plugin will be polled. In this case, it will be interrogated every 20 minutes regardless of the day of the week.

    But what if we need him to be questioned every half an hour on weekends and weekdays, nightly, and once every 15 minutes on weekdays? With half an hour it’s clear. Change 20 to 30 and everything is ok. To set the additional schedule time, you need to enable the rush node as a schedule subnode :

    Shared library

    In the library, we need exactly one Buteo :: ClientPlugin descendant class and two extern functions for creating and removing the plugin.
    class CalendarFeedPlugin : public Buteo::ClientPlugin
        CalendarFeedPlugin( const QString &pluginName,
                             const Buteo::SyncProfile &profile,
                             Buteo::PluginCbInterface *cbInterface );
        virtual ~CalendarFeedPlugin();
        virtual bool init();
        virtual bool uninit();
        virtual bool startSync();
        virtual void abortSync(Sync::SyncStatus status = Sync::SYNC_ABORTED);
        virtual Buteo::SyncResults getSyncResults() const;
        virtual bool cleanUp();
    public slots:
        virtual void connectivityStateChanged( Sync::ConnectivityType type,
                                               bool state );
    protected slots:
        void syncSuccess();
        void syncFailed();
        void updateFeed();
        void dbusRequestCompleted(const QDBusMessage &reply);
        void dbusErrorOccured(const QDBusError &error, const QDBusMessage &message);
        void updateResults(const Buteo::SyncResults &results);
        QString base64SerializedVariant(const QVariant &value) const;
        Buteo::SyncResults m_results;
    extern "C" CalendarFeedPlugin* createPlugin( const QString& pluginName,
                                                  const Buteo::SyncProfile &profile,
                                                  Buteo::PluginCbInterface *cbInterface );
    extern "C" void destroyPlugin( CalendarFeedPlugin *client );

    This header contains a class that is close to the minimum required (the minimum will be if the protected slots and private sections are removed ). Everything else is an override of virtual methods (mostly pure virtual).

    virtual bool init ();
    It is called at the very beginning and is needed to check whether the plug-in can initialize itself. If successful, returns true and the process goes further.

    virtual bool uninit ();
    Called before the plug-in is unloaded, usually cleaning takes place here.

    virtual bool startSync ();
    The method checks whether (and should) it can start the update and starts it (asynchronously). Returns the success result of the start of the update.

    virtual void abortSync (Sync :: SyncStatus status = Sync :: SYNC_ABORTED);
    Called when the user has canceled the update. It is necessary to work with accounts (where there is such a button), in our case, the user has no opportunity to cancel the update.

    virtual Buteo :: SyncResults getSyncResults () const;
    Returns the results of the last update.

    virtual bool cleanUp ();
    Called when an account is deleted. In our case, again, it is not needed (although the documentation says that it may be possible to use it later).

    virtual void connectivityStateChanged (Sync :: ConnectivityType type, bool state);
    Called if the connection status has changed. If the plugin does not work with the network, then this slot will also be empty.

    It turns out that the most interesting method is startSync (). And rightly so. In it, we run through the timer (that is, asynchronously) a method that adds our element to Feed. All other methods are mostly utility.

    Localization of the SyncFW plugin

    Naturally, this small library does not know anything about any localizations there. Therefore, you need to poke it manually in the desired location (the language will pick itself up, the main thing is to specify the desired set of .qm files).
    MLocale locale;
    itemArgs.insert("title", locale.translate("", "calendar_feed_item_title"));

    Refresh button update

    As with updating without the Internet, the documentation tells us that the Refresh button can only update embedded applications (read: Twitter, Facebook, RSS Feeds, AP Mobile). But since we are going our own way, we will still try to verify this statement ourselves.

    In the Feed database (it is located in the file /home/user/.config/meegotouchhome-nokia/ and is a regular Sqlite database) we see three tables: events, images, refreshactions. The first two are of little interest to us in this case, but the third is very interesting. She has only one attribute - action. Since the name is already familiar to us by adding the Feed element, we will try to put here the signature of the dbus method call (in the format described above). And it works! The documentation was again wrong. Well, okay, but now we can implement the functionality we need so much. We are following the path of maximum integration into the system and it will be very strange if the application does not respond to this button.

    Well, we have determined how to bind to the button, but how to make our plugin invoke outside of its schedule? To do this, there is the dbus method com.meego.msyncd / synchronizer com.meego.msyncd.startSync with one argument - the name of the client profile.

    There remains a small detail ... How to add your action to this table? Well, not a SQL query in fact. Fortunately, next to addItem in dbus there is a methodaddRefreshAction to which you can pass a signature string.

    A little later we will find a place to call addRefreshAction from . In the meantime, continue with the application itself.

    Applet for customization

    We need settings for our application. The logical question is where to put them? Make it a separate application and put it next to others? No, not an option. There are too many of them in Launcher. Another, which will be used extremely rarely (about 1 time per update), is clearly superfluous. In this case, the opportunity to add your applet to the Settings hierarchy comes to our aid. And there is no superfluous icon on the screen and you can add a call button next to turning on / off the display of Twitter and Facebook in Feed.

    Applet Creation

    MeeGo Harmattan provides 3 ways to create an applet that you can enable in Settings:
    • Declarative - the set of settings is described by the xml file
    • Binary - creates a shared library with an inheritor from DcpAppletIf
    • External - using a regular application

    The third option is not at all good (and by the way is not recommended in the documentation), since it can cause delays when switching from Settings to the application.
    The second option is the most flexible, but more complicated than the first.
    The first option is minimally flexible, but at the same time as simple as possible.

    Since we do not need particularly complex settings, we will do the first way.

    For it we need two files:
    • /usr/share/duicontrolpanel/desktops/calendarfeed.desktop - a .desktop file with a description of what, what is called and where it lies (in general, the completely standard purpose of a .desktop file)
    • /usr/share/duicontrolpanel/uidescriptions/calendarfeed.xml - description of a set of settings displayed on the screen

    We will analyze them in turn.

    [Desktop Entry]
    Name=Calendar Feed
    # this has to be identical to the value in Name
    X-Maemo-Fixed-Args=Calendar Feed
    Category=Events Feed
    Text2=Calendar events at Feed screen

    The first section is standard. Except for the parameters of X-Maemo- *, which describe which dbus method and with what arguments to call to open this applet.

    The DUI section describes how to connect a shared library. In our case (declarative applet) it is always In the case of a binary applet, the shared library name with the DcpAppletIf descendant will be here.

    The DCP section contains information about where, where, and what to insert. In our case, we get into the Events Feed settings (in the same place as the Twitter and Facebook switches). Text2 is the second line (there usually goes a description or the current state of the item).

    MMM, d

    You will laugh, but this example lists all the types of nodes except one, which can be in a declarative applet.

    For all types of nodes, two parameters are standard:
    • key - key in GConf
    • title - the name displayed in the UI. You can specify the id of the string to be translated (as was done in the example)

    Yes! Surprise-surprise! All of himself Qt-shny MeeGo Harmattan uses GConf to store internal settings. By the way, it is the settings from GConf that get into backup.

    Group - performs the role of a sort of group box (an example of the display in the second screenshot. There are two groups: Behavior and Appearance). Although the documentation says that there are no parameters, the title nevertheless works as it should.
    Boolean - switch. The most common ...
    Integer - a slider for selecting an integer value. In addition to the standard, there are two more parameters: min and max. They describe the range for the slider.
    Text - a field for entering text. By the way, for this type the title is also not declared in the documentation, but I think you already realized that it will work.
    Selection- a group of buttons, each of which is described by a sub-option with one parameter - key, which in this case plays the role of the value displayed in the UI. Each option should have text with a number (which will be stored in GConf).

    Using GConfItem

    Although GConf is used, nevertheless there is a Qt class for working with it - GConfItem. We need two methods from it: value () and set () . The first returns the value of the parameter in QVariant, the second changes the value of the parameter.

    However, not all types are supported. The following rules apply (due to gconf and qvariant):
    • QVariant :: Invalid - missing gconf parameter
    • QVariant :: Int , QVariant :: Double , QVariant :: Bool translate to their equivalent types
    • QVariant :: String is only supported with utf8 encoding
    • QVariant :: StringList - a list of strings in utf8.
    • QVariant :: List list QVariant :: Int , QVariant :: Double , QVariant :: Bool or QVariant :: String (will return as QVariant :: StringList ), but all elements must be of the same type
    • All other types (on both sides) are ignored.

    In the case of a declarative applet, this is completely suitable, since the applet provides a string, a Boolean, and an integer.
    An example of working with GConfItem
    bool fillWithFuture = false;
    GConfItem fillConfItem("/apps/ControlPanel/CalendarFeed/FillWithFuture");
    QVariant fillVariant = fillConfItem.value();
    if (fillVariant.isValid())
        fillWithFuture = fillVariant.toBool();

    Reading calendar data

    In fact, the most important part of the application. In the end, we are deriving events from there. Theoretically, everything should work through Qt Mobility and work perfectly or close to it. After all, MeeGo uses Qt at its core. Nokia, which released n9, is essentially the owner of Qt. Everything should be integrated as much as possible, right? Schschazz, yeah. As always, a pig is planted here. But for this and the post, in fact. To describe these pigs for future generations.

    All Day Events

    The calendar has the ability to set an event for the whole day (that is, without time). QOrganizer also has an allDay parameter for the event. Which is always false . Regardless of what is displayed on the calendar. Vague guesses torment that the opposite will be true (that is, creating an allDay event through QOrganizer will also create something wrong on the calendar). Nevertheless, there is a heuristic way to determine if an event is allDay or not.
    if (!isAllDay &&
            startDateTime.time().hour() == 0 &&
            startDateTime.time().minute() == 0 &&
            endDateTime.time().hour() == 0 &&
            endDateTime.time().minute() == 0 &&
        isAllDay = true;

    ToDo items

    QOrganizer knows that there are ToDo elements. He even stores the end date for them. It's just that through it it is impossible to find out whether ToDo is completed or not. It’s just not possible. There is such a flag in QOrganizer itself, but it is not cocked when QOrganizer receives data from the underlying level. Unlike the previous problem, I have no solution through Qt Mobility. And it seems to me that it is not at all through QtMobility (at least until the next firmware).

    Time zones

    Plus to the first two jambs found, a third was also discovered. Much more fun and exotic. The point is. In the data structure in which the calendar is stored in the system, there is a field in which the event is recorded in which time zone. That is, if there are two events with the same values ​​in the "Start Date" field, then it is not a fact that they start at the same time. To correctly determine the time, both fields are needed. Accordingly, if we work with some exported calendar that was created in UTC, or a common calendar synchronized by CalDAV and people from different time zones work with it, then we will get an incorrect time displayed.

    The calendar management system is built on two levels (high and low). At high QtMobility sits with the QOrganizer module, which is convenient to use, but is subject to the above phenomenon (it simply does not suspect the existence of a field with a timezone). At low sits mKCal, which always returns the correct data, but is not very convenient to use.

    Calendar Feed was originally written using QOrganizer. But after the appearance of one bug (critical, associated with incorrect display of time in some exotic cases), a sort of transition period began. At the moment, the project uses both levels. First, all events are collected through QOrganizer, then, at the extreme, the borders of the dates are constructed, according to which events are collected via mKCal. The correct data are taken from the latter (in separate data structures). Then the cycle goes on first and, if necessary, the data is replaced.

    Probably, after reading the previous paragraph, the question immediately appeared: “what for? isn’t it easier to switch to mKCal right away? ” In fact, I am thinking of a transition. But this bug is quite weighty and I had to fix it quickly. The transition to a completely new system that implements the main functionality is fraught with such bugs that you think twice. Therefore, it was decided to issue an urgent fix with a double solution, and then calmly, having weighed all the pros and cons, switch to a new system.

    By the way, mKCal (which is logical) has the correct information about the first two phenomena.

    Project files and other little things

    So, we figured out the implementation mechanisms. It remains to put it all together. First look at the .pro file
    TEMPLATE = lib
    TARGET = calendarfeed-client
    VERSION = 0.3.0
    INCLUDEPATH += . \
                    /usr/include/libsynccommon \
                    /usr/include/libsyncprofile \
                    /usr/include/gq \
                    /usr/include/mkcal \
    LIBS += -lsyncpluginmgr -lsyncprofile -lgq-gconf -lmkcal
    CONFIG += debug plugin meegotouchevents 
    CONFIG += mobility
    MOBILITY += organizer
    QT += dbus
    SOURCES += \
    HEADERS +=\
    QMAKE_CXXFLAGS = -Wall \
        -g \
        -Wno-cast-align \
        -O2 -finline-functions
    target.path = /usr/lib/sync/
    system ("cd translations && lrelease -markuntranslated '' -idbased *.ts")
    client.path = /etc/sync/profiles/client
    client.files = xml/calendarfeed.xml
    sync.path = /etc/sync/profiles/sync
    sync.files = xml/sync/calendarfeed.xml
    service.path = /etc/sync/profiles/service
    service.files = xml/service/calendarfeed.xml
    settingsdesktop.path = /usr/share/duicontrolpanel/desktops
    settingsdesktop.files = settings/calendarfeed.desktop
    settingsxml.path = /usr/share/duicontrolpanel/uidescriptions
    settingsxml.files = settings/calendarfeed.xml
    translations.path = /usr/share/l10n/meegotouch
    translations.files += translations/*qm
    INSTALLS += target client sync service settingsdesktop settingsxml translations
    OTHER_FILES += \
        qtc_packaging/debian_harmattan/rules \
        qtc_packaging/debian_harmattan/README \
        qtc_packaging/debian_harmattan/manifest.aegis \
        qtc_packaging/debian_harmattan/copyright \
        qtc_packaging/debian_harmattan/control \
        qtc_packaging/debian_harmattan/compat \
        qtc_packaging/debian_harmattan/changelog \
        qtc_packaging/debian_harmattan/postinst \

    Мы собираем shared-библиотеку (как мы помним из раздела про SyncFW, плагин должен быть именно таким), с названим calendarfeed-client (тоже обусловлено SyncFW).
    Подключаем все нужные системные хедеры и библиотеки.
    Подключаем meegotuchevents для работы MEventFeed (хоть мы и добавляем через dbus, но все равно удаление оставлено на вызове метода).
    Добавляем qdbus и mobility-organizer, а также указываем какие у нас хедеры и файлы с исходниками.
    Пока все стандартно.

    Все самое важное после комментария install.
    Мы размещаем все наши xml и desktop в нужные папки (указаны в соответствующих разделах статьи) и помещаем локализацию в папку / usr / share / l10n / meegotouch . In this case, the files should be of the form calendarfeed_ru.qm , where calendarfeed is what was specified in the desktop file in the X-translation-catalog parameter .

    Installation scripts

    There is one interesting feature with SyncFW plugins. They themselves do not know how to correctly install and remove. They need a reboot. Plus we need to remove our item from Feed when deleting. And it would be nice to add refreshAction for the Refresh button to work during installation too.

    There are two scripts for this that can be added to the deb package:
    • postinst - runs immediately after installation
    • prerm - run immediately before uninstall

    /usr/bin/aegis-exec -s -u user dbus-send --dest=com.meego.msyncd --print-reply /synchronizer com.meego.msyncd.installPlugin string:'calendarfeed'
    /usr/bin/aegis-exec -s -u user dbus-send --print-reply /eventfeed string:'com.meego.msyncd /synchronizer com.meego.msyncd startSync AAAACgAAAAAYAGMAYQBsAGUAbgBkAGEAcgBmAGUAZQBk'
    gconftool -s /apps/ControlPanel/CalendarFeed/EnableFeed -t bool true
    /usr/bin/aegis-exec -s -u user dbus-send --dest=com.meego.msyncd --print-reply /synchronizer com.meego.msyncd.startSync string:'calendarfeed'
    exit 0

    Here we explicitly say that we installed the plugin (this only works in PR1.1, in PR1.0 you still have to restart the device), add refreshAction, turn on our application and manually update the item in Feed.

    /usr/bin/aegis-exec -s -u user dbus-send --dest=com.meego.msyncd --print-reply /synchronizer com.meego.msyncd.uninstallPlugin string:'calendarfeed'
    /usr/bin/aegis-exec -s -u user dbus-send --print-reply /eventfeed string:'SyncFW-calendarfeed'
    exit 0

    Remove the plugin and element from Feed.

    By the way, even though the specified method of installing the plugin helps with the first installation, but when updating the shared library, the old one is still used before the reboot (it is not unloaded from memory). There are certain inconveniences during debugging.


    Yes. The article is finally over. I'm tired of writing it, and you, I think, are a little tired of reading it. But thanks to this article, we learned:

    In fact, of course, I did not include in this post everything that I encountered during development. And far from everything was described in detail, the post is already too long. My goal was to show how this can be done, and not to show any theoretical information or the way I went to this information. But if this information is also interesting, then you can read a few “travel notes” during the development of this application in my blog (link in the profile).

    And finally, a bunch of links:

    I hope you were interested. That's all, folks!

    Also popular now: