Недавно решил попробовать реализовать идею о том, как можно делиться местоположением через API ВКонтакте с друзьями в режиме, приближенном к реальному времени. На выходе получилось кроссплатформенное Qt-приложение для iOS/Android, веб-приложение для ВКонтакте и парочка pull request'ов для VK API. В этой статье я хотел бы поделиться некоторыми неочевидными моментами реализации, которые, может быть, будут кому-то интересны. Итак, заинтересовавшихся прошу под кат.
Зачем мне вообще это понадобилось
Рано или поздно дети взрослеют. Банальная истина. Вот и моя десятилетняя дочь в один прекрасный день заявила: “Папа, не вози меня больше на машине, я хочу ходить в школу самостоятельно!”. Ну что же, я счел требование справедливым, попросил двухнедельную отсрочку и начал подготовку.
Поскольку я имею некоторый опыт написания приложений, а дочь постоянно таскает в кармане iPhone SE, то в качестве подготовки было решено быстренько написать приложение, которое показывало бы, где дочь находится в данный конкретный момент. Да, я знаю, что сейчас таких приложений довольно много (даже в Google Maps недавно появился подобный функционал), и можно было использовать какое-то готовое решение, но мне было интересно написать что-то свое.
Почему ВКонтакте?
Поскольку связываться с обработкой и хранением чужих (в потенциально возможной перспективе) персональных данных на своем оборудовании со всеми вытекающими из этого “прелестями” мне бы не хотелось, я задумался, как бы обойтись без собственной серверной части. И тут меня осенило – ведь есть же такой монстр, как ВКонтакте! Он модный, мощный и со своим развитым API, а главное – в нем давно и плотно сидят все наши дети (нельзя сказать, чтобы мне это нравилось, но это реальность). Но черт возьми, Холмс, как мне запихнуть в него данные о местоположении так, чтобы, во-первых, они не показывались там, где не надо, а во-вторых, чтобы можно было регулировать доступ к этим данным, дабы нехорошие педобиры до них не добрались?
На помощь пришли заметки. Да-да, те самые викифицированные заметки, которые некогда (по слухам) были очень популярны, а сейчас находятся в загоне, по большей части перестали отсвечивать в лентах и переехали в отдельный раздел сайта, в который на хромой козе не попадешь. В них можно размещать произвольные текстовые данные, но главное – им можно назначать списки доступа, в которых перечисляются люди и группы, которые могут эту заметку видеть и комментировать.
Общая схема работы приложения в моих глазах стала выглядеть следующим образом:
- Создаем список друзей, с которыми нужно поделиться данными о местоположении (я назвал его «Доверенные друзья»);
- Создаем заметку с определенным именем, даем права на ее просмотр вышеупомянутому списку, записываем туда данные о местоположении и периодически их обновляем;
- Регулярно пробегаемся по списку доверенных друзей, проверяя, не появилась ли и у них заметка с этим определенным именем, если появилась, то пытаемся извлечь из ее содержимого данные о местоположении друга, и, если это удалось, то показываем его на карте;
- ???
- PROFIT!
Итак, идея есть, дело за малым — реализовать ее.
iOS
Поскольку дочь пользуется iPhone, логично было начать реализацию с iOS-версии. С прицелом на кроссплатформенность в качестве фреймворка был выбран Qt, потому что я с ним довольно давно и хорошо знаком, а в качестве движка для карты был выбран старый добрый Open Street Map, для которого в Qt Location есть плагин. Так как приложение изначально создавалось как open source, лицензионные ограничения Qt меня не пугали.
GUI был написан на QML, для работы с ВК я подключил и использовал штатный VK iOS SDK, он написан на Objective C, поэтому его интеграция никаких проблем не вызвала. Работа в фоновом режиме на iOS реализована через Significant Change Location Service. Для снижения энергопотребления приложение следит за активностью перемещений, и если понимает, что человек долго сидит примерно на одном месте (скажем, пришел в школу или в офис), то понижает требуемую точность определения геопозиции, вынуждая ОС переключиться на менее энергоемкие способы ее определения (как правило, по вышкам сотовой связи). Если же приложение понимает, что человек начал активно перемещаться, точность вновь поднимается.
Полный набор исходных текстов iOS-версии доступен в Git-репозитории SourceForge (сорри, что не на GitHub, там у меня тоже есть аккаунт, но по причинам исторического характера этот проект
Переопределение методов NSApplicationDelegate в Qt-приложении
iOS SDK от ВКонтакте требует добавления вызовов некоторых своих функций в методах application:didFinishLaunchingWithOptions: и application:openURL:options:. До какой-то версии Qt (по-моему, до 5.11) достаточно было создать категорию для QIOSApplicationDelegate примерно таким образом:
@interface QIOSApplicationDelegate : UIResponder <UIApplicationDelegate>
@end
@interface QIOSApplicationDelegate (QIOSApplicationDelegateVKGeoCategory)
@end
@implementation QIOSApplicationDelegate (QIOSApplicationDelegateVKGeoCategory)
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
[...]
}
- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options
{
[...]
}
@end
Но в последних версиях Qt QIOSApplicationDelegate уже имеет реализацию application:openURL:options:, поэтому вариант с категориями уже не прокатывает. Пришлось сделать наследника от QIOSApplicationDelegate и назначать делегата через setDelegate:
@interface VKGeoApplicationDelegate : QIOSApplicationDelegate
@end
@implementation VKGeoApplicationDelegate
- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options
{
[...]
}
@end
void InitializeVKGeoApplicationDelegate()
{
[[UIApplication sharedApplication] setDelegate:[[VKGeoApplicationDelegate alloc] init]];
}
int main(int argc, char *argv[])
{
[...]
InitializeVKGeoApplicationDelegate();
[...]
}
Обработка ошибок от VKRequest
Столкнулся с тем, что VKRequest при ошибке возвращал незаполненный (пустой) NSError. Сделал патч, который патчит чей-то предыдущий патч и pull request этого патча, но он пока что висит в нерассмотренных.
Android
Следующей была версия под Android. Код GUI был переиспользован из iOS практически полностью, для взаимодействия с ВК был использован опять же штатный VK Android SDK, взаимодействие с которым происходит через JNI, работа в фоновом режиме реализована в соответствии с заветами Google для подобных приложений — а именно, через Foreground Service. Само собой, реализована и логика по снижению энергопотребления, аналогичная используемой в iOS.
Полный набор исходных текстов Android-версии доступен опять-таки в Git-репозитории SourceForge, а вот и пара неочевидных моментов, с которыми я столкнулся в процессе реализации этой версии:
Как создать Android сервис на Qt
Для создания сервиса в Qt 5.10 появился класс QAndroidService, который нужно использовать вместо QGuiApplication. Можно собрать отдельные .so для activity и для service, а можно использовать один .so на все, а для того, чтобы код понимал, в каком режиме он работает, можно указать этот режим через ключ командной строки, примерно так:
<service android:name=".VKGeoService">
<meta-data android:name="android.app.arguments" android:value="-service"/>
</service>
int main(int argc, char *argv[])
{
if (argc == 1) {
QGuiApplication app(argc, argv);
[...]
} else if (argc == 2 && QString(argv[1]) == "-service") {
QAndroidService app(argc, argv);
[...]
} else {
return 0;
}
}
Странные «повисания» activity в onDestroy()
В процессе реализации сервиса выяснилась забавная проблема — QtActivity «висла» где-то в недрах своего onDestroy() при условии наличия работающего foreground service. По-видимому, Qt не ожидает, что после завершения activity от приложения может еще что-то оставаться. Проблема была решена разнесением activity и service по различным процессам через использование android:process в манифесте:
<service android:name=".VKGeoService" android:process=":VKGeoService">
[...]
</service>
и прибиванием процесса, в котором работает activity, в переопределенном onDestroy():
@Override
public void onDestroy()
{
[...]
/*
* This call hangs when foreground service is running,
* so we just kill activity process instead (service
* is running in a different process).
*
* super.onDestroy();
*/
Process.killProcess(Process.myPid());
}
Да, lint на это постоянно ругается, но как с этим справиться по-другому, для меня пока неочевидно, а запилить QTBUG с PoC на эту тему что-то руки пока не доходят.
Отсутствие вызова errorBlock при отмене VKBatchRequest
Я широко использую batch request'ы для облегчения нагрузки на серверы ВКонтакте, и именно под Android (под iOS все работает нормально) столкнулся с проблемой — в случае отмены VKBatchRequest errorBlock'и для отмененных запросов не вызывались. Исправил эту проблему в локальной версии библиотеки, сделал соответствующий патч и pull request этого патча, но он опять же пока что висит в нерассмотренных.
Заключение
iOS-версию Apple без проблем разместил в App Store, и она доступна там и по сей день, Android-версия прожила в Google Play некоторое время до момента ужесточения Google Play Policy (в ней прописали, что приложения, занимающиеся отслеживанием геопозиции, должны быть явно предназначены либо для семейного, либо для корпоративного использования), после чего мое приложение там благополучно заблокировали. На мою попытку апелляции ответственный сотрудник Google (а может, это и бот был, сейчас там уже не столь очевидно, кто именно отвечает на твои вопросы) непреклонно заявил, что «ну это приложение же МОЖЕТ БЫТЬ использовано НЕ ТОЛЬКО для семейного или корпоративного отслеживания», на что я не нашелся, что возразить — действительно, молотком ведь можно не только забивать гвозди, а еще и пробивать головы… Разместил Android-версию в Яндекс.Store и Amazon Appstore и в виде APK на сайте проекта.
Буду рад, если это приложение кому-то пригодится в качестве наглядного пособия для прояснения каких-то неочевидных моментов при написании Qt-приложения под iOS/Android, особенно связанных с реализацией Android-сервиса на Qt (функциональность эта относительно новая, примеров реализации, насколько я знаю, не так много). Также буду рад ответить на вопросы в комментариях, if any.
Автор: KanuTaH