This may not exactly be a question about the Maps SDK for .NET, but given that it's a .NET MAUI app that uses the SDK and location services, I thought I'd probe the mobile dev community for advice.
I have a .NET MAUI app with a map page that I'm using to track the location of the user on a map. It basically shows the breadcrumb path the user has taken on their hike with a dashboard showing their distance, timer and average speed. It works exactly as it should when the screen is turned on and the app is on the foreground: path updates when location is changed and dashboard metrics change as expected.
But when the app is minimized/suspended, or if another app is being used, or if the screen is turned off, location stops being tracked. And the timer pauses in the background as well. I restructured some of this code for location tracking to work on a background thread like following, but no changes in results.
mapViewModel.locationDataSource.LocationChanged += async (sender, e) => await LocationChanged(sender, e); // call LocationChanged method when location changes in LocationDataSource
private async Task LocationChanged(object sender, Esri.ArcGISRuntime.Location.Location e)
{
await Task.Run(() => {
MapPoint mp = e.Position;
UpdatePolyline(mp)
}
private void UpdatePolyline(MapPoint mp)
{
polylineBuilder.AddPoint(mp);
Geometry polylineGeometry = polylineBuilder.ToGeometry();
// UI updates marshaled to the main thread.
MainThread.BeginInvokeOnMainThread(() =>
{
trackedHikePolylineGraphicsOverlay.Graphics.Clear();
trackedHikePolylineGraphicsOverlay.Graphics.Add(new Graphic(polylineGeometry));
});
}
I've also added these permissions to my AndroidManifest.xml
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
I will also need to add similar permissions to the .plist for iOS permissions. Not sure what they are yet.
What have other developers done to implement background location tracking in their .NET MAUI apps? I know Android and iOS apps can do this, because I can record a manual activity in Strava while my screen is turned off. Permissions for my Maui app is set to "Allow location all the time" for Precise location.
Thanks in advance!
Solved! Go to Solution.
Awesome! Glad you got it working and thank you for bringing this up - this made us realize we need to help with a little more guidance here, and are working on that now. Wrt iOS, note that there are a few specific properties on iOS only to enable background location on the SystemLocationDatasource. You'll need to put that code in an "#if __IOS__" section since it only applies to ios that will turn on backgrounding of location. See the doc here and the remarks:
You shouldn't need to work with CLLocation directly.
I am going to first start by asking what version of .NET Maps SDK are you using? There have been a few location related fixes in past couple releases. I recommend using latest version (200.6) of .NET Maps SDK to test this.
We also have a tutorial on display device tutorial for .NET MAUI that talks about enabling location in .NET MAUI Applications. Tutorial provides step-by-step directions to create an app or you can download the solution and try it yourself. Tutorial also provides information about required permission that might be helpful for iOS and Android. I think you need following two added to the Android manifest.
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
There is a section on iOS permissions on that tutorial that will provide guidance on setting up iOS permissions.
If you still encounter an issue, provide us a reproducible case and I will be more than happy to take a look.
Hope this helps,
Preeti
Hi @PreetiMaske , thank you for your reply. It's great to have your support on this issue.
We are using 200.6.0 of Esri.ArcGISRuntime.Maui and are upgraded on pretty much all Nuget packages.
I should apologize and say I neglected to include all Android permissions I'm using in the original post. I updated my post to reflect all permissions we're requesting in Android that are location related. As you can see, we are requesting FINE_LOCATION and COARSE_LOCATION permissions.
I pulled the sample app you linked to and ran it locally. I'm experiencing the same issue with this sample, because the issue is NOT with being able to access user's location while the app is running in the foreground. The problem arises when the user needs to shut the screen off and hike with the phone in their pocket, or switch apps to take a picture of a squirrel. When they have to do this, the location stops being tracked. If you look at the below screenshot, the purple graphic shows the route the user takes with the phone in their pocket.
There are sharp cuts to the user's current location when they turn their screen back on and go back to the app. In other words, the application only records location in the foreground.
I notice that in the sample you provided, ACCESS_BACKGROUND_LOCATION permission is not enabled, implying it was meant to run only in the foreground. Are there are any samples out there for getting this info from the device on a background thread or using background/foreground services perhaps?
I will try and upload a more complete solution of just this tracking feature from my application to a new repo, so you can pull it and check the code yourself. Thank you for your help! 🙂
@PreetiMaske Here is a project on Github you can pull and run yourself: https://github.com/Cesium133/.netmaui-display-device-location
I created this from the Display Device Location sample you provided and the Show Location History sample from the Maps SDK team. You will see that if you run this on a physical device, the location will not update if the app is suspended or minimized.
Please let me know what else you may need to help us debug this issue! Thanks!
We're working on a sample/tutorial/doc to help with this scenario. But take a look at Android's doc on this, especially foreground service (keeps app open in the background):
https://developer.android.com/develop/background-work/services/fgs
and background services: https://developer.android.com/develop/sensors-and-location/location/background (runs while app isn't running)
Thanks @dotMorten_esri ! With some elbow grease, we've managed to get background location tracking working on Android with Foreground service. When I get a chance (probably not anytime soon), I will update my Github repo with the code we used. Meanwhile here is what we used in LocationService.cs that we added to Platforms/Android directory
using Android.App;
using Android.Content;
using Android.OS;
using Android.Runtime;
using AndroidX.Core.App;
using Esri.ArcGISRuntime.Location;
using Microsoft.Maui.Controls.Compatibility.Platform.Android;
using Microsoft.Maui.Devices.Sensors;
using Resource = Microsoft.Maui.Resource;
using Location = Esri.ArcGISRuntime.Location.Location;
namespace MyApp.Platforms.Android.LS;
[Service(ForegroundServiceType = global::Android.Content.PM.ForegroundService.TypeLocation)]
public class LocationService : Service
{
public override IBinder OnBind(Intent intent) => null;
public event EventHandler<Location> LocationUpdated;
public LocationDataSource locationDataSource;
private async void OnLocationChanged(Location newLocation)
{
LocationUpdated?.Invoke(this, newLocation);
}
public override StartCommandResult OnStartCommand(Intent intent, StartCommandFlags flags, int startId)
{
StartForeground(1, CreateNotification());
var locationIntent = new Intent(this, typeof(LocationService));
if (Build.VERSION.SdkInt >= BuildVersionCodes.O)
{
StartForegroundService(locationIntent);
}
else
{
StartService(locationIntent);
}
return StartCommandResult.Sticky;
}
private Notification CreateNotification()
{
// don't really need notifications for our app, but examples online used it
var channelId = "location_notification";
var channelName = "Location Tracking";
var notificationManager = (NotificationManager)GetSystemService(NotificationService);
var channel = new NotificationChannel(channelId, channelName, NotificationImportance.Default);
notificationManager.CreateNotificationChannel(channel);
var notification = new NotificationCompat.Builder(this, channelId)
.SetContentTitle("Location Tracking")
.SetContentText("Tracking location in the background")
//.SetSmallIcon(Resource.Drawable.icon)
.Build();
return notification;
}
public async Task<LocationDataSource> StartLocationUpdates()
{
locationDataSource = new SystemLocationDataSource();
PermissionStatus status = await Permissions.CheckStatusAsync<Permissions.LocationWhenInUse>();
if (status == PermissionStatus.Denied || status == PermissionStatus.Unknown)
{
await Shell.Current.DisplayAlert("Access Requested", "Please allow precise location all the time to track while phone is locked or viewing other applications.", "OK");
status = await Permissions.RequestAsync<Permissions.LocationWhenInUse>();
}
if (status != PermissionStatus.Granted)
{
return null;
}
await locationDataSource.StartAsync();
locationDataSource.LocationChanged += async (sender, e) => OnLocationChanged(e);
return locationDataSource;
}
}
We call StartLocationUpdates() from the MapPage.xaml.cs and set LocationDataSource
await this.locationService.StartLocationUpdates();
mapViewModel.locationDataSource = this.locationService.locationDataSource;
if (mapViewModel.locationDataSource.Status == LocationDataSourceStatus.Started)
{
MyMapView.LocationDisplay.DataSource = mapViewModel.locationDataSource;
MyMapView.LocationDisplay.IsEnabled = status == PermissionStatus.Granted || status == PermissionStatus.Restricted;
MyMapView.LocationDisplay.InitialZoomScale = 1000;
MyMapView.LocationDisplay.AutoPanMode = LocationDisplayAutoPanMode.Recenter;
}
this.locationService.LocationUpdated += async (sender, e) => await LocationUpdated(sender, e);
Works like a dream when it's not killing the battery. We're working on implementing this in iOS right now with CLLocation.
Thank you so much for sharing your code. This was the missing component I needed. I had been using Android Sensors to solve this problem (Microsoft.Maui.Devices.Sensor.Geolocation), and that was working great for me with the Foreground Service code similar to what you showed in your example. However, it would have required me to essentially write code similar to LocationDisplay (for creation of the various symbol properties) , and I really wanted to just use the ESRI LocationDisplay code instead. But it got all messed up when the Android Devices would go to sleep. I'll implement this shortly. Thanks again
Thanks @dotMorten_esri ! With some elbow grease, we've managed to get background location tracking working on Android with Foreground service. When I get a chance (probably not anytime soon), I will update my Github repo with the code we used. Meanwhile here is what we used in LocationService.cs that we added to Platforms/Android directory
using Android.App;
using Android.Content;
using Android.OS;
using Android.Runtime;
using AndroidX.Core.App;
using Esri.ArcGISRuntime.Location;
using Microsoft.Maui.Controls.Compatibility.Platform.Android;
using Microsoft.Maui.Devices.Sensors;
using Resource = Microsoft.Maui.Resource;
using Location = Esri.ArcGISRuntime.Location.Location;
namespace MyApp.Platforms.Android.LS;
[Service(ForegroundServiceType = global::Android.Content.PM.ForegroundService.TypeLocation)]
public class LocationService : Service
{
public override IBinder OnBind(Intent intent) => null;
public event EventHandler<Location> LocationUpdated;
int count = 0;
public LocationDataSource locationDataSource;
private async void OnLocationChanged(Location newLocation)
{
LocationUpdated?.Invoke(this, newLocation);
}
public override StartCommandResult OnStartCommand(Intent intent, StartCommandFlags flags, int startId)
{
StartForeground(1, CreateNotification());
var locationIntent = new Intent(this, typeof(LocationService));
if (Build.VERSION.SdkInt >= BuildVersionCodes.O)
{
StartForegroundService(locationIntent);
}
else
{
StartService(locationIntent);
}
return StartCommandResult.Sticky;
}
private Notification CreateNotification()
{
// don't really need notifications for our app, but examples online used it
var channelId = "location_notification";
var channelName = "Location Tracking";
var notificationManager = (NotificationManager)GetSystemService(NotificationService);
var channel = new NotificationChannel(channelId, channelName, NotificationImportance.Default);
notificationManager.CreateNotificationChannel(channel);
var notification = new NotificationCompat.Builder(this, channelId)
.SetContentTitle("Location Tracking")
.SetContentText("Tracking location in the background")
//.SetSmallIcon(Resource.Drawable.icon)
.Build();
return notification;
}
public async Task<LocationDataSource> StartLocationUpdates()
{
locationDataSource = new SystemLocationDataSource();
PermissionStatus status = await Permissions.CheckStatusAsync<Permissions.LocationWhenInUse>();
if (status == PermissionStatus.Denied || status == PermissionStatus.Unknown)
{
await Shell.Current.DisplayAlert("Access Requested", "Please allow precise location all the time to track while phone is locked or viewing other applications.", "OK");
status = await Permissions.RequestAsync<Permissions.LocationWhenInUse>();
}
if (status != PermissionStatus.Granted)
{
return null;
}
await locationDataSource.StartAsync();
locationDataSource.LocationChanged += async (sender, e) => OnLocationChanged(e);
return locationDataSource;
}
}
We call StartLocationUpdates() from the MapPage.xaml.cs and set LocationDataSource
await this.locationService.StartLocationUpdates();
mapViewModel.locationDataSource = this.locationService.locationDataSource;
if (mapViewModel.locationDataSource.Status == LocationDataSourceStatus.Started)
{
MyMapView.LocationDisplay.DataSource = mapViewModel.locationDataSource;
MyMapView.LocationDisplay.IsEnabled = status == PermissionStatus.Granted || status == PermissionStatus.Restricted;
MyMapView.LocationDisplay.InitialZoomScale = 1000;
MyMapView.LocationDisplay.AutoPanMode = LocationDisplayAutoPanMode.Recenter;
}
this.locationService.LocationUpdated += async (sender, e) => await LocationUpdated(sender, e);
Works like a dream when it's not killing the battery. We're working on implementing this in iOS right now with CLLocation
Awesome! Glad you got it working and thank you for bringing this up - this made us realize we need to help with a little more guidance here, and are working on that now. Wrt iOS, note that there are a few specific properties on iOS only to enable background location on the SystemLocationDatasource. You'll need to put that code in an "#if __IOS__" section since it only applies to ios that will turn on backgrounding of location. See the doc here and the remarks:
You shouldn't need to work with CLLocation directly.
Wow, that is amazingly simple! And it looks like it works great on IOS! (handles keeping the associated processes active as well) Thanks!
I was searching for similar within the .NET android documentation and could not find anything related, do you have any suggestions on the best way to handle that?