- PVSM.RU - https://www.pvsm.ru -

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

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

Что такое Location APIs?

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

  • попытались показать все возможности Location APIs
  • множество комментариев и обработок исключений, которые в примере можно было бы и упустить
  • все действия выполняются из активити

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

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

Задача

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

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

github [8]

Алгоритм

В общем процесс выглядит следующим образом:

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

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

К делу!

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

	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);
    }

На этом всё!

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

Автор: HE4UCTb

Источник [10]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/android-development/53446

Ссылки в тексте:

[1] Location APIs: https://developer.android.com/google/play-services/location.html

[2] видео: http://www.youtube.com/watch?v=Bte_GHuxUGc

[3] презентация: http://commondatastorage.googleapis.com/io-2013/presentations/106 - Beyond the Blue Dot- New features in Android Location (1).pdf

[4] одно упоминание: http://habrahabr.ru/post/191290/

[5] LocationManager: http://developer.android.com/reference/android/location/LocationManager.html

[6] официальный пример: http://developer.android.com/shareables/training/GeofenceDetection.zip

[7] GooglePlayServicesUtil.isGooglePlayServicesAvailable: http://developer.android.com/reference/com/google/android/gms/common/GooglePlayServicesUtil.html#isGooglePlayServicesAvailable(android.content.Context)

[8] github: https://github.com/Ne4istb/AndroidGeofenceTest

[9] здесь: http://developer.android.com/google/play-services/setup.html

[10] Источник: http://habrahabr.ru/post/210162/