.NET MAUI (Android): Location-Tracking im Hintergrund
In .NET MAUI ist es sehr einach, während der Laufzeit der Anwendung auf die Positionsdaten des Benutzers zuzugreifen. Hierfür reicht ein Aufruf von
await Geolocation.GetLocationAsync(geolocationRequest, cancellationToken);
Die Abfrage der Positionsdaten, wenn die App nicht im Vordergrund ist, ist deutlich weniger trivial, da dafür via MAUI keine direkte Methodik zur Verfügung steht. Dieser Artikel zeigt, wie man trotzdem an die Positionsdaten kommt, wenn die App minimiert oder sogar geschlossen oder das Smartphone-Display ausgeschaltet ist.
Im GitHub-Repository der blecon GmbH steht ein Beispielprojekt zur Verfügung, dass das hier gezeigte Vorgehen implementiert.
LocationService
Zunächst wird ein Service erstellt, der für die tatsächliche Ermittlung der Positionsdaten zuständig ist.
public sealed class LocationService : ILocationService
{
private readonly GeolocationRequest _geolocationRequest = new(GeolocationAccuracy.Best);
public async Task Run(CancellationToken cancellationToken)
=> await Task.Run(async () =>
{
while (!cancellationToken.IsCancellationRequested)
{
await Task.Delay(5000);
var location = await Geolocation.GetLocationAsync(_geolocationRequest, cancellationToken);
if (location is null)
{
return;
}
WeakReferenceMessenger.Default.Send(new LocationMessage(location.Latitude, location.Longitude));
}
}, cancellationToken);
}
Er bietet nur eine Methode, in der in einer while-Schleife zyklisch die aktuellen Positionsdaten abgerufen werden. Diese werden über den WeakReferenceMessenger
, der mit dem MVVM Toolkit kommt, an interessierte Stellen weitergeleitet.
LocationForegroundService
Um den erstellten LocationService
zum Einsatz zu bringen, muss ein Android ForegroundService
implementiert werden. Diese Klasse ist android-spezifisch und muss daher unter /Platforms/Android
erstellt werden. Sie muss von Android.App.Service
ableiten und das folgende Attribut aufweisen.
[Service(Name = "de.blecon.bglocationtracking.LocationForegroundService")]
Es müssen drei Methoden überschrieben werden:
OnBind
OnStartCommand
OnStopCommand
OnBind
public override IBinder? OnBind(Intent? intent)
=> null;
Für unsere Zwecke ist OnBind
irrelevant, weshalb einfach null
zurückgegeben wird.
OnStartCommand
[return: GeneratedEnum]
public override StartCommandResult OnStartCommand(
Intent? intent,
[GeneratedEnum] StartCommandFlags flags,
int startId)
{
_cancellationTokenSource = new CancellationTokenSource();
var notification = new NotificationHelper().GetServiceStartedNotification();
if (Build.VERSION.SdkInt < BuildVersionCodes.Tiramisu)
{
StartForeground(SERVICE_RUNNING_NOTIFICATION_ID, notification);
}
else
{
StartForeground(SERVICE_RUNNING_NOTIFICATION_ID, notification, ForegroundService.TypeLocation);
}
Task.Run(() =>
{
try
{
var locationService = IPlatformApplication.Current?.Services.GetService<ILocationService>();
if (locationService is null)
{
throw new InvalidOperationException($"Could not resolve required service of type {nameof(ILocationService)}");
}
locationService.Run(_cancellationTokenSource.Token).Wait();
}
catch (System.OperationCanceledException)
{
// ignore expected exception when the task is cancelled
}
finally
{
if (_cancellationTokenSource.IsCancellationRequested)
{
WeakReferenceMessenger.Default.Send(new StopForegroundServiceRequestedMessage());
}
}
}, _cancellationTokenSource.Token);
return StartCommandResult.Sticky;
}
In OnStartCommand
wird eine Notification erzeugt (hierzu wird der ebenfalls selbst erstellte NotificationHelper
verwendet), die dem Benutzer dauerhaft angezeigt wird, so lange der ForegroundService
läuft. Anschließend wird der ForegroundService
gestartet, was zur Anzeige der erwähnten Benachrichtigung führt. Nun wird die registrierte Instanz des zuvor erstellten und im DI-Container registrierten LocationService
ermittelt und die Positionsermittlung gestartet.
OnDestroy
public override void OnDestroy()
{
if (_cancellationTokenSource is not null)
{
_cancellationTokenSource.Token.ThrowIfCancellationRequested();
_cancellationTokenSource.Cancel();
}
base.OnDestroy();
}
OnDestroy
schließlich wird benötigt, um den LocationService
zu stoppen, indem auf der klasseninternen CancellationTokenSource
deren Token
der Run
-Methode des LocationService
übergeben wurde, Cancel
aufgerufen wird.
MainActivity
In der Klasse MainActivity
, ebenfalls zu finden in /Platforms/Android
muss die Methode OnCreate
überschrieben werden. Zusätzlich werden die beiden Methoden SetServiceMethods
und IsServiceRunning
hinzugefügt.
OnCreate
protected override void OnCreate(Bundle? savedInstanceState)
{
base.OnCreate(savedInstanceState);
Platform.Init(this, savedInstanceState);
_serviceIntent = new Intent(this, typeof(LocationForegroundService));
SetServiceMethods();
if (Build.VERSION.SdkInt >= BuildVersionCodes.M && !Android.Provider.Settings.CanDrawOverlays(this))
{
var intent = new Intent(Android.Provider.Settings.ActionManageOverlayPermission);
intent.SetFlags(ActivityFlags.NewTask);
StartActivity(intent);
}
}
Hier werden das Starten und Stoppen des ForegroundService
vorbereitet. Der klasseninterne Intent
wird instanziiert und anschließend die Methode SetServiceMethods
aufgerufen.
Der Inhalt der if
-Abfrage ist für ältere Android-Versionen bzw. Systeme, auf denen keine Overlays gezeichnet werden können. Dies ist außerhalb des Scopes dieser Kurzanleitung.
SetServiceMethods
private void SetServiceMethods()
{
WeakReferenceMessenger.Default.Register<StartForegroundServiceRequestedMessage>(this, (_, _) =>
{
if (!IsServiceRunning(typeof(LocationForegroundService)))
{
if (Build.VERSION.SdkInt >= BuildVersionCodes.O)
{
StartForegroundService(_serviceIntent);
}
else
{
StartService(_serviceIntent);
}
}
});
WeakReferenceMessenger.Default.Register<StopForegroundServiceRequestedMessage>(this, (_, _) =>
{
if (IsServiceRunning(typeof(LocationForegroundService)))
{
StopService(_serviceIntent);
}
});
}
Hier wird erneut der WeakReferenceMessenger
eingesetzt, diesmal, um Benachrichtigungen zum Starten und Stoppen des ForegroundService
zu erhalten. Geht eine entsprechende Meldung ein, wird er gestartet bzw. gestoppt, allerdings jeweils nur dann, wenn die IsServiceRunning
-Prüfung zuvor nicht ergibt, dass er bereits im gewünschten Zustand ist.
IsServiceRunning
private bool IsServiceRunning(Type serviceType)
{
if (GetSystemService(ActivityService) is not ActivityManager activityManager)
{
return false;
}
// GetRunningServices is marked as deprecated but still works. May break in the future. Be aware!
foreach (var service in activityManager.GetRunningServices(int.MaxValue)!)
{
if (service.Service!.ClassName == Java.Lang.Class.FromType(serviceType).CanonicalName)
{
return true;
}
}
return false;
}
Hier wird geprüft, ob der ForegroundService
aktuell läuft. Dazu wird die Methode GetRunningServices
verwendet. Diese ist zwar als obsolete
markiert, funktioniert allerdings noch. Zudem gibt es zur Zeit noch keinen alternativen Weg, um die erforderliche Prüfung zu machen.
AndroidManifest.xml
Unter /Platforms/Android
findet sich die AndroidManifest.xml
. Hier sind einige Einstellungen vorzunehmen.
<application android:allowBackup="true" android:icon="@mipmap/appicon" android:supportsRtl="true">
<service android:name="de.blecon.bglocationtracking.LocationForegroundService"
android:foregroundServiceType="location"
android:exported="false" />
</application>
Damit der ForegroundService
korrekt angesprochen wird, muss er im Manifest definiert werden. Hierzu wird im application
-Knoten ein service
-Knoten hinzugefügt. Sein Name muss dem entsprechen, der auch im Service
-Attribut des LocationForegroundService
angegeben wurde. Also foregroundServiceType
ist in unserem Fall location
zu wählen.
permissions
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
Um auf die Positionsdaten zugreifen zu können, sind einige Permissions nötig, die ebenfalls im Manifest hinterlegt werden müssen. Die FOREGROUND_SERVICE
und FOREGROUND_SERVICE_LOCATION
-Permissions werden benötigt, um den ForegroundService
starten zu können. Die SYSTEM_ALERT_WINDOW
-Permission schließlich wird benötigt, damit unsere App im Display over other apps
-Dialog von Android erscheint und ihr das entsprechende Recht gegeben werden kann. Ohne dieses wird der ForegroundService
nicht laufen.
PermissionService
Damit Positionsdaten ermittelt werden dürfen, muss der Benutzer dies explizit erlauben. Um die benötigten Berechtigungen anzufordern, wird der PermissionService
verwendet.
public async Task<PermissionStatus> CheckAndRequestLocationAlwaysPermission(bool showRationale = false)
{
var status = await Permissions.CheckStatusAsync<Permissions.LocationAlways>();
if (status == PermissionStatus.Granted)
{
return status;
}
await Application.Current.MainPage.DisplayAlert(
"Standortberechtigung",
"Diese App benötigt während der Laufzeit dauerhaften Zugriff auf Ihren Standort.",
"OK");
return await Permissions.RequestAsync<Permissions.LocationAlways>();
}
Da für die Hintergrund-Abfrage der Positionsdaten die höchstmögliche Berechtigung aus dem Bereich Location
nötig ist, ist es ausreichend, diese anzufragen. Das passiert in der Methode CheckAndRequestLocationAlwaysPermission
.
Konsumieren der Positionsdaten
Da die ermittelten Positionsdaten vom LocationService
über den WeakReferenceMessenger
als LocationMessage
versendet werden, können die Positionsdaten an jeder beliebigen Stelle in der Anwendung abgefangen werden, indem ein entsprechender Handler eingerichtet wird. Im Demo-Projekt findet sich z.B. im MainViewModel
die folgende Implementierung.
private void HandleLocationMessage(object recipient, LocationMessage locationMessage)
{
var locationDetails = $"[{DateTime.Now:HH:mm:ss}] Latitude: {locationMessage.Latitude} - Longitude: {locationMessage.Longitude}";
LocationChanges.Add(locationDetails);
Debug.WriteLine(locationDetails);
}
Diese sorgt dafür, dass bei jedem Eingang einer LocationMessage
eine ObservableCollection<string>
, die eine CollectionView
speist, um die neuen Daten ergänzt wird. Zusätzlich erfolgt eine Ausgabe via Debug.WriteLine
, um auch während die App minimiert oder geschlossen ist, im Debug-Output von Visual Studio sehen zu können, dass der ForegroundService
noch läuft.
WARNUNG
Durch den ForegroundService
werden die Positionsdaten auch dann abgefragt, wenn die App geschlossen wurde. Ist das explizit nicht erwünscht, sollte vor Beenden der Anwendung eine StopForegroundServiceRequestedMessage
gesendet werden, die von der MainActivity
aufgegriffen wird und den Service beendet.
Hinterlasse einen Kommentar
An der Diskussion beteiligen?Hinterlasse uns deinen Kommentar!