Pull to refresh

Глубокое погружение в определение местоположения

Development for Android *
Этот пост является переводом топика из блога android-developers. Далее повествование ведется от Рето Майера, автора книги Professional Android 2 Application Development. Он пишет о том, как можно улучшить приложения, использующие местоположение, в смысле кэширования результатов, скорости работы и так далее.

Без разницы, ищете ли вы место, где бы поесть, или ближайшее место велосипедов Boris Bike, всегда есть задержка при получении данных местоположения от GPS и заполнении абстрактного списка результатов в вакууме. Когда вы находитесь в месте, где хотели бы получить контекстную информацию, то часто вы сталкиваетесь с отсутствием подключения к данным.

Вместо того, чтобы грозить кулаком в небо, я написал open-source приложение, которое включает в себя советы и рекомендации по сокращению времени между открытием приложения и просмотром актуальной информации о близлежащих местах, вкупе с разумным обеспечением offline режима работы. И всё это, сохраняя использование аккумулятора на возможном минимуме.

Покажи мне код!


Вы можете проверить мой проект Android Protips for Location. Не забудьте прочесть Readme.txt, чтобы успешно скомпилировать и запустить приложение.

Что оно делает на самом деле?


Оно использует Google Places API для реализации базовой функциональности приложений, которые которые используют местоположение для определения списка близлежащих достопримечательностей (точек на карте), позволяют просмотреть их детали, а также проверить или оценить.

Код реализует многие из лучших практик, о которых я подробно рассказал на своей сессии на Google I/O 2011, Android Protips: Advanced Topics for Expert Android Developers (видео). В том числе использование Intent'ов для получения обновлений о местоположении, используя Passive Location Provider, наблюдение за состоянием устройства для изменения частоты обновлений, переключение Receiver'ов во время исполнения приложения, и также используя Cursor Loader.

Приложение написано для Honeycomb, но также может работать и на версия 1.6 и выше.

Теперь у вас есть код, давайте подробнее рассмотрим его


Мой главный приоритет — свежесть: минимизация задержек между открытием приложения и возможностью проверить нужные места, и в то же время минимизировать использование аккумулятора.

Требования:
  • Текущее местоположение должно находиться так быстро, как это возможно
  • Список мест должен обновляться при изменение местоположения
  • Список близлежащих мест должен быть доступен (в деталях) в автономном режиме
  • Отметки также должны быть доступны в автономном режиме
  • Данные о местоположении и другие данные пользователя должны быть правильно обработаны (см. более ранний пост с лучшими практиками )

Свежесть — значит никогда не ждать


Можно значительно сократить время ожидания получения первого приближения к местоположению, путём выбора последнего известного из Location Manager'a каждый раз, когда приложение переходит в активный режим.

В этом примере из GingerbreadLastLocationFinder, мы перебираем все провайдеры местоположения на устройстве, даже те, которые на данный момент недоступны, чтобы найти наиболее точное последнее местоположение.

List<String> matchingProviders = locationManager.getAllProviders();
for (String provider: matchingProviders) {
  Location location = locationManager.getLastKnownLocation(provider);
  if (location != null) {
    float accuracy = location.getAccuracy();
    long time = location.getTime();
        
    if ((time > minTime && accuracy < bestAccuracy)) {
      bestResult = location;
      bestAccuracy = accuracy;
      bestTime = time;
    }
    else if (time < minTime && 
             bestAccuracy == Float.MAX_VALUE && time > bestTime){
      bestResult = location;
      bestTime = time;
    }
  }
}

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

Во втором случае (когда определено, что последнее обновление местоположения недостаточно недавнее), значение так или иначе возвращается, но мы запрашиваем одно обновление местоположения, используя самый быстрый провайдер.

if (locationListener != null &&
   (bestTime < maxTime || bestAccuracy > maxDistance)) { 
  IntentFilter locIntentFilter = new IntentFilter(SINGLE_LOCATION_UPDATE_ACTION);
  context.registerReceiver(singleUpdateReceiver, locIntentFilter);      
  locationManager.requestSingleUpdate(criteria, singleUpatePI);
}

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

Отметим также, что этот фрагмент кода показывает GingerbreadLastLocationFinder, который использует метод requestSingleUpdate для получения единоразового обновления местоположения. Эта функциональность не была доступна до Gingerbread — проверьте LegacyLastLocationFinder, посмотрите как я реализовал такую же функциональность для более ранних версий Android.

singleUpdateReceiver передает полученное обновление назад к вызывающему классу через слушателя Location Listener.
protected BroadcastReceiver singleUpdateReceiver = new BroadcastReceiver() {
  @Override
  public void onReceive(Context context, Intent intent) {
    context.unregisterReceiver(singleUpdateReceiver);
      
    String key = LocationManager.KEY_LOCATION_CHANGED;
    Location location = (Location)intent.getExtras().get(key);
      
    if (locationListener != null && location != null)
      locationListener.onLocationChanged(location);
      
    locationManager.removeUpdates(singleUpatePI);
  }
};

Используйте Intent'ы для получения обновлений местоположения


Получив наиболее точную/своевременную оценку текущей позиции, мы также хотим получать её обновления.

Класс PlacesConstants включает в себя набор значений, которые определяют частоту обновления местоположения. Настройте их, чтобы убедиться, что обновления приходят так часто, как это требуется.

// The default search radius when searching for places nearby.
public static int DEFAULT_RADIUS = 150;
// The maximum distance the user should travel between location updates. 
public static int MAX_DISTANCE = DEFAULT_RADIUS/2;
// The maximum time that should pass before the user gets a location update.
public static long MAX_TIME = AlarmManager.INTERVAL_FIFTEEN_MINUTES; 

Следующий шаг — запросить обновление местоположения от Location Manager. В следующем фрагменте кода, взятом из GingerbreadLocationUpdateRequester мы можем передать критерии, используемые для определения — какой Location Manager будет запрашивать обновления напрямую в вызове метода requestLocationUpdates.

public void requestLocationUpdates(long minTime, long minDistance, 
  Criteria criteria, PendingIntent pendingIntent) {

  locationManager.requestLocationUpdates(minTime, minDistance, 
    criteria, pendingIntent);
}

Обратите внимание, что мы передаём Pending Intent, а не Location Listener.

Intent activeIntent = new Intent(this, LocationChangedReceiver.class);
locationListenerPendingIntent = 
  PendingIntent.getBroadcast(this, 0, activeIntent, PendingIntent.FLAG_UPDATE_CURRENT);

Вообще, я предпочитаю использовать Location Listener'ов, потому что они предоставляют гибкость в регистрации приёмников в нескольких Activity или Сервисах, или напрямую в манифесте.

В этом приложении, новое местоположение означает обновленный список близлежащих мест. Это происходит через сервисы, которые отправляют запросы к серверу и обновляют Контент-провайдер, который заполняет список мест.

Так как изменение местоположения напрямую не обновляет UI, то имеет смысл создавать и регистрировать связанный LocationChangedReceiver в манифесте, а не в Activity.

<receiver android:name=".receivers.LocationChangedReceiver"/>

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

if (intent.hasExtra(locationKey)) {
  Location location = (Location)intent.getExtras().get(locationKey);
  Log.d(TAG, "Actively Updating place list");
  Intent updateServiceIntent = 
    new Intent(context, PlacesConstants.SUPPORTS_ECLAIR ? EclairPlacesUpdateService.class : PlacesUpdateService.class);
  updateServiceIntent.putExtra(PlacesConstants.EXTRA_KEY_LOCATION, location);
  updateServiceIntent.putExtra(PlacesConstants.EXTRA_KEY_RADIUS, PlacesConstants.DEFAULT_RADIUS);
  updateServiceIntent.putExtra(PlacesConstants.EXTRA_KEY_FORCEREFRESH, true);

  context.startService(updateServiceIntent);
}

Получение данных во время автономной работы


Чтобы добавить поддержку режима offline, мы начнём с кэширования результатов поиска по PlacesContentProvider и PlaceDetailsContentProvider.

Также, при определенный обстоятельствах нужно проводить выборку сведений о местоположении. Этот фрагмент кода из сервиса PlacesUpdateService демонстрирует, как предварительная выборка включается для ограниченного количества мест.

Обратите внимание на то, что предварительная выборка данных потенциально выключается при низком заряде аккумулятора.

if ((prefetchCount < PlacesConstants.PREFETCH_LIMIT) &&
    (!PlacesConstants.PREFETCH_ON_WIFI_ONLY || !mobileData) &&
    (!PlacesConstants.DISABLE_PREFETCH_ON_LOW_BATTERY || !lowBattery)) {
  prefetchCount++;
      
  // Start the PlaceDetailsUpdateService to prefetch the details for this place.
}

Похожая техника используется для реализации offline отметок.

Оптимизация времени работы аккумулятора: умные сервисы и использование состояния устройства для переключения ресиверов


Нет смысла запускать сервис обновления, когда устройство находится в автономном режиме. Сервис PlaceUpdateService проверяет возможность соединения, прежде чем пытаться получить обновление.

NetworkInfo activeNetwork = cm.getActiveNetworkInfo();
boolean isConnected = activeNetwork != null &&
                      activeNetwork.isConnectedOrConnecting();

Если соединения нет, то ActiveLocationChangedReceiver и PassiveLocationChangedReceiver выключаются, а ConnectivityChangedReceiver включается.

ComponentName connectivityReceiver = 
  new ComponentName(this, ConnectivityChangedReceiver.class);
ComponentName locationReceiver = 
  new ComponentName(this, LocationChangedReceiver.class);
ComponentName passiveLocationReceiver = 
  new ComponentName(this, PassiveLocationChangedReceiver.class);

pm.setComponentEnabledSetting(connectivityReceiver,
  PackageManager.COMPONENT_ENABLED_STATE_ENABLED, 
  PackageManager.DONT_KILL_APP);
            
pm.setComponentEnabledSetting(locationReceiver,
  PackageManager.COMPONENT_ENABLED_STATE_DISABLED, 
  PackageManager.DONT_KILL_APP);
      
pm.setComponentEnabledSetting(passiveLocationReceiver,
  PackageManager.COMPONENT_ENABLED_STATE_DISABLED, 
  PackageManager.DONT_KILL_APP);

ConnectivityChangedReceiver прослушивает все изменения сетевого соединения. Когда производится новое подключение, то он просто выключает себя и включает слушателей местоположения.

Мониторинг состояния аккумулятора для уменьшения функциональности и экономии энергии


Когда телефон на последних 15%, то большинство приложений не работает, для того чтобы сохранить заряд. Мы можем зарегистрировать ресиверов в манифесте, чтобы выдавать предупреждение, когда устройство входит или выходит из состояния низкой батареи.

<receiver android:name=".receivers.PowerStateChangedReceiver">
  <intent-filter>
    <action android:name="android.intent.action.ACTION_BATTERY_LOW"/>
    <action android:name="android.intent.action.ACTION_BATTERY_OKAY"/>
  </intent-filter>
</receiver>

Этот фрагмент из PowerStateChangedReceiver выключает PassiveLocationChangedReceiver всякий раз, когда устройство переходит в состояние низкого аккумулятора и включает его, когда заряд в порядке.

boolean batteryLow = intent.getAction().equals(Intent.ACTION_BATTERY_LOW);
 
pm.setComponentEnabledSetting(passiveLocationReceiver,
  batteryLow ? PackageManager.COMPONENT_ENABLED_STATE_DISABLED :
               PackageManager.COMPONENT_ENABLED_STATE_DEFAULT,  
  PackageManager.DONT_KILL_APP);

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

Что дальше?


Пост уже получился большим, поэтому я остановлюсь здесь. Продолжение читайте в моём блоге The Radioactive Yak
Tags:
Hubs:
Total votes 35: ↑33 and ↓2 +31
Views 22K
Comments Comments 1