This post participates in the competition "
Smart phones for smart posts ".
Nokia N9 is a nice device in many ways. But out of the box there is no one very important functionality. It’s impossible to take a look at the home screen of the phone, to understand what events ahead are scheduled in the calendar. To fix this annoying problem, I developed the Calendar Feed application (
OVI Store ,
source code ). Under the cat, I will tell in more detail about the application itself (a little) and how it was created (most of the post).
Carefully, there is a lot of text. If you are ready, then ...
So, the application Calendar Feed. It is fully integrated into the OS and is called again via the OS, and not by the user (although the user can also hurry the system with updating the application data using the Refresh button). Settings are fully integrated into the system and look like its part. During operation, the application periodically (once every 20 minutes) lays out in the Feed (Channel in the Russian version) an element with the nearest events from the calendar, deleting the old one. This element is always the top (its time is set as current plus one day). The application has a few settings (it is still very young and does little), but there are enough of them for the full functionality of the basic functionality. You can adjust the number of events in the element, whether or not to supplement today's events with future ones (if there are few) and restriction (with the possibility of disconnection) in days for future events (so as not to show events from the next year or month).
Screenshots
')
Well, these are all lyrics and not as interesting as the development of this application itself. In the end, Habr is a technical resource. So let's look at how it all worked out in more detail.
The following points will be disassembled:
Disclaimer:
The documentation on Harmattan is very poor and does not shine with fullness, so most of what is written here had to be found out by typing and digging into binaries. Maybe some ways are not optimal. If you know an easier way to do anything described in this article, write it in the comments.
Show events on screen
The most important and basic functionality of the application is to add elements to the Feed. There are two and a half ways here:
- simple - through the MEventFeed class;
- more difficult - through DBus;
- halfway - there is a wrapper over DBus, written by Johan Paul (Johan Paul), it can be viewed at: gitorious.org/libharmattaneventview
We will not consider the last option, but consider the first two.
Class MEventFeed
This option is proposed documentation for Harmattan. It is simple as an ax. There is a class MEventFeed, which has 2 methods (although there are three of them in the online documentation, but the third one is not available in target in QtSDK):
addItem and
removeItemsBySourceName . The first method adds an element to the Feed, the second deletes all elements with the same source.
qlonglong addItem(const QString &icon,
The first time I saw THIS, my face was stretched. Is it really impossible to make a small class with getters and setters and transfer it? Well, okay, that gave, in order and work. 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, here everything is simple. We pass the same id as indicated in the previous method, and all elements with this id are deleted.
This way is simple and convenient, but it limits us in possibilities. The fact is that in addition to the url transition, it is still possible to call the dbus method. But using this class it is impossible to specify the signature of the method being called, so we will consider a more universal way of working with Feed - via dbus.
DBus
QDBusMessage message = QDBusMessage::createMethodCall( "com.nokia.home.EventFeed", "/eventfeed", "com.nokia.home.EventFeed", "addItem"); QList<QVariant> 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(); args.append(itemArgs); message.setArguments(args); bus.callWithCallback(message, this, SLOT(dbusRequestCompleted(QDBusMessage)));
There is more code here, but we need the possibility of setting the action property (in which the signature of the dbus method is stored, but more on that later). You can set all the parameters that were in MEventFeed: addItem plus action. The only thing to remember is the transfer of a timestamp. It is transmitted as a string in the form
"-- ::".
You can use system icons by transferring its id to the icon instead of the path (which is what Calendar Feed uses. The icon with
icon-l-calendar-12 is used , where the number of the first event in the list is substituted for 12).
Also using DBus allows you to delete and update an item by id (which is absent in MEventFeed).
Features storage DBus signatures in the depths of Harmattan
I had to work on solving this puzzle for a couple of evenings, but in the end (using
hexdump ,
strings and such-and-such mother) I could understand how to transfer the method signature so that it was correctly called.
We have the following signature:
com.nokia.Calendar / com.nokia.maemo.meegotouch.CalendarInterface.showMonthView 2011 11 25
When calling this method with these parameters on the device, a calendar will open in the month view with the selected date November 25, 2011.
To call this method, we need to put the following line in the
action element:
com.nokia.Calendar / com.nokia.maemo.meegotouch.CalendarInterface showMonthView AAAAAgAAAAfb AAAAAgAAAAAL AAAAAgAAAAAZ
Well, with a space between the interface and the name of the method, everything is clear. In the
QDBusMessage constructor
, they are also divided into two different parameters. But what about three strange string arguments instead of three numbers? It's very simple when you know, hehe (I killed 2 or 3 in the evening digging in binary and dbus methods). This is Base64 from the
QVariant object serialized in
QDataStream .
Helper method for generating such strings:
QString CalendarFeedPlugin::base64SerializedVariant(const QVariant &value) const { QByteArray ba; QDataStream stream(&ba, QIODevice::WriteOnly); stream << value; return ba.toBase64(); }
There is a suspicion that this notation is used everywhere in Harmattan, where the signature of the dbus method is stored.
Syncfw
We learned how to create a feed element. Now we need to somehow add it there periodically. It is advisable not to fence the demons, which will eat the battery, but to integrate as much as possible into the system so that it decides when to upgrade.
For this purpose, Harmattan has a
SyncFW framework to which you can write a plugin. This plugin will be polled in accordance with the specified parameters. The plugin is logically divided into two parts: metadata and a shared library.
Important point: in PR1.0, you need to restart the device after installing a new plug-in. In PR1.1, this problem can be circumvented (see the Installation Scripts section). But updating a plugin always requires a reboot. Without it, the old shared library will simply be used (perhaps somehow it can be unloaded without rebooting, but I don’t know this method) with new metadata.
Another important point: wherever the name calendarfeed is used in the examples, it is assumed that the name of the plugin should be there (the library should be called lib <plugin_name> -client.so, that is, in our case, libcalendarfeed-client.so). There should be no hyphens in the plugin name. Ideally - only Latin letters, I think the use of numbers is also allowed (did not check).
Metadata
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
/etc/sync/profiles/client/calendarfeed.xml
<?xml version="1.0" encoding="UTF-8"?> <profile name="calendarfeed" type="client" > </profile>
Just create a client profile (a shared library acts as a client)
/etc/sync/profiles/service/calendarfeed.xml
<?xml version="1.0" encoding="UTF-8"?> <profile name="calendarfeed" type="service"> <key name="destinationtype" value="offline"/> <profile name="calendarfeed" type="client" > </profile> </profile>
Create a service profile and indicate that it can work in offline mode. That is, our plug-in does not need an internet connection to work successfully (even though the Harmattan documentation states that this is not possible, in fact it works). The client profile is also indicated.
/etc/sync/profiles/sync/calendarfeed.xml
<?xml version="1.0" encoding="UTF-8"?> <profile name="calendarfeed" type="sync" > <key name="displayname" value="Calendar Feed"/> <key name="enabled" value="true" /> <key name="use_accounts" value="false" /> <key name="hidden" value="true" /> <profile type="service" name="calendarfeed" > </profile> <schedule enabled="true" interval="20" days="1,2,3,4,5,6,7" syncconfiguredtime="" time=""> </schedule> </profile>
Create a sync profile. Content node
profile :
- displayname - the name that will be displayed in the UI
- enabled - whether or not the profile is activated
- use_accounts - whether you need to use the account framework
- hidden - is this profile hidden in 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, he will be polled every 20 minutes regardless of the day of the week.
But what to do if we need to be polled once every half hour on weekends and on weekday nights, and once every 15 minutes on weekdays? With half an hour 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 subdirectory:
<rush begin="00:00:00" enabled="true" interval="15" end="17:00:00" days="1,2,3,4,5"/>
Shared library
In the library, we need exactly one class-successor
Buteo :: ClientPlugin and two extern functions for creating and deleting a plugin.
class CalendarFeedPlugin : public Buteo::ClientPlugin { Q_OBJECT public: 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); private: 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 represents a class close to the minimum required (the minimum will be if you remove the sections
protected slots and
private ). All the rest is redefinition of virtual methods (mostly pure virtual).
virtual bool init ();
Called at the very beginning and needed to check whether the plugin can initialize itself. If successful, returns true and the process goes on.
virtual bool uninit ();
Called before the plug-in is unloaded, cleaning usually occurs here.
virtual bool startSync ();
The method checks whether it can (and should) start the update and start it (asynchronously). Returns the result of the success of the start of the update.
virtual void abortSync (Sync :: SyncStatus status = Sync :: SYNC_ABORTED);
Called when the user 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 deleting an account. In our case, again, it is not needed (although it is written in the documentation that it may be possible to use it later).
virtual void connectivityStateChanged (Sync :: ConnectivityType type, bool state);
Called when the connection status has changed. If the plugin does not work with the network, this slot will also be empty.
It turns out that the most interesting method is startSync (). And right. In it, we run through a timer (that is, asynchronously) a method that adds our item to the Feed. All other methods are mostly service.
Localization SyncFW plugin
Naturally this small library does not know anything about any localizations there. Therefore, you need to manually poke it into the desired localization (the language will be picked up by itself, the main thing is to indicate the necessary set of .qm files).
MLocale locale; locale.installTrCatalog("calendarfeed");
Refresh button update
As is the case with the update without the presence of the Internet, the documentation tells us that the Refresh button is able to update only embedded applications (read: Twitter, Facebook, RSS Feeds, AP Mobile). But since we go our own way, we will try to verify this statement ourselves.
In the base of Feed (it is in the file
/home/user/.config/meegotouchhome-nokia/eventsfeed.data and is the usual Sqlite base) we see three tables: events, images, refreshactions. The first two of us in this case are of little interest, but the third is very interesting. It has only one attribute -
action . Since the name is already familiar to us by the addition of 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 determined how to attach to the button, but how to make our plugin be called up outside of its schedule? For this there is a dbus-method
com.meego.msyncd / synchronizer com.meego.msyncd.startSync with one argument - the name of the client profile.
There is a small detail left ... How to add your action to this table? Well, not an SQL query, in fact. The benefit next to addItem in dbus is the
addRefreshAction method, to which you can pass a string with a signature.
A little later we will find a place to call
addRefreshAction . For now, continue with the application itself.
Applet for customization
We need settings for our application. The logical question is where to put them? To make a separate application and put next to others? No, not an option. There are already too many of them in the Launcher. Another one that will be used extremely rarely (about 1 time for each update) is clearly superfluous. In this case, we can be helped by the possibility of adding your applet to the Settings hierarchy. And there are no unnecessary icons on the screen and you can add a call button next to the on / off display of Twitter and Facebook in Feed.
Creating an applet
MeeGo Harmattan provides 3 ways to create an applet that can be enabled in Settings:
- Declarative - the set of settings is described by the xml file.
- Binary - a shared library is created with an inheritor from DcpAppletIf
- External - a normal application is used.
The third option is not at all good (and by the way is not recommended in the documentation), as it can cause delays in the transition 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 it in 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 the name and where it lies (in general, quite a standard target .desktop file)
- /usr/share/duicontrolpanel/uidescriptions/calendarfeed.xml - description of the set of settings that are displayed on the screen
Let's sort them in turn.
/usr/share/duicontrolpanel/desktops/calendarfeed.desktop
[Desktop Entry] Type=ControlPanelApplet Name=Calendar Feed X-logical-id=calendar_feed_title X-translation-catalog=calendarfeed Icon= X-Maemo-Service=com.nokia.DuiControlPanel X-Maemo-Method=com.nokia.DuiControlPanelIf.appletPage X-Maemo-Object-Path=/ # this has to be identical to the value in Name X-Maemo-Fixed-Args=Calendar Feed [DUI] X-DUIApplet-Applet=libdeclarative.so [DCP] Category=Events Feed Order=100 Part=calendarfeed.xml Text2=Calendar events at Feed screen Text2-logical-id=calendar_feed_subtitle
The first section is standard. Except for the X-Maemo- * parameters, which describe which dbus method and with which arguments to call to open this applet.
Section DUI describes how to connect the shared library. In our case (declarative applet) this is always libdeclarative.so. In the case of a binary applet, there will be the name of a shared library with the successor DcpAppletIf.
The DCP section contains information about where, to which place and what to insert. In our case we get into the settings of the Events Feed (in the same place where the Twitter and Facebook switches are). Text2 is the second line (there is usually a description or the current state of the item).
/usr/share/duicontrolpanel/uidescriptions/calendarfeed.xml
<settings> <group title="calendar_feed_setting_group_behavior"> <boolean key="/apps/ControlPanel/CalendarFeed/EnableFeed" title="calendar_feed_setting_publish_to_feed"></boolean> <boolean key="/apps/ControlPanel/CalendarFeed/FillWithFuture" title="calendar_feed_setting_fill_with_future"></boolean> <boolean key="/apps/ControlPanel/CalendarFeed/LimitFuture" title="calendar_feed_setting_limit_future"></boolean> <integer key="/apps/ControlPanel/CalendarFeed/LimitDaysSize" title="calendar_feed_setting_future_limit_size" min="1" max="31"></integer> </group> <group title="calendar_feed_setting_group_display"> <integer key="/apps/ControlPanel/CalendarFeed/FeedSize" title="calendar_feed_setting_events_number" min="1" max="5"></integer> <boolean key="/apps/ControlPanel/CalendarFeed/ShowCalendarBar" title="calendar_feed_setting_show_calendar_bar"></boolean> <text key="/apps/ControlPanel/CalendarFeed/DateFormat" title="calendar_feed_setting_date_format">MMM, d</text> <boolean key="/apps/ControlPanel/CalendarFeed/HighlightToday" title="calendar_feed_setting_highlight_today"></boolean> </group> </settings>
You will laugh, but this example lists all the types of nodes except one that can be in the declarative applet.
For all types of nodes two parameters are standard:
- key - the key in GConf
- title is the name displayed in the UI. You can set the id of the translated string (as was done in the example)
Yes! Surprise surprise! Qt-shny MeeGo Harmattan is all about using GConf for storing internal settings. By the way, it is the settings from GConf that fall into backup.
Group - serves as a sort of group box (an example of the display in the second screenshot. There are two groups: Behavior and Appearance). Although it is written in the documentation that there are no parameters, nevertheless the title works as it should.
Boolean - switch. The most ordinary ...
Integer - a slider to select an integer value. In addition to the standard, there are still two parameters: min and max. They describe the range for the slider.
Text -
text field. By the way, for this type, the title is also not stated in the documentation, but you, I think, have already understood that it will work.
Selection is a group of buttons, each of which is described by a subnote of an option with one parameter - key, which in this case plays the role of the value displayed in UI. Each option should have a text with a number (which will be saved in GConf).
Using GConfItem
Although GConf is used, nevertheless there is a Qt-shny 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 the nature of gconf and qvariant):
- QVariant :: Invalid - missing gconf parameter
- QVariant :: Int , QVariant :: Double , QVariant :: Bool are converted to their equivalent types.
- QVariant :: String is only supported with utf8 encoding.
- QVariant :: StringList is 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 appropriate, 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(); else fillConfItem.set(false);
Reading calendar data
In fact, the most important part of the application. In the end, we after all deduce events from there. Theoretically, everything should work through Qt Mobility and, moreover, work ideally or close to it. After all, MeeGo uses at its core Qt. Nokia, which released n9 is essentially the owner of Qt. Everything should be integrated as much as possible, is not it? Schchazz, aha. As always a pig is planted here. But for this and the post, strictly speaking. 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 . No matter what is on the calendar is set. They are plagued by vague guesses that the opposite will be true (that is, the creation of an allDay event through QOrganizer will also create something wrong in the calendar). But nevertheless, there is a heuristic way to determine whether the event is
allDay or not.
if (!isAllDay && startDateTime.time().hour() == 0 && startDateTime.time().minute() == 0 && endDateTime.time().hour() == 0 && endDateTime.time().minute() == 0 && startDateTime.date().addDays(1) == endDateTime.date()) isAllDay = true;
ToDo items
QOrganizer knows that there are ToDo elements. He even keeps an expiration date for them. That's just through him it is impossible to know whether ToDo is done or not. Just impossible. There is a flag in the QOrganizer itself, but it is not cocked when the QOrganizer receives data from the underlying level. Unlike the previous problem, I do not have a 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 shoals found, a third was also found. Much more fun and exotic. The essence of what. In the data structure in which the calendar is stored in the system, there is a field in which it is recorded in which timezone the event is made. That is, if there are two events with the same values in the Start Date field, it is not a fact that they start at the same time. To correctly determine the time you need both fields. Accordingly, if we work with some exported calendar that was created in UTC, or the calendar is shared, synchronized with CalDAV, and people from different time zones work with it, then we will get incorrect displayed time.
The calendar system is built on two levels (high and low). QtMobility sits on high with a QOrganizer module, which is convenient to use, but is subject to the above-described phenomenon (it simply does not realize the existence of a timezone field). On the low sits mKCal, which always returns correct data, but is not very convenient in operation.
Initially Calendar Feed was 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. . QOrganizer, , mKCal. ( ). .
, : « ? mKCal?». , . . , , . , , , .
, mKCal ( ) .
, . . .pro-
TEMPLATE = lib TARGET = calendarfeed-client VERSION = 0.3.0 DEPENDPATH += . INCLUDEPATH += . \ /usr/include/libsynccommon \ /usr/include/libsyncprofile \ /usr/include/gq \ /usr/include/mkcal \ /usr/include/kcalcoren LIBS += -lsyncpluginmgr -lsyncprofile -lgq-gconf -lmkcal CONFIG += debug plugin meegotouchevents CONFIG += mobility MOBILITY += organizer QT += dbus #input SOURCES += \ calendarfeedplugin.cpp HEADERS +=\ calendarfeedplugin.h QMAKE_CXXFLAGS = -Wall \ -g \ -Wno-cast-align \ -O2 -finline-functions #install 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
shared- ( SyncFW, ), calendarfeed-client ( SyncFW).
.
meegotuchevents MEventFeed ( dbus, ).
qdbus mobility-organizer , .
.
install.
xml desktop ( )
/usr/share/l10n/meegotouch .
calendarfeed_ru.qm , calendarfeed , desktop-
X-translation-catalog .
SyncFW . . . Feed . refreshAction Refresh .
, deb-:
postinst
, ( PR1.1, PR1.0 ), refreshAction, Feed.
prerm
Feed.
, , , shared , ( ). .
Conclusion
Yes. - . , , , . :
, , , . , . , - , . , « » ( ).
:
, . That's all folks!