Некоторое время назад передо мной была поставлена задача по определению смены местоположения пользователя на карте. По результатам эксперимента в статье, для этих целей, по точности определения и энергоэффективности, прекрасно подходит Google Services Geofences.


image


Как работать с Geofences подробно рассмотрено в единственном русскоязычном примере по использованию Location APIs в статье на хабре, но с тех пор прошло уже 2 года, и информация сильно устарела.


Пример автора на github, к сожалению, даже не компилировался, поэтому я решил его завести под свежие версии библиотек. На мое удивление, изменений в API между com.google.android.gms:play-services:4.0.30 и com.google.android.gms:play-services:8.4.0 оказалось много! Собственно, о них дальше и пойдет речь в статье.


Код

Обновленный пример на github (на момент написания статьи автор оригинального примера не принял pull request).


Так в чем же отличия?


Для начала желательно ознакомиться с оригиналом.


В самой концепции ничего не поменялось, изменились только ответственные классы.
Так, вместо LocationClient имеем api.GoogleApiClient, callback'и из GooglePlayServicesClient также переехали в api.GoogleApiClient, вместо new LocationClient(this, this, this) появился удобный билдер:


    new GoogleApiClient.Builder(this)
        .addApi(LocationServices.API)
        .addConnectionCallbacks(this)
        .addOnConnectionFailedListener(this)
        .build();

Немаловажным отличием является то, что добавлением и удалением геозон mGoogleApiClient занимается не напрямую, а через LocationServices.GeofencingApi.


    GeofencingRequest build = ...
    LocationServices.GeofencingApi.addGeofences(mGoogleApiClient, builder.build(), getPendingIntent())
            .setResultCallback(new ResultCallback<Status>() {
                @Override
                public void onResult(@NonNull Status status) {
                    if (status.isSuccess()) {
                        String msg = "Geofences added: " + status.getStatusMessage();
                        Log.e("GEO", msg);
                        Toast.makeText(GeofencingService.this, msg, Toast.LENGTH_SHORT)
                                .show();
                    }
                    GeofencingService.this.onResult(status);
                }
            });

Также изменился первый параметр: вместо списка геозон передается GeofencingRequest, который можно получить через специальный билдер:


    GeofencingRequest.Builder builder = new GeofencingRequest.Builder();
    builder.setInitialTrigger(GeofencingRequest.INITIAL_TRIGGER_ENTER);
    builder.addGeofences(mGeofenceListsToAdd);
    GeofencingRequest build = builder.build();

Одна из возможностей нового билдера — управление поведением геозон в момент добавления. Например, в комментариях к оригинальной статье спрашивали про возможность срабатывания триггера Exit geofence для случая, когда девайс находится снаружи зоны в момент ее установки. Теперь это можно сделать передав флаг GeofencingRequest.INITIAL_TRIGGER_EXIT через метод setInitialTrigger (int initialTrigger), по умолчанию флаги GeofencingRequest.INITIAL_TRIGGER_ENTER и GeofencingRequest.INITIAL_TRIGGER_DWELL. Флаги можно комбинировать между собой.


Кроме этого был убран последний параметр-callback, теперь вместо него LocationServices.GeofencingApi.addGeofences возвращает PendingResult, с помощью которого можно, блокируя поток, ожидать результат или получить ответ асинхронно, с помощью callback метода setResultCallback(..). В любом случае результатом будет статус операции добавления\удаления геозоны. Данный callback заменяет собой OnAddGeofencesResultListener или onRemoveGeofencesByRequestIdsResult из оригинальной статьи.


Удалить не нужные геозоны можно через методы:


    LocationServices.GeofencingApi.removeGeofences(mGoogleApiClient, /*PendingIntent или список id геозон*/)

Изменения в ReceiveTransitionsIntentService


Если раньше для обработки результатов срабатывания триггеров на геозоне использовались статические методы из класса LocationClient, которые требовали в качестве параметра пришедший Intent, то сейчас этим занимается GeofencingEvent, который имеет одноименные методы для выполнения той же работы. Получить его можно следующим образом:


GeofencingEvent geofencingEvent = GeofencingEvent.fromIntent(intent);

Создания самих геозон осталось без изменений и происходит через Geofence.Builder.


Еще одним из новшеств является то, что после срабатывания триггера геозона удаляется автоматически, таким образом нам не нужно больше убирать их самостоятельно!


Так же в код примера я добавил еще одну кнопку, которая ставит геозону с триггером на выход из нее.


Несколько советов по работе с геозонами


  • Для тестирования я выбрал эмулятор Genymotion, но при попытке установить геозону LocationServices выдавал ошибку status code = 1000 (GEOFENCE_NOT_AVAILABLE). Решение этой проблемы нашлось на stackoverflow


  • Если выставить в качестве условия срабатывания триггеры GeofencingRequest.INITIAL_TRIGGER_EXIT и GeofencingRequest.INITIAL_TRIGGER_ENTER или Geofence.GEOFENCE_TRANSITION_ENTER и Geofence.GEOFENCE_TRANSITION_EXIT, то сработает только одно условие из каждой пары, после чего зона будет удалена


  • Для работы с Rx можно использовать эту библиотеку, тогда весь процесс по добавлению\удалению сводится к коду:


    GeofencingRequest.Builder builder = new GeofencingRequest.Builder()
    ...
    new ReactiveLocationProvider(context).
    locationProvider.addGeofences(getPendingGeoIntent(),builder.build())
    .subscribe(status -> {
        if (status.isSuccess()) {}
    };

  • К сожалению, если просто изменять координаты через встроенную в Genymotion карту, то триггеры не срабатывают. Но через lockito все отлично работает!

Ссылки:


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


  1. rude
    21.04.2016 16:44

    на последнем эмуляторе из SDK, geo-fence срабатывает при эмуляции локации GPS


    1. Vilkaman
      21.04.2016 17:47

      Если изменить координаты в ручном режиме, не через сторонние приложения?


      1. rude
        21.04.2016 20:26

        Да, если менять вручную. Например вот так: imageNexus 5X API23 with Google API

        Сначала ставим координаты вне geo-fence локации, а потом меняем на координаты в радиусе нашей geo-fence.


  1. furyon
    22.04.2016 08:30

    Недавно была необходимость решать подобную задачу, после различных попыток остановился на pathsense.com, довольно простой API, неплохая точность, батарейку жрет также как geofences. Настроил работу в сервисе, при входе в зону вылетал пуш даже при закрытом приложении.


    1. Vilkaman
      22.04.2016 08:43

      Спасибо, не знал о таком! Если верить табличке сравнения на их сайте, но это решение действительно лучше. Однозначно стоит попробовать!
      Но я думаю, если на телефоне будет установлено несколько различных приложений, которые используют pathsense.com то расход батареи будет больше, за счет независимой обработки в каждом приложении. Что думаете?


      1. furyon
        22.04.2016 09:02

        Я в конечно счете после долгого тестирования отказался как от pathsense так и geofence из-за расхода батареи. Тратилось до 10% если постоянно включено. Реализовал так, раз в несколько часов подгружается список координат с радиусами, и сервисом раз в минуту проверять кто вошел, сейчас по результатам тестов батарейки есть почти в 10 раз меньше, но разумеется от такого способа свои проблемы, но меня все устраивает.


        1. Vilkaman
          22.04.2016 09:23

          10% за какой период? Для своей задачи, я использовал geofance немного не по назначению, для определения что пользователь просто начал двигаться (планирую написать об этом еще одну статью) и по сравнению с другими подходами (прямую проверку GPS координат, google services activity recognition..) такой расход батарее заказчика устраивал)


          1. furyon
            22.04.2016 09:29

            Поскольку мои geofences работали в фоне всегда, я говорю про 10 процентов от общего заряда батареии. Там на pathsense написано что они пожирают около 0.7% в час.


  1. Harco
    22.04.2016 08:30

    Geofence всё ещё имеет проблемы, так например не всегда срабатывает триггер при выходе из гео-зоны, на реальных устройствах, поэтому надеяться на его работу неблагодарное занятие.


    1. Vilkaman
      22.04.2016 08:45

      Как часто это происходит? Можно ли это как-то повторить? или это рандомный баг, который например, повторяется только где-нибудь в лесу?


      1. Harco
        22.04.2016 08:53

        Да, в официальной документации написано, что если вы находитесь где-то за городом, то триггеры не будут срабатывать. В моём же случае они не срабатывали в городе, причём на разных устройствах в случайном порядке которые находились рядом, закономерности никакой. furyon пишет про pathsense сервис, да он уменьшает количество не срабатываний, но напирмер на nexus 5 он тоже не всегда отрабатывает при выходе из зоны.


  1. andreich
    24.04.2016 10:22

    Мне кажется, не стоит удивляться, что между четырьмя мажорными версиями много изменений в api


    1. Vilkaman
      24.04.2016 11:40

      Согласен. Не стоит удивляться!