Как определить "Где я?" в приложениях на Windows Mobile

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

Каким образом реализуются приложения, зависящие от местоположения сегодня? Как правило, на ум сразу приходит использование GPS-устройств, которые способны предоставлять координаты текущего местоположения. Однако, GPS устройства есть далеко не везде и не всегда. Более того, применение GPS-устройств ограничивается тем, что их нельзя использовать внутри зданий и закрытых помещениях. Засчастую на этом этапе заканчиваются идеи относительно получения текущего местоположения. Однако, если задуматься, то существуют и другие источники получения этой информации. Поскольку в нашем случае речь идет о Windows Mobile, а устройства на базе этой операционной системы зачастую обладают GSM-модулем, то информацию о местоположении можно брать с базовых станций состовых операторов. Кроме того, не стоит забывать про сервисы определения местоположения по IP-адресу – на подобных устройствах мобильный интернет настроен в большинстве случаев.

Каждый из приведенных выше способов имеет собственные преимущества и недостатки. Поэтому каждый способ может работать только в определенных условиях и при определенных ограничениях. Наиболее правильный способ – использовать все приведенные ранее способы и объеденить преимущества этих способов в одном приложении.

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

GPS

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

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

Для работы с GPS-приемником в Windows Mobile могут использоваться различные подходы. Обычно GPS-приемник поставляет свои данные через COM-порт. Поэтому можно подключиться к нему и считывать всю необходимую информацию. Однако, при таком подходе могут быть проблемы с подключением нескольких приложений к GPS-приемнику. Кроме того, в этом случае нам придется вручную разбирать форматы данных, которые поступают от приемника (мелочь, а не приятно). Поэтому в данном случае рекомендуется использовать некоторую «прослойку» в Windows Mobile, которая позволяет централизованно получить информацию с GPS-устройства – GPS API. По сути, GPS API работает напрямую с COM-портом, обрабатывает данные и предоставляет интерфейсы для других приложений. Используя GPS API, приложения могут получать всю информацию, которая доступна от GPS-приемника.

Для работы с GPS API в Windows Mobile существует библиотека gpsapi.dll, которая поставляется вместе с операционной системой. Эта библиотека является неуправляемой, поэтому для работы с ней необходимо использовать PInvoke. К счастью, в Microsoft уже сделали управляемую обертку для этой библиотеки, которая поставляется вместе с Windows Mobile 6 SDK. В нашем случае мы возьмем эту обертку для использования в нашем приложении.

Использование управляемой обертки является достаточно простым. Для этого необходимо создать объект NativeWrapperGps, подписаться на событие LocationChanged и вызвать метод Open. При изменении текущего местоположения будет сгенерировано событие LocationChanged. После окончания работы с GPS-устройством необходимо вызвать метод Close. В общем случае работа через GPS API может выглядеть следующим образом.

var gps = new NativeWrapperGps();
gps.LocationChanged +=
  delegate(object sender, NativeWrapperGps.LocationChangedEventArgs args)
  {
    // ...
  };

gps.Open();
// ...
gps.Close();

Таким образом, мы можем работать с GPS-приемником в Windows Mobile и получать информацию о текущем местоположении.

Сотовые сети

Другим источником информации о текущем местоположении может служить информация, которая доступна от оператора сотовой связи. Поскольку сети беспроводной связи строятся по принципу «сот», то в каждый момент мы работаем с какой-либо конкретной базовой станцией. Каждая базовая станция имеет некий уникальный идентификатор, который определяет каждую базовую станцию. Этот идентификатор состоит из нескольких значений – Cell ID (или Tower ID), Location Area Code (LAC), Mobile Country Code (MCC) и Mobile Network Code (MNC). Все эти значения можно получить от базовой станции и они будут уникальными для каждой базовой станции.

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

Таким образом, предложенный способ не содержит недостатков способа определения местоположения с помощью GPS – не требуется длительная инициализация, процесс определения не зависит от погодных условий и положения спутников, а определение местоположения происходит достаточно быстро. Однако, у приведенного способа есть и недостатки. Основным недостатком является низкая точность определения местоположения: если GPS определяет координаты с точностью до нескольких метров, то приведенный способ может «ошибиться» на несколько километров. Кроме того, централизованные базы данных могут не содержать информацию о базовой станции с которой вы сейчас работаете – в этом случае определить местоположение невозможно. Наконец, для определения местоположения требуется наличие покрытия территории сетями мобильных операторов, а также требуется доступ к Интернет (GPRS, EDGE, 3G и т.д.). Доступ к глобальной сети необдоходим для того, чтобы воспользоваться услугами централизованных баз данных для определения координат. Таким образом, видно, что данный способ имеет достаточно серьезные ограничение, но в некоторых случаях может применяться достаточно успешно.

Для того, чтобы получать информацию от GSM-модуля телефона можно воспользоваться библиотекой ril.dll (RIL, Radio Interface Layer). Эта библиотека поставляется вместе с операционной системой и разработана на неуправляемом коде. По этой причине использовать её функции можно через PInvoke. В рамках RIL доступно множество интересных функций, которыми можно воспользоваться. В нашем случае необходимо получить информацию о базовой станции, это можно сделать используя функцию GetCellTowerInfo. Перед использованием этой функции необходимо вызвать функцию инициализации, а после использования — деинициализации. При инициализации указывается метод, который будет вызван после получения информации (процесс получения информации о базовой станции асинхронный). Таким образом, для работы с RIL потребуется небольшая обертка над неуправляемой библиотекой.

public static class RilInterop
{
  public delegate void RilResultCallback(uint dwCode, IntPtr hrCmdID, IntPtr lpData, uint cbData, uint dwParam);

  public delegate void RilNotifyCallback(uint dwCode, IntPtr lpData, uint cbData, uint dwParam);

  [DllImport("ril.dll", EntryPoint = "RIL_Initialize")]
  public static extern IntPtr Initialize(uint dwIndex, RilResultCallback pfnResult, RilNotifyCallback pfnNotify, uint dwNotificationClasses, uint dwParam, out IntPtr lphRil);

  [DllImport("ril.dll", EntryPoint = "RIL_GetCellTowerInfo")]
  public static extern IntPtr GetCellTowerInfo(IntPtr hRil);

  [DllImport("ril.dll", EntryPoint = "RIL_Deinitialize")]
  public static extern IntPtr Deinitialize(IntPtr hRil);
}

После этого необходимо проинициализировать RIL, указать метод обратного вызова, вызвать метод GetCellTowerInfo и вызвать метод деинициализации. Данные будут получены в асинхронном режиме, поэтому если необходимо их получать синхронно нужно воспользоваться объектами синхронизации, например, AutoResetEvent. Таким образом, получение информации о базовой станции будет выглядеть следующим образом.

public class RilWrapper
{
  private static RilCellTowerInfo _towerDetails;

  private static readonly AutoResetEvent WaitHandle = new AutoResetEvent(false);

  public static CellTower GetCellTowerInfo()
  {
    IntPtr rilHandle;

    if (RilInterop.Initialize(1, CellDataCallback, null, 0, 0, out rilHandle) != IntPtr.Zero)
    {
      return null;
    }

    RilInterop.GetCellTowerInfo(rilHandle);

    WaitHandle.WaitOne();

    RilInterop.Deinitialize(rilHandle);

    return new CellTower
    {
      TowerId = Convert.ToInt32(_towerDetails.DwCellID),
      LocationAreaCode = Convert.ToInt32(_towerDetails.DwLocationAreaCode),
      MobileCountryCode = Convert.ToInt32(_towerDetails.DwMobileCountryCode),
      MobileNetworkCode = Convert.ToInt32(_towerDetails.DwMobileNetworkCode),
    };
  }

  public static void CellDataCallback(uint dwCode, IntPtr hrCmdID, IntPtr lpData, uint cbData, uint dwParam)
  {
    _towerDetails = new RilCellTowerInfo();
    Marshal.PtrToStructure(lpData, _towerDetails);
    WaitHandle.Set();
  }
}

После вызова метода GetCellTowerInfo() будет возвращен объект, содержащий значения, которые характеризуют текущую базовую станцию. Как было сказано выше, для получения географических координат на основе этой информации необходимо использовать какую-либо общедоступную базу данных. Например, такую информацию содержит открытый сервис OpenCellId (opencellid.org). Используя этот ресурс можно получить координаты местоположения по информации о базовой станции. Используя заданный формат URL, можно обратиться к сервису и получить ответ в виде XML. Для распознования информации от этого сервиса создадим небольшой объект, выполняющий эти функции.

public class OpenCellIdProvider : ICellToGpsProvider
{
  public GpsLocation GetCoordinates(int cellId, int locationAreaCode, int mobileCountryCode, int mobileNetworkCode)
  {
    GpsLocation result = null;

    var doc = XDocument.Load(String.Format("http://www.opencellid.org/cell/get?key={4}&mnc={3}&mcc={2}&lac={1}&cellid={0}", cellId, locationAreaCode, mobileCountryCode, mobileNetworkCode, Key));

    if ((doc.Root != null) && (doc.Root.Attribute(XName.Get("stat")).Value == "ok"))
    {
      var cellInfoElem = doc.Root.Element(XName.Get("cell"));

      if (cellInfoElem != null)
      {
        result = new GpsLocation
        {
          Latitude = Convert.ToDouble(cellInfoElem.Attribute(XName.Get("lat")).Value.Replace('.', ',')),
          Longitude = Convert.ToDouble(cellInfoElem.Attribute(XName.Get("lon")).Value.Replace('.', ','))
        };
      }
    }

    return result;
  }
}

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

GeoIp

Последний способ, который мы рассмотрим в качестве варианта получения текущего местоположение – это определение текущих координат на основе IP-адреса. Поскольку большинство устройств имеют GRPS-подключение к Интернет, то этот способ может достаточно успешно работать. Получение местоположения по IP-адресу – это операция, которая не всегда возвращает достоверную информацию. За счет использования «серых» IP-адресов информация может сильно отличаться от действительности (например, когда российский IP-адрес определяется ирландским). Тем не менее, при использовании мобильного интернета вероятность получения правильной информации выше. Поэтому мы рассмотрим этот метод в качестве одной из альтернатив.

Сильной стороной этого метода является то, что он не зависит от наличия приемника GPS, погодных условий и возможности соединения спутника. Кроме того, этот способ не зависит от наличия информации в базе данных по базовым станциям. Однако, недостатки у этого метода присутствуют и они являются достаточно существенными. Во-первых, точность определения местоположения зачастую оставляет желать лучшего. Как правило – это рамки города (даже не района). Однако, если этот уровень детализации устраивает, то почему бы и нет? Другой недостаток – это необходимость наличия подключенного интернета; если подключенного интернета нет, то этот способ не работает. Более того, большинство полноценных иструментов определения местоположения по IP-адресу либо являются платными, либо накладывают ограничения на число обращений.

Тем не менее, давайте возьмем на заметку этот способ на случай, когда все перечисленные ограничения не являются существенными. GeoIp-сервисов существует большое множество. Большинство из них платные, другие могут просто требовать регистрации. В нашем случае мы остановимся на сервисе GeoIpTool (geoiptool.com, в общем же случае можно использовать любой из представленных в сети Интернет. Этот сервис возвращает HTML при обращении к его главной странице, внутри которой есть координаты, соответствющие IP-адресу клиента. В данном случае мы просто обработаем HTML-код и получим из него координаты.

public GpsLocation GetLocation()
{
  double? lat = null;
  double? lon = null;

  var html = GetHtml("http://www.geoiptool.com/en/");

  var lines = Regex.Matches(html.Replace("\r", " ").Replace("\n", " "), @"<tr>(.)+?</tr>", RegexOptions.IgnoreCase);
  foreach (Match line in lines)
  {
    try
    {
      if (line.Value.Contains("Longitude:"))
      {
        lon = Convert.ToDouble(Regex.Match(line.Value, @"<td(.)*?>(?<val>[0-9\.]+)</td>").Groups["val"].Value.Replace('.', ','));
      }
      if (line.Value.Contains("Latitude:"))
      {
        lat = Convert.ToDouble(Regex.Match(line.Value, @"<td(.)*?>(?<val>[0-9\.]+)</td>").Groups["val"].Value.Replace('.', ','));
      }
    }
    catch (FormatException)
    {
    }
  }

  return (lat != null) && (lon != null) ? new GpsLocation { Latitude = (double)lat, Longitude = (double)lon } : null;
}

Таким образом, мы можем получить свое местоположение по IP-адресу.

Строим финальное приложение

Теперь, когда мы умеем определять текущее местоположение как минимум тремя способами, давайте построим приложение, которое будет использовать одновременно все эти способы. Поскольку каждый из способов имет свои преимущества и недостатки расположим эти способы в порядке приоритетов. Понятно, что самым точным способом является определение координат на основе данных GPS. Поэтому приоритетно будем использовать его. В случаях, когда GPS-приемник отсутствует в устройстве или он не может соединиться со спутниками, мы будем использовать данные от базовых станций мобильной сети. В этом случае точность определения будет снижена, но свое примерное местоположение мы узнать сможем. Наконец, если по каким-то причинам мы не смогли узнать местоположение и этим способом (например, данные о текущей базовой станции не найдены в базе данных), то можно узнать местоположение с помощью мобильного интернета и IP-адреса. В этом случае точность снизится еще больше, но информацию мы получим.

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

public interface ILocationProvider
{
  GpsLocation GetLocation();
  string ProviderName { get; }
}

Этот интерфейс содержит метод для получения текущего местоположения, а также возвращает имя провайдера. После этого необходимо реализовать этот интерфейс в объектах, реализующих логику для каждого из приведенных способов. Несложно догадаться как это можно сделать на основе кода, приведенного для каждого из способов. Наконец, необходим объект, выполняющий функции координирования всех провайдеров, своего рода менеджер. Этот объект должен содержать коллекцию доступных провайдеров и при обращении к нему должен поочередно опрашивать каждого провайдера. Если более приоритетный провайдер сгенерировал исключение или просто вернул null, то нужно обратиться к следующему провайдеру и т.д. Таким образом, получается следующее описание класса для подобного объекта.

public class LocationManager : ILocationProvider
{
  private readonly List<ILocationProvider> _providers = new List<ILocationProvider>();

  private string _lastProviderName = null;

  public void ClearProviders()
  {
    _providers.Clear();
  }

  public void RegisterProvider(ILocationProvider provider)
  {
    if (_providers.Contains(provider)==false)
    {
      _providers.Add(provider);
    }
  }

  public void RemoveProvider(ILocationProvider provider)
  {
    if (_providers.Contains(provider) == true)
    {
      _providers.Remove(provider);
    }
  }

  #region ILocationProvider Members

  public GpsLocation GetLocation()
  {
    GpsLocation result = null;
    _lastProviderName = null;

    foreach (var provider in _providers)
    {
      try
      {
        result = provider.GetLocation();

        if (result != null)
        {
          _lastProviderName = provider.ProviderName;
          break;
        }
      }
      catch (Exception ex)
      {
        continue;
      }
    }
    
    return result;
  }
  
  public string ProviderName
  {
    get { return _lastProviderName; }
  }
  
  #endregion
}

Таким образом, для использования этого механизма необходимо зарегистрировать все необходимые провайдеры, а затем обратиться к объекту-менеджеру с вопросом «где я?». Например, этот код может выглядть так.

LocationManager manager = new LocationManager();

manager.RegisterProvider(new GpsLocationProvider());
manager.RegisterProvider(new CellLocationProvider(new OpenCellIdProvider()));
manager.RegisterProvider(new GeoIpLocationProvider());

var location = manager.GetLocation();

В случае, когда нужно исключить из процесса какой-либо из провайдеров, мы просто его не регистрируем. Напоследок, можно отобразить полученные координаты на карте и вывести их пользователю.

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

Вместо заключения

Приложения, реагирующие на изменение контекста становятся все более популярными в условиях развития современных приложений. Мобильные приложения не являются исключением. Однако, многие разработчики до сих пор считают приемник GPS единственным средством получения информации о текущем местоположении. Здесь мы рассмотрели способ, с помощью которого можно получать подобную информацию в случае невозможности использования GPS и даже без наличия приемника GPS. Зачастую альтернативные способы являются менее точными при определении местоположения, но точность нужна не везде и не всегда – большое количество сценариев отлично реализуются и на основе информации о том, в каком я сейчас городе или даже стране. Поэтому не нужно ограничивать себя в подобных возможностях, а нужно просто включить фантазию и начать использовать контекстно-зависимые сценарии в своих приложениях.