Pull to refresh

Calendar Feed для N9: что это и как это разрабатывалось

Reading time20 min
Views1.9K
Этот пост участвует в конкурсе „Умные телефоны за умные посты“.

Nokia N9 — приятный девайс во многих отношениях. Но из коробки в нем нет одного очень важного функционала. Нельзя, бросив один взгляд на домашний экран телефона, понять какие впереди ждут события, прописанные в календаре. Чтобы исправить эту досадную проблему мною было разработано приложение Calendar Feed (OVI Store, исходники). Под катом я расскажу поподробнее о самом приложении (немного) и о том, как оно создавалось (большая часть поста).

Осторожно, там много текста. Если готовы, то…

Итак, приложение Calendar Feed. Оно целиком интегрируется в ОС и вызывается опять же через ОС, а не самим пользователем (хотя пользователь может и поторопить систему с обновлением данных приложения с помощью кнопки Refresh). Настройки полностью интегрированы в систему и выглядят как ее часть. Во время работы приложение периодически (раз в 20 минут) выкладывает в Feed (Канал в русской версии) элемент с ближайшими событиями из календаря, удаляя при этом старое. Этот элемент всегда является верхним (его время задается как текущее плюс одни сутки). Настроек у приложения немного (оно еще очень молодое и умеет немного), но их достаточно для полноценной работы базового функционала. Можно настроить количество событий в элементе, дополнять или нет сегодняшние события будущими (если сегодняшних мало) и ограничение (с возможностью отключения) в днях для будущих событий (чтобы не показывать события из следующего года или месяца).
Скриншоты


Ну это все лирика и не так интересно, как собственно разработка этого приложения. В конце концов Хабр — технический ресурс. Так что разберемся с тем, как же это все разрабатывалось поподробнее.

Будут разобраны следующие моменты:

Disclaimer:
Документация по Harmattan очень скудна и не блещет полнотой, поэтому большую часть, того что здесь написано пришлось выяснять методом тыка и копанием в бинарниках. Возможно какие-то пути неоптимальны. Если вы знаете более простой способ сделать что-либо описанное в этой статье, напишите это в комментариях.

Вывод событий на экран


Самым главным и базовым функционалом приложения является добавление элементов в Feed. Тут существуют два с половиной пути:
  • простой — через класс MEventFeed;
  • посложнее — через DBus;
  • половинчатый — существует обертка над DBus, написанная Йоханом Паулом (Johan Paul), ее можно посмотреть по адресу: gitorious.org/libharmattaneventview

Последний вариант мы рассматривать не будем, а рассмотрим первые два.

Класс MEventFeed

Этот вариант предлагается документацией по Харматтану. Он прост как топор. Есть класс MEventFeed, у которого есть 2 метода (хотя по онлайн-документации их и три, но в таргете в QtSDK третий недоступен): addItem и removeItemsBySourceName. Первый метод добавляет элемент в Feed, второй удаляет все элементы с одинаковым источником.
qlonglong addItem(const QString &icon, // Иконка (отображается слева)
                  const QString &title, // Заголовок (сверху болдом)
                  const QString &body, // Собственно сам текст
                  const QStringList &imageList, // Список изображений, которые будут выведены под текстом (используется например Facebook'ом)
                  const QDateTime &timestamp, // Временная метка (именно через нее осуществляется вечное верхнее положение)
                  const QString &footer, // Футер (отображается снизу, рядом с временем)
                  bool video, // Флаг является ли элемент в imageList ссылкой на видео (в этом случае элемент в этом списке может быть только один, остальные будут проигнорированы)
                  const QUrl &url, // Урл, по которому перейти при нажатии на элемент
                  const QString &sourceName, // Id источника
                  const QString &sourceDisplayName // Название источника (будет выведено на экран в некоторых случаях. Например, при долгом нажатии на элемент)
                 );

Первый раз когда я увидел ЭТО, мое лицо сильно вытянулось. Неужели нельзя было сделать небольшой класс с геттерами и сеттерами и его передавать? Ну да ладно, что дали, с тем и работаем. В комментариях указано что обозначает каждый параметр. Метод возвращает либо id элемента, либо -1 (если что-то пошло не так. Чаще всего, это некорректно заданные параметры).
void removeItemsBySourceName(const QString &sourceName);

Ну а тут все вообще просто. Передаем тот же id, что указывали в предыдущем методе, и все элементы с этим id удаляются.

Этот путь прост и удобен, но он ограничивает нас в возможностях. Дело в том, что кроме перехода по урлу еще возможен вызов dbus-метода. Но с помощью этого класса нельзя задать сигнатуру вызываемого метода, поэтому мы рассмотрим более универсальный способ работы с Feed — через 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)));

Здесь кода побольше, но есть необходимая нам возможность задания свойства action (в котором и хранится сигнатура dbus-метода, но об этом позже). Задать можно все параметры, которые были в MEventFeed:addItem и плюс action. Единственное, что необходимо помнить — передача временной метки. Передается она строкой в виде "ГГГГ-ММ-ДД чч:мм:сс".
Иконки можно использовать системные, передав вместо пути к иконке ее id (чем и пользуется Calendar Feed. Используется иконка с id icon-l-calendar-12, где вместо 12 подставляется число первого события в списке).

Также использование DBus дает возможность удалять и обновлять элемент по id (что отсутствует в MEventFeed).

Особенности хранения DBus сигнатур в недрах Harmattan

Над разгадыванием этого пазла мне пришлось потрудиться пару вечеров, но в итоге (с помощью hexdump, strings и такой-то матери) я смог понять, как же надо передавать сигнатуру метода, чтобы она была корректно вызвана.

У нас есть следующая сигнатура:
com.nokia.Calendar / com.nokia.maemo.meegotouch.CalendarInterface.showMonthView 2011 11 25
При вызове этого метода с этими параметрами на девайсе откроется календарь в режиме просмотра месяца с выбранной датой 25 ноября 2011.

Чтобы вызвался этот метод, нам нужно в action элемента положить следующую строку:
com.nokia.Calendar / com.nokia.maemo.meegotouch.CalendarInterface showMonthView AAAAAgAAAAfb AAAAAgAAAAAL AAAAAgAAAAAZ

Ну с пробелом между интерфейсом и названием метода все понятно. В конструкторе QDBusMessage они тоже разделены на два разных параметра. Но что за три странных строковых аргумента вместо трех чисел? Все очень просто, когда знаешь, хехе (я убил 2 или 3 вечера копаясь в бинарях и dbus-методах). Это Base64 от сериализованного в QDataStream объекта QVariant.

Хелпер-метод для генерации таких строк:
QString CalendarFeedPlugin::base64SerializedVariant(const QVariant &value) const
{
    QByteArray ba;
    QDataStream stream(&ba, QIODevice::WriteOnly);
    stream << value;
    return ba.toBase64();
}

Есть подозрение, что такая нотация используется везде в Harmattan, где хранится сигнатура dbus-метода.

SyncFW


Мы научились создавать элемента Feed'а. Теперь нам нужно каким-то образом периодически его туда добавлять. Желательно при этом не городить демоны, которые будут кушать батарею, а максимально интегрироваться в систему, чтобы она сама решала когда обновить.

Для этой цели в Harmattan есть фреймворк SyncFW, к которому можно написать плагин. Этот плагин будет опрашиваться в соответствии с заданными параметрами. Плагин логически делится на две части: метаданные и shared библиотека.

Важный момент: в PR1.0 нужна перезагрузка девайса после установки нового плагина. В PR1.1 эту проблему можно обойти (см. раздел Установочные скрипты). Но обновление плагина всегда требует перезагрузки. Без нее просто будет использована старая shared библиотека (возможно как-то ее можно выгрузить без перезагрузки, но данного способа я не знаю) с новыми метаданными.

Другой важный момент: везде, где в примерах используется имя calendarfeed, подразумевается, что там должно быть название плагина (при этом библиотека должна называться lib<plugin_name>-client.so, то есть в нашем случае это libcalendarfeed-client.so). В названии плагина не должно быть дефисов. Идеально — только латинские буквы, использование цифр думаю тоже разрешено (не проверял).

Метаданные

Для описания плагина нам нужны следующие файлы:
  • /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>

Просто создаем профиль клиентской части (в роли клиента выступает shared библиотека)

/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>

Создаем профиль сервиса и указываем, что он может работать в offline режиме. То есть нашему плагину не нужно подключение к интернету для успешной работы (хоть в документации по Harmattan и указано что это не возможно, на самом деле это работает). Также указывается профиль клиента.

/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>

Создаем профиль синхронизации. Содержимое ноды profile:
  • displayname — имя, которое будет отображаться в UI
  • enabled — активирован или нет профиль
  • use_accounts — нужно ли использовать фреймворк аккаунтов
  • hidden — спрятан ли данный профиль в UI
  • profile — профиль сервиса

Самая главная часть здесь это, конечно же, расписание. Именно по информации из этой ноды наш плагин будет опрашиваться. В данном случае он будет опрашиваться каждые 20 минут независимо от дня недели.

Но что делать, если нам нужно чтобы в выходные и в будни вечером-ночью он опрашивался раз в полчаса, а в будни днем раз в 15 минут? С получасом понятно. Меняем 20 на 30 и все ок. Чтобы задать дополнительное время расписания, нужно включить ноду rush, как подноду schedule:
<rush begin="00:00:00" enabled="true" interval="15" end="17:00:00" days="1,2,3,4,5"/>


Shared библиотека

В библиотеке нам нужен ровно один класс-наследник Buteo::ClientPlugin и две extern функции для создания и удаления плагина.
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 );

В этом хедере представлен близкий к минимально необходимому класс (минимальный будет если убрать разделы protected slots и private). Все остальное это переопределение virtual методов (по большей части pure virtual).

virtual bool init();
Вызывается в самом начале и нужен для проверки может ли плагин инициализировать сам себя. В случае успеха возвращается true и процесс идет дальше.

virtual bool uninit();
Вызывается перед выгрузкой плагина, тут обычно происходит очистка.

virtual bool startSync();
Метод проверяет может ли (и должен ли) он стартовать обновление и стартует его (асинхронно). Возвращает результат успеха старта обновления.

virtual void abortSync(Sync::SyncStatus status = Sync::SYNC_ABORTED);
Вызывается когда пользователь отменил обновление. Нужно для работы с аккаунтами (где есть такая кнопка), в нашем же случае возможности отменить обновление у пользователя нет.

virtual Buteo::SyncResults getSyncResults() const;
Возвращает результаты последнего обновления.

virtual bool cleanUp();
Вызывается при удалении аккаунта. В нашем случае опять же не нужен (хотя в документации написано что возможно какое-нибудь применение ему и найдется позже).

virtual void connectivityStateChanged(Sync::ConnectivityType type,bool state);
Вызывается, если изменилось состояние подключения. Если плагин не работает с сетью, то этот слот также будет пустым.

Получается, что самый интересный метод это startSync(). И верно. В нем мы запускаем через таймер (то есть асинхронно) метод, добавляющий наш элемент в Feed. Все остальные методы по большей части служебные.

Локализация SyncFW плагина

Естественно эта небольшая библиотека ничего не знает о каких-то там локализациях. Потому нужно тыкнуть ее вручную в нужную локализацию (язык подцепится сам, главное указать нужный набор .qm файлов).
MLocale locale;
locale.installTrCatalog("calendarfeed");
//...
itemArgs.insert("title", locale.translate("", "calendar_feed_item_title"));


Обновление по кнопке Refresh

Как и в случае с обновлением без наличия интернета, документация говорит нам, что кнопка Refresh умеет обновлять только встроенные приложения (читай: Twitter, Facebook, RSS Feeds, AP Mobile). Но раз уж мы идем своим путем, то попробуем все же сами проверить это утверждение.

В базе Feed (она находится в файле /home/user/.config/meegotouchhome-nokia/eventsfeed.data и представляет из себя обычную Sqlite базу) мы видим три таблицы: events, images, refreshactions. Первые две нас в данном случае мало интересуют, а вот третья очень интересна. У нее есть всего один атрибут — action. Так как название нам уже знакомо по добавлению элемента Feed, попробуем положить сюда сигнатуру вызова dbus-метода (в описанном выше формате). И ведь работает! Документация опять оказалась не права. Ну да ладно, зато теперь мы можем реализовать столь нужный нам функционал. Мы ведь идем по пути максимальной интеграции в систему и будет очень странно, если приложение не будет реагировать на эту кнопку.

Хорошо, мы определили как привязаться к кнопке, но как заставить наш плагин вызваться вне своего расписания? Для этого есть dbus-метод com.meego.msyncd /synchronizer com.meego.msyncd.startSync с одним аргументом — именем профиля клиента.

Осталась маленькая деталь… Как добавить свой action в эту таблицу? Ну не SQL-запросом же в самом деле. Благо рядом с addItem в dbus есть метод addRefreshAction, которому можно передать строку с сигнатурой.

Чуть позже мы найдем место, откуда вызывать addRefreshAction. А пока продолжим с самим приложением.

Апплет для настройки


Нам нужны настройки для нашего приложения. Логичный вопрос — куда их положить? Делать отдельным приложением и класть рядом с другими? Нет, не вариант. Их и так слишком много в Launcher'е лежит. Еще одно, которое будет использоваться крайне редко (примерно 1 раз на каждый апдейт), явно лишнее. В этом случае нам на помощь приходит возможность добавления своего апплета в иерархию Settings. И иконки лишней на экране нет и можно добавить кнопку вызова рядом с с включением/выключением показа Twitter'а и Facebook'а в Feed.

Создание апплета

MeeGo Harmattan предоставляет 3 способа создания апплета, который можно включить в Settings:
  • Declarative — набор настроек описывается xml файлом
  • Binary — создается shared библиотека с наследником от DcpAppletIf
  • External — используется обычное приложение

Третий вариант совсем не хороший (и кстати не рекомендуется в документации), так как может вызвать задержки при переходе из Settings в приложение.
Второй вариант наиболее гибок, но сложнее первого.
Первый вариант минимально гибок, но при этом максимально прост.

Так как нам не нужны особо сложные настройки, то будем делать первым способом.

Для него нам потребуется два файла:
  • /usr/share/duicontrolpanel/desktops/calendarfeed.desktop — .desktop файл с описанием что, как называется и где лежит (вообщем вполне стандартная цель .desktop файла)
  • /usr/share/duicontrolpanel/uidescriptions/calendarfeed.xml — описание набора настроек, выводимых на экран

Разберем их по очереди.

/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

Первый раздел стандартный. Разве что кроме параметров X-Maemo-*, которые описывают какой dbus-метод и с какими аргументами вызывать, чтобы открыть этот апплет.

Секция DUI описывает какую подключать shared библиотеку. В нашем случае (декларативный апплет) это всегда libdeclarative.so. В случае бинарного апплета тут будет стоять имя shared библиотеки с наследником DcpAppletIf.

В секции DCP содержится информация о том куда, на какое место и что вставлять. В нашем случае мы влезаем в настройки Events Feed (там же где переключатели Twitter'а и Facebook'а). Text2 это вторая строка (там обычно идет описание или текущее состояние пункта).

/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>

Вы будете смеяться, но в этом примере перечислены все типы нод кроме одного, которые могут быть в декларативном апплете.

Для всех типов нод стандартны два параметра:
  • key — ключ в GConf
  • title — название, отображаемое в UI. Можно задать id переводимой строки (как и сделано в примере)

Да! Сюрприз-сюрприз! Весь из себя Qt-шный MeeGo Harmattan юзает GConf для хранения внутренних настроек. Кстати, именно настройки из GConf'а попадают в бекап.

Group — выполняет роль этакого групбокса (пример отображения на втором скриншоте. Там две группы: Behavior и Appearance). Хоть в документации и написано, что нет никаких параметров, тем не менее title работает как надо.
Boolean — переключатель. Самый обыкновенный…
Integer — ползунок для выбора целочисленного значения. Кроме стандартных еще есть два параметра: min и max. Они описывают диапазон для ползунка.
Text — поле для ввода текста. Кстати, для этого типа тоже в документации не заявлен title, но вы я думаю уже поняли что он будет работать.
Selection — группа кнопок, каждая из которых описывается поднодой option с одним параметром — key, который в данном случае выполняет роль выводимого в UI значения. У каждого option должен быть текст с числом (которое и будет сохранено в GConf).

Использование GConfItem

Хоть и используется GConf, тем не менее есть Qt-шный класс для работы с ним — GConfItem. Нам от него нужны два метода: value() и set(). Первый возвращает значение параметра в QVariant, второй меняет значение параметра.

При этом поддерживаются не все типы. Применяются следующие правила (из за особенностей gconf и qvariant):
  • QVariant::Invalid — отсутствующий параметр gconf
  • QVariant::Int, QVariant::Double, QVariant::Bool переводятся в эквивалентные им типы
  • QVariant::String поддерживается только с кодировкой utf8
  • QVariant::StringList — список строк в utf8.
  • QVariant::List список QVariant::Int, QVariant::Double, QVariant::Bool или QVariant::String (вернется как QVariant::StringList), но все элементы должны быть одного типа
  • Все остальные типы (с обеих сторон) игнорируются

В случае с декларативным апплетом это полностью подходит, так как апплет предоставляет строку, булево и целое число.
Пример работы с GConfItem
bool fillWithFuture = false;
GConfItem fillConfItem("/apps/ControlPanel/CalendarFeed/FillWithFuture");
QVariant fillVariant = fillConfItem.value();
if (fillVariant.isValid())
    fillWithFuture = fillVariant.toBool();
else
    fillConfItem.set(false);


Чтение данных из календаря


По сути самая главная часть приложения. В конце концов, мы ведь выводим события оттуда. Теоретически все должно работать через Qt Mobility и причем работать идеально или близко к этому. Ведь MeeGo использует в своей основе Qt. Nokia, которая выпустила n9 — по сути владелец Qt. Все должно быть интегрировано максимально, не правда ли? Щщазз, ага. Как всегда тут подложена свинья. Но для этого и пост, собственно говоря. Чтобы этих свиней описать для будущих поколений.

События на весь день

В календаре есть возможность задать событие на весь день (то есть без времени). В QOrganizer тоже есть параметр allDay у события. Который всегда false. Независимо от того, что там в календаре выставлено. Терзают смутные догадки, что наоборот будет справедливо (то есть создание allDay-события через QOrganizer тоже создаст что-нибудь не то в календаре). Но тем не менее, имеется эвристический способ определить является ли событие allDay или нет.
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 элементы

QOrganizer знает что бывают ToDo элементы. Он даже для них хранит дату окончания. Вот только через него невозможно узнать выполнено ToDo или нет. Просто невозможно. Флаг такой в самом QOrganizer есть, но вот он не взводится при получении данных QOrganizer'ом от нижележащего уровня. В отличие от предыдущей проблемы, решения через Qt Mobility у меня нет. И кажется мне, что его через QtMobility нет совсем (как минимум до следующей прошивки).

Таймзоны

Плюс к первым двум найденным косякам, обнаружился еще и третий. Гораздо более веселый и экзотичный. Суть в чем. В структуре данных, в которой хранится календарь в системе присутствует поле, в котором записано в какой таймзоне сделано событие. То есть если есть два события с одиаковыми значениями в поле «Дата начала», то не факт, что они начинаются в одно время. Для корректного определения времени нужны оба поля. Соответственно если мы работаем с каким-то экспортированным календарем, который создавался в UTC, или календарь общий, синхронизирующийся по CalDAV и с ним работают люди из разных часовых поясов, то мы получим некорректное отображаемое время.

Система работы с календарем построена на двух уровнях (высоком и низком). На высоком сидит QtMobility с модулем QOrganizer, который удобен в использовании, но подвержен вышеописанному явлению (он просто не подозревает о существовании поля с таймзоной). На низком сидит mKCal, который всегда возвращает корректные данные, но не очень удобен в работе.

Изначально Calendar Feed писался с использованием QOrganizer. Но после появления одного бага (критического, связанного с некорректным отображением времени в некоторых экзотических случаях) начался этакий переходный период. На данный момент в проекте используется оба уровня. Сначала собираются все события через 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/*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 \
    qtc_packaging/debian_harmattan/prerm

Мы собираем 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 — выполняется сразу после установки
  • prerm — выполняется сразу перед удалением


postinst
#!/bin/sh

/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 --dest=com.nokia.home.EventFeed --print-reply /eventfeed com.nokia.home.EventFeed.addRefreshAction 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

Здесь мы явно говорим, что мы поставили плагин (это работает только в PR1.1, в PR1.0 все равно придется перезагружать устройство), добавляем refreshAction, включаем наше приложение и вручную обновляем элемент в Feed.

prerm
#!/bin/sh

/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 --dest=com.nokia.home.EventFeed --print-reply /eventfeed com.nokia.home.EventFeed.removeItemsBySourceName string:'SyncFW-calendarfeed'

exit 0

Удаляем плагин и элемент из Feed.

Кстати, хоть указанный способ установки плагина и помогает при первой установке, но, при обновлении shared библиотеки, до перезагрузки все равно используется старая (она не выгружается из памяти). С этим сопряжены определенные неудобства при отладке.

Заключение


Да. Статья наконец-то закончилась. Я устал ее писать, а вы, я думаю, слегка устали ее читать. Но зато благодаря этой статье мы узнали:

На самом деле, конечно, я включил в этот пост далеко не все, с чем столкнулся при разработке. И далеко не все описал детально, пост и без того слишком длинный. Я преследовал цель показать как это можно сделать, а не показать какие-то теоретические сведения или путь, которым я шел к этим сведениям. Но если интересны и эти сведения, то вы можете прочесть несколько «путевых заметок» по ходу разработки этого приложения у меня в блоге (ссылка в профиле).

Ну и напоследок кучка ссылок:

Я надеюсь, вам было интересно. That's all, folks!
Tags:
Hubs:
Total votes 66: ↑51 and ↓15+36
Comments35

Articles