Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Essentials] Geolocation foreground listening #9572

Merged
merged 23 commits into from
Feb 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
63e5c21
added geolocation foreground listening public API without implementation
vividos Aug 21, 2022
6b94c1c
added section on geolocation sample page for foreground listening fea…
vividos Aug 22, 2022
c350b77
implemented geolocation foreground listening for Android, iOS, MacOS …
vividos Aug 22, 2022
478c172
moved common code for determining DesiredAccuracy on UWP to extension…
vividos Sep 5, 2022
95acfd8
renamed class ListeningRequest to GeolocationListeningRequest
vividos Jan 22, 2023
a21d39f
renamed property IsListening to IsListeningForeground
vividos Jan 22, 2023
1adefda
added xml documentation for all new public methods and properties
vividos Jan 22, 2023
25f81c3
implemented event LocationError, using enum GeolocationError and clas…
vividos Jan 22, 2023
90ac7d5
fixed potential leak where ContinuousLocationListener keeps the refer…
vividos Jan 22, 2023
e44a8bc
changed StopListeningForegroundAsync() to StopListeningForeground() a…
vividos Jan 22, 2023
2c5448f
fixed error in Essentials samples where async keyword is not necessar…
vividos Jan 22, 2023
cdb1003
enabled nullable checks for GeolocationListeningRequest class
vividos Jan 22, 2023
06062db
renamed ListeningRequest.ios.macos.cs to match class name; no source …
vividos Jan 28, 2023
549269c
call StopListeningForeground() on Android, iOS and macOS before signa…
vividos Feb 2, 2023
b737cc9
replaced throwing ArgumentNullException with ArgumentNullException.Th…
vividos Feb 2, 2023
ef775d2
added xml documentation for all newly added public geolocaion foregro…
vividos Feb 2, 2023
187f45d
removed duplicated code for determining GeolocationAccuracy on iOS an…
vividos Feb 2, 2023
f4b4637
renamed event LocationError to ListeningFailed and GeolocationErrorEv…
vividos Feb 4, 2023
51ebc87
fixed IsListeningForeground property on Windows
vividos Feb 4, 2023
6f0ff82
Merge branch 'main' into geolocation-foreground-listener
jfversluis Feb 13, 2023
9dd72a2
Fixed naming
jfversluis Feb 13, 2023
6ed2ac7
Update GeolocationViewModel.cs
jfversluis Feb 13, 2023
35fdb18
Thank you for your pull request. We are auto-formating your source co…
Feb 13, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions src/Essentials/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>
</Grid>
Expand Down
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.IsListeningForeground;

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,53 @@ async void OnGetCurrentLocation()
IsBusy = false;
}

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

var request = new GeolocationListeningRequest((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, GeolocationLocationChangedEventArgs e)
{
ListeningLocation = FormatLocation(e.Location);
}

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

Geolocation.StopListeningForeground();

ListeningLocationStatus = "Stopped listening for foreground location updates";
}
catch (Exception ex)
{
ListeningLocationStatus = FormatLocation(null, ex);
}

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

string FormatLocation(Location location, Exception ex = null)
{
if (location == null)
Expand Down Expand Up @@ -116,6 +187,9 @@ public override void OnDisappearing()
if (cts != null && !cts.IsCancellationRequested)
cts.Cancel();
}

OnStopListening();

base.OnDisappearing();
}
}
Expand Down
169 changes: 168 additions & 1 deletion src/Essentials/src/Geolocation/Geolocation.android.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,19 @@ partial class GeolocationImplementation : IGeolocation
const long twoMinutes = 120000;
static readonly string[] ignoredProviders = new string[] { LocationManager.PassiveProvider, "local_database" };

static ContinuousLocationListener continuousListener;
static List<string> listeningProviders;

static LocationManager locationManager;

static LocationManager LocationManager =>
locationManager ??= Application.Context.GetSystemService(Context.LocationService) as LocationManager;

/// <summary>
/// Indicates if currently listening to location updates while the app is in foreground.
/// </summary>
public bool IsListeningForeground { get => continuousListener != null; }

public async Task<Location> GetLastKnownLocationAsync()
{
await Permissions.EnsureGrantedOrRestrictedAsync<Permissions.LocationWhenInUse>();
Expand All @@ -42,7 +50,7 @@ public async Task<Location> GetLastKnownLocationAsync()

public async Task<Location> GetLocationAsync(GeolocationRequest request, CancellationToken cancellationToken)
{
_ = request ?? throw new ArgumentNullException(nameof(request));
ArgumentNullException.ThrowIfNull(request);

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

Expand Down Expand Up @@ -112,6 +120,101 @@ void RemoveUpdates()
}
}

/// <summary>
/// Starts listening to location updates using the <see cref="Geolocation.LocationChanged"/>
/// event or the <see cref="Geolocation.ListeningFailed"/> event. Events may only sent when
/// the app is in the foreground. Requests <see cref="Permissions.LocationWhenInUse"/>
/// from the user.
/// </summary>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="request"/> is <see langword="null"/>.</exception>
/// <exception cref="FeatureNotSupportedException">Thrown if listening is not supported on this platform.</exception>
/// <exception cref="InvalidOperationException">Thrown if already listening and <see cref="IsListeningForeground"/> returns <see langword="true"/>.</exception>
/// <param name="request">The listening request parameters to use.</param>
/// <returns><see langword="true"/> when listening was started, or <see langword="false"/> when listening couldn't be started.</returns>
public async Task<bool> StartListeningForegroundAsync(GeolocationListeningRequest request)
vividos marked this conversation as resolved.
Show resolved Hide resolved
{
ArgumentNullException.ThrowIfNull(request);

if (IsListeningForeground)
throw new InvalidOperationException("Already listening to location changes.");

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

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(Android.Locations.LocationManager.GpsProvider))
listeningProviders.Add(Android.Locations.LocationManager.GpsProvider);
if (allProviders.Contains(Android.Locations.LocationManager.NetworkProvider))
listeningProviders.Add(Android.Locations.LocationManager.NetworkProvider);

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

var continuousListener = new ContinuousLocationListener(LocationManager, providerInfo.Accuracy, listeningProviders);
continuousListener.LocationHandler = HandleLocation;
continuousListener.ErrorHandler = HandleError;

// 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 location)
{
OnLocationChanged(location.ToLocation());
}

void HandleError(GeolocationError geolocationError)
{
StopListeningForeground();
OnLocationError(geolocationError);
}
}

/// <summary>
/// Stop listening for location updates when the app is in the foreground.
/// Has no effect when not listening and <see cref="Geolocation.IsListeningForeground"/>
/// is currently <see langword="false"/>.
/// </summary>
public void StopListeningForeground()
vividos marked this conversation as resolved.
Show resolved Hide resolved
{
if (continuousListener == null)
return;

continuousListener.LocationHandler = null;
continuousListener.ErrorHandler = null;

if (listeningProviders == null)
return;

for (var i = 0; i < listeningProviders.Count; i++)
{
LocationManager.RemoveUpdates(continuousListener);
}

continuousListener = null;
}

static (string Provider, float Accuracy) GetBestProvider(LocationManager locationManager, GeolocationAccuracy accuracy)
{
// Criteria: https://developer.android.com/reference/android/location/Criteria
Expand Down Expand Up @@ -276,4 +379,68 @@ void ILocationListener.OnStatusChanged(string provider, Availability status, Bun
}
}
}

class ContinuousLocationListener : Java.Lang.Object, ILocationListener
{
readonly LocationManager manager;

float desiredAccuracy;

HashSet<string> activeProviders = new HashSet<string>();

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

internal Action<GeolocationError> ErrorHandler { get; set; }

internal ContinuousLocationListener(LocationManager manager, float desiredAccuracy, IEnumerable<string> providers)
{
this.manager = manager;
this.desiredAccuracy = desiredAccuracy;

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

void ILocationListener.OnLocationChanged(AndroidLocation location)
{
if (location.Accuracy <= desiredAccuracy)
{
LocationHandler?.Invoke(location);
return;
}
}

void ILocationListener.OnProviderDisabled(string provider)
{
lock (activeProviders)
{
if (activeProviders.Remove(provider) &&
activeProviders.Count == 0)
ErrorHandler?.Invoke(GeolocationError.PositionUnavailable);
}
}

void ILocationListener.OnProviderEnabled(string provider)
{
lock (activeProviders)
activeProviders.Add(provider);
}

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

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