Skip to content
This repository was archived by the owner on May 15, 2024. It is now read-only.

Geolocation foreground listener #1579

Closed
wants to merge 7 commits into from
10 changes: 10 additions & 0 deletions Samples/Samples/View/GeolocationPage.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,16 @@
IsEnabled="{Binding IsNotBusy}"
HorizontalOptions="FillAndExpand" />
<Button Text="Refresh" Command="{Binding GetCurrentLocationCommand}" IsEnabled="{Binding IsNotBusy}" />

<Label Text="Foreground listener for Location:" FontAttributes="Bold" Margin="0,6,0,0" />
<StackLayout Orientation="Horizontal" Spacing="6">
<Button Text="Start Listening" HorizontalOptions="FillAndExpand"
Command="{Binding StartListeningCommand}" IsEnabled="{Binding IsNotListening}" />
<Button Text="Stop listening" HorizontalOptions="FillAndExpand"
Command="{Binding StopListeningCommand}" IsEnabled="{Binding IsListening}" />
</StackLayout>
<Label Text="{Binding ListeningLocationStatus}" />
<Label Text="{Binding ListeningLocation}" />
</StackLayout>
</ScrollView>
</StackLayout>
Expand Down
78 changes: 77 additions & 1 deletion Samples/Samples/ViewModel/GeolocationViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,25 @@ public class GeolocationViewModel : BaseViewModel
string currentLocation;
int accuracy = (int)GeolocationAccuracy.Default;
CancellationTokenSource cts;
string listeningLocation;
string listeningLocationStatus;

public GeolocationViewModel()
{
GetLastLocationCommand = new Command(OnGetLastLocation);
GetCurrentLocationCommand = new Command(OnGetCurrentLocation);
StartListeningCommand = new Command(OnStartListening);
StopListeningCommand = new Command(OnStopListening);
}

public ICommand GetLastLocationCommand { get; }

public ICommand GetCurrentLocationCommand { get; }

public ICommand StartListeningCommand { get; }

public ICommand StopListeningCommand { get; }

public string LastLocation
{
get => lastLocation;
Expand All @@ -45,6 +53,22 @@ public int Accuracy
set => SetProperty(ref accuracy, value);
}

public bool IsListening => Geolocation.IsListening;

public bool IsNotListening => !IsListening;

public string ListeningLocation
{
get => listeningLocation;
set => SetProperty(ref listeningLocation, value);
}

public string ListeningLocationStatus
{
get => listeningLocationStatus;
set => SetProperty(ref listeningLocationStatus, value);
}

async void OnGetLastLocation()
{
if (IsBusy)
Expand Down Expand Up @@ -88,6 +112,55 @@ async void OnGetCurrentLocation()
IsBusy = false;
}

async void OnStartListening()
{
try
{
Geolocation.LocationChanged += Geolocation_LocationChanged;

var request = new ListeningRequest((GeolocationAccuracy)Accuracy);

var success = await Geolocation.StartListeningForegroundAsync(request);

ListeningLocationStatus = success
? "Started listening for foreground location updates"
: "Couldn't start listening";
}
catch (Exception ex)
{
ListeningLocationStatus = FormatLocation(null, ex);
}

OnPropertyChanged(nameof(IsListening));
OnPropertyChanged(nameof(IsNotListening));
}

void Geolocation_LocationChanged(object sender, LocationEventArgs e)
{
ListeningLocation = FormatLocation(e.Location);
}

async void OnStopListening()
{
try
{
Geolocation.LocationChanged -= Geolocation_LocationChanged;

var success = await Geolocation.StopListeningForegroundAsync();

ListeningLocationStatus = success
? "Stopped listening for foreground location updates"
: "Couldn't stop listening";
}
catch (Exception ex)
{
ListeningLocationStatus = FormatLocation(null, ex);
}

OnPropertyChanged(nameof(IsListening));
OnPropertyChanged(nameof(IsNotListening));
}

string FormatLocation(Location location, Exception ex = null)
{
if (location == null)
Expand All @@ -106,7 +179,7 @@ string FormatLocation(Location location, Exception ex = null)
$"Speed: {(location.Speed.HasValue ? location.Speed.Value.ToString() : notAvailable)}\n" +
$"Date (UTC): {location.Timestamp:d}\n" +
$"Time (UTC): {location.Timestamp:T}\n" +
$"Moking Provider: {location.IsFromMockProvider}";
$"Mocking Provider: {location.IsFromMockProvider}";
}

public override void OnDisappearing()
Expand All @@ -116,6 +189,9 @@ public override void OnDisappearing()
if (cts != null && !cts.IsCancellationRequested)
cts.Cancel();
}

OnStopListening();

base.OnDisappearing();
}
}
Expand Down
175 changes: 175 additions & 0 deletions Xamarin.Essentials/Geolocation/Geolocation.android.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ public static partial class Geolocation
const long twoMinutes = 120000;
static readonly string[] ignoredProviders = new string[] { LocationManager.PassiveProvider, "local_database" };

static ContinuousLocationListener continuousListener;
static List<string> listeningProviders;

static async Task<Location> PlatformLastKnownLocationAsync()
{
await Permissions.EnsureGrantedAsync<Permissions.LocationWhenInUse>();
Expand Down Expand Up @@ -195,6 +198,91 @@ internal static bool IsBetterLocation(AndroidLocation location, AndroidLocation

return false;
}

static bool PlatformIsListening() => continuousListener != null;

static async Task<bool> PlatformStartListeningForegroundAsync(ListeningRequest request)
{
if (IsListening)
throw new InvalidOperationException("This Geolocation is already listening");

await Permissions.EnsureGrantedAsync<Permissions.LocationWhenInUse>();

var locationManager = Platform.LocationManager;

var enabledProviders = locationManager.GetProviders(true);
var hasProviders = enabledProviders.Any(p => !ignoredProviders.Contains(p));

if (!hasProviders)
throw new FeatureNotEnabledException("Location services are not enabled on device.");

// get the best possible provider for the requested accuracy
var providerInfo = GetBestProvider(locationManager, request.DesiredAccuracy);

// if no providers exist, we can't listen for locations
if (string.IsNullOrEmpty(providerInfo.Provider))
return false;

var allProviders = locationManager.GetProviders(false);

listeningProviders = new List<string>();
if (allProviders.Contains(LocationManager.GpsProvider))
listeningProviders.Add(LocationManager.GpsProvider);
if (allProviders.Contains(LocationManager.NetworkProvider))
listeningProviders.Add(LocationManager.NetworkProvider);

if (listeningProviders.Count == 0)
listeningProviders.Add(providerInfo.Provider);

continuousListener = new ContinuousLocationListener(locationManager, request.MinimumTime, listeningProviders);
continuousListener.LocationHandler = HandleLocation;

// start getting location updates
// make sure to use a thread with a looper
var looper = Looper.MyLooper() ?? Looper.MainLooper;

var minTimeMilliseconds = (long)request.MinimumTime.TotalMilliseconds;

foreach (var provider in listeningProviders)
locationManager.RequestLocationUpdates(provider, minTimeMilliseconds, providerInfo.Accuracy, continuousListener, looper);

return true;

void HandleLocation(AndroidLocation androidLocation)
{
OnLocationChanged(androidLocation.ToLocation());
}
}

static Task<bool> PlatformStopListeningForegroundAsync()
{
if (continuousListener == null)
return Task.FromResult(true);

if (listeningProviders == null)
return Task.FromResult(true);

var providers = listeningProviders;
continuousListener.LocationHandler = null;

var locationManager = Platform.LocationManager;

for (var i = 0; i < providers.Count; i++)
{
try
{
locationManager.RemoveUpdates(continuousListener);
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine("Unable to remove updates: " + ex);
}
}

continuousListener = null;

return Task.FromResult(true);
}
}

class SingleLocationListener : Java.Lang.Object, ILocationListener
Expand Down Expand Up @@ -271,4 +359,91 @@ void ILocationListener.OnStatusChanged(string provider, [GeneratedEnum] Availabi
}
}
}

class ContinuousLocationListener : Java.Lang.Object, ILocationListener
{
readonly HashSet<string> activeProviders = new HashSet<string>();
readonly LocationManager manager;
IList<string> providers;

string activeProvider;
AndroidLocation lastLocation;
TimeSpan timePeriod;

public ContinuousLocationListener(LocationManager manager, TimeSpan timePeriod, IList<string> providers)
{
this.manager = manager;
this.timePeriod = timePeriod;
this.providers = providers;

foreach (var p in providers)
{
if (manager.IsProviderEnabled(p))
activeProviders.Add(p);
}
}

internal Action<AndroidLocation> LocationHandler { get; set; }

void ILocationListener.OnLocationChanged(AndroidLocation location)
{
if (location.Provider != activeProvider)
{
if (activeProvider != null && manager.IsProviderEnabled(activeProvider))
{
var pr = manager.GetProvider(location.Provider);
var lapsed = GetTimeSpan(location.Time) - GetTimeSpan(lastLocation.Time);

if (pr.Accuracy > manager.GetProvider(activeProvider).Accuracy
&& lapsed < timePeriod.Add(timePeriod))
{
location.Dispose();
return;
}
}

activeProvider = location.Provider;
}

var previous = Interlocked.Exchange(ref lastLocation, location);
if (previous != null)
previous.Dispose();

LocationHandler?.Invoke(location);
}

public void OnProviderDisabled(string provider)
{
lock (activeProviders)
activeProviders.Remove(provider);
}

public void OnProviderEnabled(string provider)
{
if (provider == LocationManager.PassiveProvider)
return;

lock (activeProviders)
activeProviders.Add(provider);
}

public void OnStatusChanged(string provider, Availability status, Bundle extras)
{
switch (status)
{
case Availability.Available:
OnProviderEnabled(provider);
break;

case Availability.OutOfService:
OnProviderDisabled(provider);
break;
}
}

TimeSpan GetTimeSpan(long time)
{
return new TimeSpan(TimeSpan.TicksPerMillisecond * time);
}
}
}
Loading