Работа с геозонами (geofences) в Android


Добрый день, хабровчане. Сегодня я хотел бы рассказать о Location APIs в общем и о геозонах (geofences) в частности, которые были представлены на Google I/0 2013 (видео и презентация). Не смотря на то, что событие произошло более полугода назад, на хабре до сих пор нет вменяемой информации об этом (только одно упоминание). Постараюсь немного исправить ситуацию.

Что такое Location APIs?


Location APIs являются частью Google Play сервисов, которая предназначена для создания приложений работающих с местоположением устройства. В отличие от подобных функций в LocationManager, данные API отличаются улучшенным энергосбережением. В данный момент доступна следующая функциональность: определение местоположения устройства, работа с геозонами и распознавание активности пользователя. Определение местоположения позволяет балансировать между точностью определения и потреблением энергии, а также предоставляет доступ к наиболее частым местоположениям. Распознавание активности позволяет узнать, что делает пользователь устройства: едет на машине, едет на велосипеде, идет пешком или находится на одном месте. Ну и, собственно, работа с геозонами позволяет посылать сообщения, когда пользователь устройства входит в конкретную зону, покидает её либо находится в зоне определенный период времени.
На мой взгляд официальный пример довольно сложный и запутанный. Это связано с тем, что в нём:
  • попытались показать все возможности Location APIs
  • множество комментариев и обработок исключений, которые в примере можно было бы и упустить
  • все действия выполняются из активити

Исходя из этого в данной статье я сфокусируюсь только на геозонах и опущу некоторые обработки исключений.

Примечание: Google Play сервисы могут быть отключены на устройстве. Это может нарушить работу многих приложений и система честно предупреждает пользователя об этом перед их отключением. Но всё же хорошим тоном будет проверять это в своем приложении с помощью GooglePlayServicesUtil.isGooglePlayServicesAvailable и как-то предупреждать пользователя.

Задача


Итак, для примера напишем приложение, в котором можно явно указать координаты и радиус геозоны. При входе/выходе из неё в статус бар будет добавляться уведомление с id геозоны и типом перемещения. После выхода из геозоны мы её удалим.

Исходники для нетерпеливых


Алгоритм


В общем процесс выглядит следующим образом:
  1. Из активити создаем сервис, в который передаем данные о геозоне.
  2. Сервис инициализирует LocationClient.
  3. Когда LocationClient инициализировался, добавляем в него геозоны (Geofence) и соответствующие им PendingIntent.
  4. Когда геозоны добавлены, отключаемся от LocationClient и останавливаем сервис.
  5. Далее вся надежда на PendingIntent, который запустит IntentService при входе в зону или выходе из зоны. Сервис добавляет уведомления в статус бар и создает сервис для удаления отработанных геозон.
  6. Созданный сервис снова инициализирует LocationClient.
  7. Когда LocationClient инициализировался, удаляем отработанные геозоны.
  8. Когда геозоны удалены, отключаемся от LocationClient и останавливаем сервис.
  9. Profit!

Как мы видим, главным действующим лицом является LocationClient. Он отвечает за доступ к API для определения местоположения и работы с геозонами.

К делу!


Для начала необходимо подключить Google Play сервисы. Как это сделать описано здесь.
Далее в активити инициализируем элементы отображения. Из этой области нас интересует вызов сервиса при обработке нажатия на кнопку:

	int transitionType = Geofence.GEOFENCE_TRANSITION_ENTER | Geofence.GEOFENCE_TRANSITION_EXIT;

	MyGeofence myGeofence = new MyGeofence(mId, latitude, longitude, radius, transitionType);

	Intent geofencingService = new Intent(activity, GeofencingService.class);

	geofencingService.putExtra(GeofencingService.EXTRA_ACTION, GeofencingService.Action.ADD);
	geofencingService.putExtra(GeofencingService.EXTRA_GEOFENCE, myGeofence);

	activity.startService(geofencingService);

Тут мы создаем Intent для нашего сервиса (GeofencingService) и передаем в него необходимые данные. Так как GeofencingService отвечает за добавление и удаление геозон (в примере я решил не разделять эти действия на разные сервисы), то нам надо передать тип операции, которая должна быть выполнена сервисом. В данном случае это добавление (GeofencingService.Action.ADD). Также сервису нужны данные о геозоне. Их мы передаем в виде объекта класса MyGeofence, который по сути является оберткой над Geofence.Builder (о нём мы поговорим позже).
Итак, мы передаем координаты центра и радиус зоны, а также тип перемещения. Последний может быть трех видов: GEOFENCE_TRANSITION_ENTER, GEOFENCE_TRANSITION_EXIT и GEOFENCE_TRANSITION_DWELL. Если с первыми двумя все понятно, то к третьему необходимы разъяснения. GEOFENCE_TRANSITION_DWELL указывает на то, что пользователь вошел в зону и пробыл в ней некоторое время. Чтобы использовать этот сигнал, вы должны установить setLoiteringDelay при построении геозоны. В данном примере GEOFENCE_TRANSITION_DWELL не используется.

Перейдем к сервису. Сервис имплементирует GooglePlayServicesClient.ConnectionCallbacks, GooglePlayServicesClient.OnConnectionFailedListener, LocationClient.OnAddGeofencesResultListener, LocationClient.OnRemoveGeofencesResultListener интерфейсы. Это позволяет ему полностью отвечать за работу с LocationClient.
В onStartCommand мы получаем тип операции (ADD или REMOVE) и вытягиваем необходимые для выполнения этого действия данные. После этого инициализируем и запускаем LocationClient:

	mAction = (Action) intent.getSerializableExtra(EXTRA_ACTION);

	switch (mAction) {
		case ADD:
			MyGeofence newGeofence = (MyGeofence) intent.getSerializableExtra(EXTRA_GEOFENCE);
			mGeofenceListsToAdd.add(newGeofence.toGeofence());
			break;
		case REMOVE:
			mGeofenceListsToRemove = Arrays.asList(intent.getStringArrayExtra(EXTRA_REQUEST_IDS));
			break;
        }

	mLocationClient = new LocationClient(this, this, this);
        mLocationClient.connect();

Прежде чем добавить геозону mGeofenceListsToAdd, мы вызвали метод toGeofence() объекта класса MyGeofence. Я уже говорил, что MyGeofence является обёрткой над Geofence.Builder:

    public MyGeofence(int id, double latitude, double longitude, float radius, int transitionType) {
        this.id = id;
        this.latitude = latitude;
        this.longitude = longitude;
        this.radius = radius;
        this.transitionType = transitionType;
    }

    public Geofence toGeofence() {
        return new Geofence.Builder()
                .setRequestId(String.valueOf(id))
                .setTransitionTypes(transitionType)
                .setCircularRegion(latitude, longitude, radius)
                .setExpirationDuration(ONE_MINUTE)
                .build();
    }

Geofence.Builder — это служебный класс для создания Geofence. Мы задаем необходимые параметры, а потом вызываем метод build() для создания объекта. Выше указан необходимый минимум параметров. Тут стоит обратить внимание на setExpirationDuration. Дело в том, что зарегистрированные геозоны могут быть удалены только в двух случаях: по истечении заданного времени или при явном удалении. Поэтому, если вы передаете в качестве параметра NEVER_EXPIRE, то вы обязаны позаботиться об удалении объекта самостоятельно. Для Location APIs есть ограничение: максимум 100 геозон на одно приложение одновременно.

После того как LocationClient подключится, сработает onConnected колбэк интерфейса GooglePlayServicesClient.ConnectionCallbacks. В нем мы выполняем добавление либо удаление в зависимости от текущего типа действия:

    @Override
    public void onConnected(Bundle bundle) {
        Log.d("GEO", "Location client connected");

        switch (mAction) {
            case ADD:
                Log.d("GEO", "Location client adds geofence");
                mLocationClient.addGeofences(mGeofenceListsToAdd, getPendingIntent(), this);
                break;
            case REMOVE:
                Log.d("GEO", "Location client removes geofence");
                mLocationClient.removeGeofences(mGeofenceListsToRemove, this);
                break;
        }
    }

Как мы видим, addGeofences одним из параметров требует PendingIntent, который сработает при перемещении. В нашем случае PendingIntent будет запускать IntentService:

    private PendingIntent getPendingIntent() {
        Intent transitionService = new Intent(this, ReceiveTransitionsIntentService.class);
        return PendingIntent.getService(this, 0, transitionService, PendingIntent.FLAG_UPDATE_CURRENT);
    }

После выполнения действия у нас срабатывают OnAddGeofencesResultListener или onRemoveGeofencesByRequestIdsResult , в которых мы отключаемся от LocationClient и останавливаем сервис:

    @Override
    public void onAddGeofencesResult(int i, String[] strings) {
        if (LocationStatusCodes.SUCCESS == i) {

            Log.d("GEO", "Geofences added " + strings);

            for (String geofenceId : strings)
                Toast.makeText(this, "Geofences added: " + geofenceId, Toast.LENGTH_SHORT).show();

            mLocationClient.disconnect();
            stopSelf();
        } else {
            Log.e("GEO", "Error while adding geofence: " + strings);
        }
    }

    @Override
    public void onRemoveGeofencesByRequestIdsResult(int i, String[] strings) {
        if (LocationStatusCodes.SUCCESS == i) {
            Log.d("GEO", "Geofences removed" + strings);
            mLocationClient.disconnect();
            stopSelf();
        } else {
            Log.e("GEO", "Error while removing geofence: " + strings);
        }
    }

Последняя часть приложения – это IntentService, который запускается при пересечении границы геозоны пользователем устройства. Все действия выполняются в onHandleIntent:

    @Override
    protected void onHandleIntent(Intent intent) {

        if (LocationClient.hasError(intent)) {
            Log.e(TRANSITION_INTENT_SERVICE, "Location Services error: " + LocationClient.getErrorCode(intent));
            return;
        }

        int transitionType = LocationClient.getGeofenceTransition(intent);

        List<Geofence> triggeredGeofences = LocationClient.getTriggeringGeofences(intent);
        List<String> triggeredIds = new ArrayList<String>();

        for (Geofence geofence : triggeredGeofences) {
            Log.d("GEO", "onHandle:" + geofence.getRequestId());
            processGeofence(geofence, transitionType);
            triggeredIds.add(geofence.getRequestId());
        }

        if (transitionType == Geofence.GEOFENCE_TRANSITION_EXIT)
            removeGeofences(triggeredIds);
    }

Здесь у нас фигурируют в основном статические методы LocationClient. Сначала мы делаем проверку на наличие ошибок с помощью hasError. Затем получаем тип перемещения и список сработавших геозон с помощью getGeofenceTransition и getTriggeringGeofences соответственно. Вызываем обработку каждой геозоны и сохраняем её id. Ну и напоследок, удаляем геозоны в случае, если данное перемещение было выходом из геозоны.
Для удаления геозон мы опять создаём сервис, в который передаём тип операции (REMOVE) и список id на удаление:

    private void removeGeofences(List<String> requestIds) {
        Intent intent = new Intent(getApplicationContext(), GeofencingService.class);

        String[] ids = new String[0];
        intent.putExtra(GeofencingService.EXTRA_REQUEST_IDS, requestIds.toArray(ids));
        intent.putExtra(GeofencingService.EXTRA_ACTION, GeofencingService.Action.REMOVE);

        startService(intent);
    }


На этом всё!


Надеюсь пример получился понятным и интересным. Желаю всем хороших приложений!

UPDATE:
Статья и код сильно устарели за 2 года.
Спасибо Vilkaman за обновления кода в репозитории.
Подробнее о работе с обновленным Location API можно прочитать в его статье
  • +16
  • 17.6k
  • 7
Share post

Similar posts

AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 7

    0
    Я обязан задавать эти «гео-зоны» когда я работаю с Locations API?
    Можно просто определить «глобальную зону» и получать координаты с заданной переодичностью (раз в минуту)?
    И при необходимости запрашивать текущие координаты?
      0
      Если вы хотите получать координаты с заданной периодичностью, то задавать геозоны нет необходимости. В данном случае вам лучше использовать другую часть данных API — LocationRequest
      0
      Добрый день. Спасибо за отличный материал.
      У меня есть вопрос, касательный использования гугл сервисов для определения зон для геллокационных ремайндерах. Если запустить Ваш сервис в автозагрузке смартфона, то будет ли всё корректно работать?
      P.S. Иду тянуть с гитхаба сорцы, буду экспериментировать на базе вашего приложения :)
        0
        При перезагрузке все зарегистрированные зоны очищаются, поэтому вам необходимо будет их регистрировать при запуске сервиса.
        0
        Каким образом можно имитировать выход за зону?
          0
          Хотелось бы это инициировать, т.к. при установке зоны, срабатывает «Enter geofence», когда девайс находится внутри.
          Но почему-то не срабатывает «Exit geofence» для аналогичного случая, но когда девайс находится снаружи.
            0
            Такое поведение логично. Обработчик срабатывает именно при изменении состояния. Состояние по-умолчанию — «вне зоны». То есть для Enter — телефон был вне зоны > приложение инициаизировалось > телефон оказался в зоне > изменение состояния вызывает обработчик. Для Exit — телефон был вне зоны > приложение инициаизировалось > телефон оказался вне зоне > изменение состояния не произошло.
            Если хотите имитировать — задавайте фейковые координаты. Для этого можно использовать либо готовые проги (например, Lockito), либо в режиме дебага через telnet, либо самописное что-то)) Я использовал последний вариант, но сейчас бы наверное взял готовую прогу.

        Only users with full accounts can post comments. Log in, please.