Недавно решил попробовать реализовать идею о том, как можно делиться местоположением через 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, там у меня тоже есть аккаунт, но по причинам исторического характера этот проект хостится на SF). Вот пара неочевидных моментов, с которыми я столкнулся в процессе реализации:

Переопределение методов 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.

Комментарии (20)


  1. alexxxst
    02.01.2019 17:26

    Но зачем? В том же iOS есть системное приложение «Find Friends», которое делает ровно то же самое. Думаю, и в Android есть что-то подобное.


    1. KanuTaH Автор
      02.01.2019 17:36

      1. «Да, я знаю, что сейчас таких приложений довольно много (даже в Google Maps недавно появился подобный функционал), и можно было использовать какое-то готовое решение, но мне было интересно написать что-то свое».
      2. Не уверен, что Find Friends в iOS совместимо с «чем-то подобным» в Android.


  1. jevius
    02.01.2019 17:34
    +1

    Но ведь в Telegram есть ровно то что вам нужно!


    1. KanuTaH Автор
      02.01.2019 17:36

      1. «Да, я знаю, что сейчас таких приложений довольно много (даже в Google Maps недавно появился подобный функционал), и можно было использовать какое-то готовое решение, но мне было интересно написать что-то свое». Хобби у меня такое.
      2. В Телеграме, насколько я понимаю, эта функция ограничена по времени, и сделать ее постоянно работающей нельзя. Хотя, может быть, я и ошибаюсь.


      1. Gorthauer87
        04.01.2019 09:42

        Но можно же тогда свой урезаный телеграмм клиент сделать в теории чтобы он продлял локейшн нет? Тем более исходники самого клиента открыты


        1. KanuTaH Автор
          04.01.2019 13:54

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


    1. arcman
      03.01.2019 13:21

      Максимум на 8 часов, потом нужно заново разрешать.
      Нельзя сделать перманентным.


  1. merhalak
    02.01.2019 21:27

    Помню, когда-то в начале 10-х одноклассник показывал работу локатора на гуглокартах. Выглядело весьма впечатляюще, а потом вся эта функциональность куда-то слилась.

    Google, что с них взять.

    P.S. Про это знаю: support.google.com/maps/answer/7326816?co=GENIE.Platform%3DAndroid&hl=en



  1. tuxi
    02.01.2019 23:35

    Абстрагируясь от деталей реализации, хочу поделиться реальным опытом, что лучше всего такие трекеры гео позиции реализовывать на SMS сообщениях. Интернет слишком нежная субстанция в реальной жизни.


    1. KanuTaH Автор
      02.01.2019 23:41

      Насколько я знаю, в iOS штатными средствами доступ к SMS получить нельзя. А в условиях большинства российских городов интернет сейчас есть практически везде, где есть сотовый сигнал.


      1. tuxi
        02.01.2019 23:45

        iOS боль, согласен. Насчет доступности интенета, очень не соглашусь. Имею реальный опыт эксплуатации трекеров на порядка 20 грузовых авто в пределах Москвы и ближайшего МО. Плюс все тот же детский трекер. У меня знакомый в итоге под андроид переписал свой трекер используя СМС как транспорт.


        1. KanuTaH Автор
          02.01.2019 23:51

          Ну в целом SMS конечно выгоднее — вероятность доставки выше, особенно в области где-нибудь, можно эффективнее экономить батарею, отправляя геоданные только по запросу. Для Android SMS, конечно, хороший вариант.


  1. Vacxe
    03.01.2019 11:33

    Я конечно прошу прощения НО:
    1) Задача была поставлена как «следить за геолокацией ребенка». Вам не кажется что есть гораздо более изящные способы? Я просто не могу понять, как и зачем Вам пришло использовать в голову заметки во Вконтакте?

    2) Из любой статьи должен быть хоть какой то вывод или новое знание. Здесь же его абсолютно нет. Есть только ужасный пример «забивания гвоздей в землю с помощью замороженной рыбы».

    3) Просто оставлю это здесь. Фукнционал доступен больше 5 лет от Android, также поддерживается на iOS

    4) Мне кажется что развернуть сервер с парочкой методов на том же самом Express было бы гораздо проще (Ну строчек 20 бы заняло) и вы бы не зависели от 3rd party libraries

    5) Заключение что «из Google play приложение было удалено» — не самый лучший знак

    6) Пожалуй самое последнее, если пишите Pet-project то не нужно каждый раз стараться рассказать об этом IT сообществу.

    7) Если вносите изменения в 3rd party libraries, то пожалуй стоит задумать над сохранением предыдущего функционала

    8) Товарищ майор, хорошая попытка трекать в реальном времени


    1. KanuTaH Автор
      03.01.2019 11:40

      1. В статье написано, как и зачем.
      2. В статье написано, какое новое знание из нее можно почерпнуть. Например, для тех, кто хочет посмотреть, как писать Android-сервисы на Qt, есть живой рабочий пример.
      3. Отвечено неоднократно и в самой статье (конкретно про Google Maps, да), и в предыдущих моих комментариях другим людям, не вижу смысла повторяться.
      4a. Какие 3rd party libraries вы имеете в виду?
      4b. Вот тогда бы я точно был товарищем майором с непрозрачным для пользователя сервером :) И к тому же зависел бы еще и от этого сервера.
      5. В статье написано, почему именно оно было удалено.
      6. Если у этого pet project открытые исходники, из которых интересующиеся могут что-то новое почерпнуть для себя — почему нет? Например, я вижу, что на данный момент статья в закладках у 34 человек, значит, хотя бы 34 человека решили, что могут из этого проекта что-то почерпнуть для себя. Не так плохо.
      7. Какой именно «предыдущий функционал» и в каких именно «3rd party libraries» я не сохранил?
      8. Паранойя излечима :)


      1. Vacxe
        03.01.2019 12:01

        3rd party — VK API

        — if (mCanceled) return;
        + if (mErrorOccured) return; mErrorOccured = true;

        Вот вам не кажется что здесь вы все таки изменили функционал, который отвечал за cancel request'а? И что ваш новый функционал сработает только после получения второго исключения? То есть по сути вы добавили игнорирование исключений если их больше чем одно.


        1. KanuTaH Автор
          03.01.2019 12:12

          1. Ну, как сказать. Это официальный SDK от ВК.
          2. Конечно, изменил. В этом и суть. Раньше provideError() при отмене пакетного запроса просто сразу выходил, и не вызывал пользовательские обработчики onError() для упакованных в него подзапросов, хотя по описанию в коде VKRequest.cancel() он должен это делать («Cancel current request. Result will be not passed. errorBlock will be called with error code»). В общем, вам надо бы самому попробовать, как работает cancel() в случае пакетного запроса с моей правкой и без нее, тогда вам будет не «казаться», а вы будете точно знать, что происходит в том и в другом случае.

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


          1. Vacxe
            03.01.2019 13:05

            что VKBatchRequest изначально (почему-то) устроен так, что у него, так сказать, «одна ошибка на всех»

            Может быть ключевое слово «Batch» о чем то должно о чем то подсказать?


            1. KanuTaH Автор
              03.01.2019 13:08

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


    1. arcman
      03.01.2019 13:27

      6. Эта статья полезнее 90% материалов на хабре, почему нет?