Содержание
Использование геолокации на устройстве — это, по-видимому, одно из самых интересных и популярных направлений в разработке различных приложений. С геолокацией мы сталкиваемся в различных шагомерах, при работе с картографическими приложениями, с использованием геолокации может рассчитываться скорость перемещения устройства в пространстве и так далее. В этой части мы рассмотрим использование геолокации в Android.
Определение необходимых разрешений
Для получения данных о координатах расположения устройства, нашему приложению потребуется два разрешения:
ACCESS_COARSE_LOCATION
— позволяет приложению получить доступ к приблизительному местоположению.ACCESS_FINE_LOCATION
— позволяет приложению получать доступ к точному местоположению.ACCESS_BACKGROUND_LOCATION
— позволяет приложению получать доступ к местоположению в фоновом режиме
Все три разрешения относятся к категории «опасные», то есть мы должны будем явно запросить у пользователя разрешение на использование этих данных, как мы это делали при работе с контактами. Кроме того, мы должны также определить в нашем приложении то, что будем использовать аппаратные возможности:
android.hardware.location
— приложение использует одну или несколько функций устройства для определения местоположения, например местоположение по GPS или местоположение по вышкам сотовой сетиandroid.hardware.location.gps
— приложение использует точные координаты местоположения, полученные от приемника глобальной системы позиционирования (GPS) на устройствеandroid.hardware.location.network
— приложение использует приблизительные координаты местоположения, полученные из сетевой системы геолокации, поддерживаемой устройством
Все эти разрешение и использование аппаратных возможностей мы можем определить в с помощью атрибутов сборки в файле Platforms/Android/MainApplication.cs следующим образом:
[assembly: UsesPermission(Android.Manifest.Permission.AccessCoarseLocation)] [assembly: UsesPermission(Android.Manifest.Permission.AccessFineLocation)] [assembly: UsesPermission(Android.Manifest.Permission.AccessBackgroundLocation)] [assembly: UsesFeature("android.hardware.location", Required = false)] [assembly: UsesFeature("android.hardware.location.gps", Required = false)] [assembly: UsesFeature("android.hardware.location.network", Required = false)]
Что касается разрешений, то мы уже ни один раз их использовали при работе в Android. Здесь стоит обратить внимание на последние три строки. Здесь мы указываем какие функции будет использовать наше приложение.
Пот определении атрибутов с UsesFeature()
мы используем параметр Required=false
, который означает, что приложение использует эту функцию, если она присутствует на устройстве, но оно предназначено для работы без указанной функции. То есть, грубо говоря, если функция имеется на устройстве — супер, будем использовать, нет — хорошо, обойдемся без неё.
Теперь перейдем непосредственно к рассмотрению возможностей, которые нам дает интерфейс IGeolocation
.
Интерфейс IGeolocation и класс Geolocation
Интерфейс IGeolocation
предоставляет нам следующие методы:
Метод | Возвращаемый тип | Описание |
Get |
Task<Location> |
Возвращает последнее известное расположение устройства. |
Get |
Task<Location> |
Возвращает текущее расположение устройства. |
Start |
Task<Boolean> |
Начинает прослушивать обновления расположения с помощью события LocationChanged . События могут отправляться только в том случае, если приложение находится на переднем плане. |
Stop |
void |
Останавливает прослушивание обновлений расположения, когда приложение находится на переднем плане. Не оказывает никакого влияния, если свойство IsListeningForeground имеет значение false . |
Также у интерфейса определено два события:
event EventHandler<GeolocationLocationChangedEventArgs>? LocationChanged;
и
event EventHandler<GeolocationListeningFailedEventArgs>? ListeningFailed;
Которое возникает при ошибке во время прослушивания события LocationChanged
. При срабатывании этого события прослушивание дальнейших обновлений расположения останавливается.
Интерфейс реализуется в свойстве Default
класса Geolocation
. Теперь рассмотрим применение геолокации в нашем приложении.
Запрос необходимых разрешений у пользователя
Изменим код компонента Home
следующим образом:
@page "/" <button @onclick="CheckPermissions" class="btn-danger">Получить разрешения</button> @code{ public async Task<PermissionStatus> CheckAndRequestPermission<T>() where T: Permissions.BasePermission, new() { PermissionStatus status = await Permissions.CheckStatusAsync<T>(); if (status == PermissionStatus.Granted) return status; status = await Permissions.RequestAsync<T>(); return status; } private async Task CheckPermissions() { await CheckAndRequestPermission<Permissions.LocationWhenInUse>(); await CheckAndRequestPermission<Permissions.LocationAlways>(); } }
Здесь мы определили универсальный метод CheckAndRequestPermission<T>()
для запроса необходимых разрешений у пользователя. При этом, в качестве параметра T
у этого метода выступает один из наследников класса Permissions.BasePermission
. Этот метод мы используем в методе CheckPermissions()
, который проверяет необходимые разрешения
private async Task CheckPermissions() { await CheckAndRequestPermission<Permissions.LocationWhenInUse>(); await CheckAndRequestPermission<Permissions.LocationAlways>(); }
Выше было сказано, что нашему приложению необходимо три разрешения, однако вызовов метода CheckAndRequestPermission()
всего два. Дело в том, что использование класса Permissions.LocationWhenInUse
запрашивает у пользователя два разрешения: ACCESS_COARSE_LOCATION
и ACCESS_FINE_LOCATION, а Permissions.LocationAlways
— третье разрешение ACCESS_BACKGROUND_LOCATION
.
Теперь протестируем наше приложение. Запустите его и нажмите кнопку «Получить разрешения». Вы увидите следующее диалоговое окно:
После того, как вы разрешите использовать данные о местоположении устройства, откроется второй диалог:
для выбора разрешения того, в каком режиме приложение может считывать данные о местоположении. Теперь, когда мы можем получать необходимые разрешения от пользователя, можно приступать непосредственно к работе с интерфейсом
IGeolocation
.
Получение данных о последнем местоположении
Устройства могут кэшировать данные о последнем местоположении и часто получить последнее местоположение из кэша устройства бывает быстрее, чем ожидать выполнение запроса. При этом, стоит учитывать, что полученные из кэша данные могут быть менее точными. Перепишем код компонента Home
следующим образом:
@page "/" <button @onclick="GetCachedLocation" class="btn-danger">Получить последнее местоположение</button> <p>Последнее местоположение: @data</p> @code{ string data = "Неизвестно"; public async Task<PermissionStatus> CheckAndRequestPermission<T>() where T: Permissions.BasePermission, new() { PermissionStatus status = await Permissions.CheckStatusAsync<T>(); if (status == PermissionStatus.Granted) return status; status = await Permissions.RequestAsync<T>(); return status; } public async Task GetCachedLocation() { await CheckPermissions(); try { Location? location = await Geolocation.Default.GetLastKnownLocationAsync(); if (location != null) data = $"Latitude: {location.Latitude}, Longitude: {location.Longitude}, Altitude: {location.Altitude}"; } catch (FeatureNotSupportedException fnsEx) { data = "Функция не поддерживается на этом устройстве"; } catch (FeatureNotEnabledException fneEx) { data = "Функция не доступна на этом устройстве"; } catch (PermissionException pEx) { data = "Нет разрешения на получение местоположения устройства"; } catch (Exception ex) { data = "При получении местоположения возникла ошибка"; } data = "Данные не кэшировались"; await InvokeAsync(StateHasChanged); } private async Task CheckPermissions() { await CheckAndRequestPermission<Permissions.LocationWhenInUse>(); await CheckAndRequestPermission<Permissions.LocationAlways>(); } }
Здесь мы добавили новый метод проверки кэшированных данных о местоположении устройства:
public async Task GetCachedLocation() { await CheckPermissions(); try { Location? location = await Geolocation.Default.GetLastKnownLocationAsync(); if (location != null) data = $"Latitude: {location.Latitude}, Longitude: {location.Longitude}, Altitude: {location.Altitude}"; } catch (FeatureNotSupportedException fnsEx) { data = "Функция не поддерживается на этом устройстве"; } catch (FeatureNotEnabledException fneEx) { data = "Функция не доступна на этом устройстве"; } catch (PermissionException pEx) { data = "Нет разрешения на получение местоположения устройства"; } catch (Exception ex) { data = "При получении местоположения возникла ошибка"; } data = "Данные не кэшировались"; await InvokeAsync(StateHasChanged); }
Метод GetLastKnownLocationAsync()
возвращает либо null
, либо объект Location
, который содержит данные о координатах. Также в нашем методе определено несколько фильтров исключений с помощью которых мы можем более подробно указать пользователю на ошибку получения координат местоположения. Также мы немного изменили разметку компонента:
<button @onclick="GetCachedLocation" class="btn-danger">Получить последнее местоположение</button> <p>Последнее местоположение: @data</p>
добавив в обработчик onclick
кнопки новый метод. Теперь можно запустить приложение и нажать кнопку «Получить последнее местоположение» — приложение запросит необходимые разрешения и, если на вашем устройстве имеются кэшированные данные о последнем местоположении, то вы их увидите. В момем случае кэшированных данные не было обнаружено:
Получение текущего местоположения
Для получения текущего местоположения устройства используется метод GetLocationAsync()
. В отличие от предыдущего метода, здесь мы можем указать точность определения координат, а также время ожидания результата. При этом, при использовании метода GetLocationAsync()
рекомендуется обеспечить поддержку отмены операции. Итак, допишем наш компонент Home
приложения следующим образом (ниже представлены только изменения в компоненте):
@page "/" тут разметка для получения последнего местоположения <button @onclick="GetCurrentLocation" class="btn-primary">Получить текущее местоположение</button> <button @onclick="CancelRequest" class="btn-danger">Отменить запрос</button> <br /> <p>Текущее местоположение: @currentLocation</p> @code{ string currentLocation = "Неизвестно"; private CancellationTokenSource _cancelTokenSource; private bool _isCheckingLocation; тут методы для проверки разрешений и получения последнего местоположения public async Task GetCurrentLocation() { await CheckPermissions(); try { _isCheckingLocation = true; GeolocationRequest request = new GeolocationRequest(GeolocationAccuracy.Best, TimeSpan.FromSeconds(10)); _cancelTokenSource = new CancellationTokenSource(); Location? location = await Geolocation.Default.GetLocationAsync(request, _cancelTokenSource.Token); if (location != null) currentLocation = $"Latitude: {location.Latitude}, Longitude: {location.Longitude}, Altitude: {location.Altitude}"; } catch (FeatureNotSupportedException fnsEx) { currentLocation = "Функция не поддерживается на этом устройстве"; } catch (FeatureNotEnabledException fneEx) { currentLocation = "Функция не доступна на этом устройстве"; } catch (PermissionException pEx) { currentLocation = "Нет разрешения на получение местоположения устройства"; } catch (Exception ex) { currentLocation = "При получении местоположения возникла ошибка"; } finally { _isCheckingLocation = false; } await InvokeAsync(StateHasChanged); } public void CancelRequest() { if (_isCheckingLocation && _cancelTokenSource != null && _cancelTokenSource.IsCancellationRequested == false) _cancelTokenSource.Cancel(); } }
Так как получение текущего местоположения может занимать продолжительное время, то мы добавили в компонент новый метод для отмены операции:
public void CancelRequest() { if (_isCheckingLocation && _cancelTokenSource != null && _cancelTokenSource.IsCancellationRequested == false) _cancelTokenSource.Cancel(); }
Теперь посмотрим на метод GetCurrentLocation()
. Обработка ошибок в этом методе происходит также, как и при получении последнего местоположения, поэтому эту часть метода пропустим. Чтобы получить текущее местоположение мы должны создать запрос на получение данных:
GeolocationRequest request = new GeolocationRequest(GeolocationAccuracy.Best, TimeSpan.FromSeconds(10));
создать токен отмены и выполнить запрос:
_cancelTokenSource = new CancellationTokenSource(); Location? location = await Geolocation.Default.GetLocationAsync(request, _cancelTokenSource.Token);
Что касается запроса на получение местоположения, то это объект типа GeolocationRequest
, который имеет следующие свойства:
Свойство | Тип | Описание |
Desired |
GeolocationAccuracy |
Точность определения местоположения. Возможные значения:
|
Request |
bool |
Указывает следует ли запрашивать разрешение на временное использование служб расположения с полной точностью. |
Timeout |
TimeSpan |
Время ожидания запроса расположения. |
Таким образом, в нашем запросе мы просим предоставить координаты с самой лучшей точность и, если ответ не поступит в течение 10 секунд, то запрос прерывается. В разметке компонента мы добавляем две кнопки — на получение данных и отмену операции, а также поле для вывода полученных значений:
<br/> <button @onclick="GetCurrentLocation" class="btn-primary">Получить текущее местоположение</button> <button @onclick="CancelRequest" class="btn-danger">Отменить запрос</button> <br /> <p>Текущее местоположение: @currentLocation</p>
Запустим приложение, получим необходимые разрешения от пользователя и воспользуемся новым методом. После того, как будет нажата кнопка «Получить текущее местоположение» пройдет несколько секунд прежде, чем будет получен результат:
Следует отметить, что в текущем местоположении может отсутствовать значение высоты (Altitude
).
Использование событий IGeolocation
События IGeolocation
удобно использовать в том случае, если вам необходимо постоянно получать данные о местоположении устройства, например, для отслеживания перемещения. Рассмотрим использование событий на примере нашего приложения. Добавим в компонент Home
следующий код:
@page "/" тут разметка компонента, созданная ранее <br /> <button @onclick="OnStartListening" class="btn-primary">Отслеживать местоположение</button> <button @onclick="OnStopListening" class="btn-danger">Остановить отслеживание</button> <br /> <p>Последнее местоположение: @status</p> @code{ тут другие переменные string status = "Неизвестно"; async void OnStartListening() { try { await CheckPermissions(); Geolocation.LocationChanged += LocationChanged; Geolocation.ListeningFailed += ListeningFiled; var request = new GeolocationListeningRequest(GeolocationAccuracy.Best); request.MinimumTime = TimeSpan.FromMilliseconds(50); var success = await Geolocation.StartListeningForegroundAsync(request); status = success ? "Приложение отслеживает перемещение устройства" : "Не смогли запустить прослушивание события"; } catch (Exception ex) { status = "Во время включения прослушивания события LocationChanged произошла ошибка"; } } void OnStopListening() { if (Geolocation.IsListeningForeground) { Geolocation.StopListeningForeground(); Geolocation.LocationChanged -= LocationChanged; Geolocation.ListeningFailed -= ListeningFiled; status = "Отслеживание перемещения остановлено"; } } void ListeningFiled(object? sender, GeolocationListeningFailedEventArgs e) { status = $"Ошибка определения местоположения {e.Error}"; } void LocationChanged(object? sender, GeolocationLocationChangedEventArgs e) { if (e.Location != null) { status = $"Latitude: {e.Location.Latitude}, Longitude: {e.Location.Longitude}, Altitude: {e.Location.Altitude}"; } } тут прочий код компонента, разработанный ранее }
Здесь мы добавили два обработчика событий IGeolocation
:
void ListeningFiled(object? sender, GeolocationListeningFailedEventArgs e) { status = $"Ошибка определения местоположения {e.Error}"; } void LocationChanged(object? sender, GeolocationLocationChangedEventArgs e) { if (e.Location != null) { status = $"Latitude: {e.Location.Latitude}, Longitude: {e.Location.Longitude}, Altitude: {e.Location.Altitude}"; } }
Первый обработчик срабатывает, если при определении местоположения возникает какая-либо ошибка, а второй — срабатывает каждый раз при изменении местоположения. Метод OnStartListening()
запускает прослушивание событий:
Geolocation.LocationChanged += LocationChanged; Geolocation.ListeningFailed += ListeningFiled; var request = new GeolocationListeningRequest(GeolocationAccuracy.Best); request.MinimumTime = TimeSpan.FromMilliseconds(50); var success = await Geolocation.StartListeningForegroundAsync(request);
Здесь для начала прослушивания мы также создаем запрос — объект типа GeolocationListeningRequest
, в котором указываем требуемую точность, а также минимальный интервал времени для отправки сообщений о событии.
var request = new GeolocationListeningRequest(GeolocationAccuracy.Best); request.MinimumTime = TimeSpan.FromMilliseconds(50);
Этот запрос мы передаем в метод StartListeningForegroundAsync()
для включения прослушивания. Соответственно, для остановки прослушивания мы используем метод
void OnStopListening() { if (Geolocation.IsListeningForeground) { Geolocation.StopListeningForeground(); Geolocation.LocationChanged -= LocationChanged; Geolocation.ListeningFailed -= ListeningFiled; status = "Отслеживание перемещения остановлено"; } }
После запуска приложения и включения прослушивания вы увидите на экране следующее сообщение:
После смены местоположения вы увидите координаты местоположения устройства:
LocationChanged
срабатывает не всегда. Так, например на Android 14, у меня событие срабатывало только после того, как я перемещался на расстояние порядка 100-150 метров от начальной точки в которой начиналось отслеживание местоположения. На устройстве с Android 12 событие не срабатывало вообще, хотя все необходимые разрешения приложению были выданы, WebView обновлен до последней версии.Итого
Интерфейс IGeolocation
позволяет получить доступ к системе геолокации на устройстве Android. Используя методы IGeolocation
мы можем запрашивать последнее местоположение из кэша устройства, определять текущее местоположение устройства, а также получать уведомления об изменении положения устройства.