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

[Bug]: iOS Location Updates Not Working: Recurring Background Location Tracking Issues with Shiny.Locations #1547

Open
8 of 12 tasks
DohanResponse24 opened this issue Jan 10, 2025 · 3 comments
Labels
bug Something isn't working unverified This issue has not been verified by a maintainer

Comments

@DohanResponse24
Copy link

DohanResponse24 commented Jan 10, 2025

Component/Nuget

GPS or Geofencing (Shiny.Locations)

What operating system(s) are effected?

  • iOS (13+ supported)
  • Mac Catalyst
  • Android (8+ supported)

Version(s) of Operation Systems

Physical Device : 15.8.3
Mac : Sequoia 15.2

Hosting Model

  • MAUI
  • Native/Classic Xamarin
  • Manual

Steps To Reproduce

I have a location tracking implementation that's showing different behaviors on Android and iOS:

On Android:

  • Works as expected with minimum timer intervals
  • Properly tracks location changes
  • Functions in both background and terminated states

On iOS:

  • Only works during the first app launch
  • Functions when app is minimized (background)
  • Stops tracking when app is terminated

I attempted to solve this by implementing CLLocation for iOS background tracking, which worked for getting location updates, but I couldn't integrate it with my existing OnGpsReading event handler where the core logic resides.

Through research, I found discussions suggesting using only Shiny.NET location services (rather than platform-specific implementations) for handling all location tracking scenarios, including background and terminated states works as expected.

Also the location permission is set to always and background refresh is also added.

Thank you for taking the time to review my issue. I look forward to working together to find a solution?

Expected Behavior

On iOS:

  • Works all the time.
  • Should publish to api when iOS is in the background.

Actual Behavior

On iOS:

  • Only works during the first app launch
  • Functions when app is minimized (background)
  • Stops tracking when app is terminated

Exception or Log output

No response

Code Sample

Info.Plist

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>

	<key>BGTaskSchedulerPermittedIdentifiers</key>
    <array>
        <string>com.response24.plus.refresh</string>
    </array>

    <key>CFBundleIdentifier</key>
    <string>com.response24.plus</string>

	<key>ITSAppUsesNonExemptEncryption</key>
	<false/>
	<key>LSRequiresIPhoneOS</key>
	<true/>
	<key>UIDeviceFamily</key>
	<array>
		<integer>1</integer>
		<integer>2</integer>
	</array>
	<key>UIRequiredDeviceCapabilities</key>
	<array>
		<string>arm64</string>
	</array>
	<key>UISupportedInterfaceOrientations</key>
	<array>
		<string>UIInterfaceOrientationPortrait</string>
		<string>UIInterfaceOrientationLandscapeLeft</string>
		<string>UIInterfaceOrientationLandscapeRight</string>
	</array>
	<key>UISupportedInterfaceOrientations~ipad</key>
	<array>
		<string>UIInterfaceOrientationPortrait</string>
		<string>UIInterfaceOrientationPortraitUpsideDown</string>
		<string>UIInterfaceOrientationLandscapeLeft</string>
		<string>UIInterfaceOrientationLandscapeRight</string>
	</array>
	<key>XSAppIconAssets</key>
	<string>Assets.xcassets/appicon.appiconset</string>

	<!-- Location permissions -->
	<key>NSLocationWhenInUseUsageDescription</key>
	<string>We need access to location to provide accurate location-based services</string>
	<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
	<string>We need background location access to provide continuous location-based services</string>
	<key>NSLocationAlwaysUsageDescription</key>
	<string>We need background location access to provide continuous location-based services</string>

	<!-- Camera permissions -->
	<key>NSCameraUsageDescription</key>
	<string>We need camera access to capture photos</string>

	<!-- Contacts permissions -->
	<key>NSContactsUsageDescription</key>
	<string>We need access to contacts to provide enhanced contact-based features</string>

	<!-- Notifications -->
	<key>NSNotificationUsageDescription</key>
	<string>We need to send you notifications for important updates and alerts</string>
    <!-- Notification Permissions (if not already present) -->
    <key>NSUserNotificationUsageDescription</key>
    <string>We need to send you notifications about location updates</string>


	<!-- Network capabilities -->
	<key>UIBackgroundModes</key>
	<array>
		<string>fetch</string>
		<string>location</string>
		<string>remote-notification</string>
	</array>
	<key>BGTaskSchedulerPermittedIdentifiers</key>
	<array>
		<string>com.response24.plus.refresh</string>
	</array>

</dict>
</plist>

ShinyGpsDelegate

using Microsoft.Extensions.Logging;
using R24_Maui_App.Core.Services.MQTT;
using R24_Maui_App.Core.Services.SQLite;
using R24_Maui_App.Core.Utils.DeviceInfo;
using Shiny;
using Shiny.Locations;
using System.Globalization;
using System.Text.Json;

namespace R24_Maui_App.Core.Delegate
{
    public partial class ShinyGpsDelegate : GpsDelegate, Shiny.Locations.IGpsDelegate
    {
        private readonly ILogger<ShinyGpsDelegate> _logger;
        private readonly IMqttService _mqttService;
        private bool _isReconnecting = false;
        private const int NOTIFICATION_ID = 1001;
        private readonly ISQLiteService _sqliteService;

        public ShinyGpsDelegate(
            ILogger<ShinyGpsDelegate> logger,
            IMqttService mqttService,
            ISQLiteService sqliteService) : base(logger)
        {
            _logger = logger;
            _mqttService = mqttService;
            _sqliteService = sqliteService;
            this.MinimumDistance = Distance.FromMeters(10);
            this.MinimumTime = TimeSpan.FromSeconds(10);
            

        }

        // Add a public method to handle external readings
        public async Task HandleLocationUpdate(GpsReading reading)
        {
            await OnGpsReading(reading);
        }

        protected override async Task OnGpsReading(GpsReading reading)
        {
            try
            {
                _logger.LogInformation($"OnGpsReading : Started");

                // Create and save new location entry to SQLite
                var locationEntry = new LocationEntry
                {
                    Latitude = reading.Position.Latitude,
                    Longitude = reading.Position.Longitude,
                    Timestamp = reading.Timestamp.DateTime,
                    Accuracy = reading.PositionAccuracy,
                    Speed = reading.Speed,
                    Heading = reading.Heading,
                    IsPublished = false
                };

                await _sqliteService.SaveLocationAsync(locationEntry);
                _logger.LogInformation($"OnGpsReading : GPS reading saved to SQLite - Lat: {reading.Position.Latitude}, Lon: {reading.Position.Longitude}");

                // Handle MQTT connection and publishing
                if (!_mqttService.IsConnected && !_isReconnecting)
                {
                    _isReconnecting = true;
                    try
                    {
                        await _mqttService.Connect();
                        await _mqttService.SubscribeToTopic();
                        _logger.LogInformation("OnGpsReading : Successfully reconnected to MQTT broker");
                    }
                    catch (Exception ex)
                    {
                        _logger.LogError(ex, "OnGpsReading : Failed to reconnect to MQTT broker");
                    }
                    finally
                    {
                        _isReconnecting = false;
                    }
                }

                // If connected to MQTT, process unpublished entries
                if (_mqttService.IsConnected)
                {
                    try
                    {
                        var unpublishedLocations = await _sqliteService.GetUnpublishedLocationsAsync();
                        _logger.LogInformation($"OnGpsReading : Found {unpublishedLocations.Count} unpublished locations");

                        foreach (var location in unpublishedLocations)
                        {
                            try
                            {
                                await _mqttService.PublishAsync(
                                    location.Latitude.ToString("F7", CultureInfo.InvariantCulture),
                                    location.Longitude.ToString("F7", CultureInfo.InvariantCulture),
                                    location.Timestamp
                                );

                                // Mark as published and update SQLite
                                location.IsPublished = true;
                                await _sqliteService.UpdateLocationAsync(location);
                                _logger.LogInformation($"OnGpsReading : Successfully published and updated location ID: {location.Id}");
                            }
                            catch (Exception mqttEx)
                            {
                                _logger.LogError(mqttEx, $"OnGpsReading : Failed to publish location ID: {location.Id}. Will retry on next reading.");
                                break; // Break the loop on first failure to try again next time
                            }
                        }

                        // Clean up published entries
                        await _sqliteService.DeletePublishedLocationsAsync();
                    }
                    catch (Exception ex)
                    {
                        _logger.LogError(ex, "OnGpsReading : Error processing unpublished locations");
                    }
                }

#if ANDROID
                // Update notification if needed
                var activity = ActivityStateManager.Default.GetCurrentActivity();
                if (Application.Current?.MainPage != null && activity != null)
                {
                    MainThread.BeginInvokeOnMainThread(() =>
                    {
                        UpdateNotification(reading);
                    });
                }
#endif
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "OnGpsReading : Failed to process GPS reading");
            }
        }

#if ANDROID
        public void Configure(AndroidX.Core.App.NotificationCompat.Builder builder)
        {
            builder
                .SetOngoing(true)                    // Notification cannot be dismissed by user
                .SetAutoCancel(false)                // Notification persists after tap
                .SetContentTitle("Location Tracking Active")
                .SetContentText("Tracking your location in background")
                .SetSmallIcon(Android.Resource.Drawable.IcMenuMyLocation)
                .SetPriority((int)Android.App.NotificationPriority.Default)
                .SetCategory(AndroidX.Core.App.NotificationCompat.CategoryService)
                .SetVisibility(AndroidX.Core.App.NotificationCompat.VisibilityPrivate)
                .SetForegroundServiceBehavior(AndroidX.Core.App.NotificationCompat.ForegroundServiceImmediate)
                .SetChannelId("location_service")    // Must match channel created in your MainActivity
                .SetOnlyAlertOnce(true);            // Notify only on first creation
        }
        private void UpdateNotification(GpsReading reading)
        {
            try
            {
                var context = Platform.CurrentActivity ?? Android.App.Application.Context;
                var notificationManager = context.GetSystemService(Android.Content.Context.NotificationService) as Android.App.NotificationManager;

                var builder = new AndroidX.Core.App.NotificationCompat.Builder(context, "location_service")
                    .SetOngoing(true)
                    .SetAutoCancel(false)
                    .SetContentTitle("Location Tracking Active")
                    .SetContentText($"Lat: {reading.Position.Latitude:F4}, Lon: {reading.Position.Longitude:F4}")
                    .SetSmallIcon(Android.Resource.Drawable.IcMenuMyLocation)
                    .SetPriority((int)Android.App.NotificationPriority.Default);

                notificationManager?.Notify(NOTIFICATION_ID, builder.Build());
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Failed to update notification");
            }
        }

        private string FormatLocationMessage(GpsReading reading)
        {
            return $"Latitude: {reading.Position.Latitude:F7}\n" +
                   $"Longitude: {reading.Position.Longitude:F7}\n" +
                   $"Heading: {reading.Heading:F1}°\n" +
                   $"Accuracy: {reading.PositionAccuracy:F1}m\n" +
                   $"Speed: {reading.Speed:F1} m/s\n" +
                   $"Timestamp: {reading.Timestamp:yyyy-MM-dd HH:mm:ss}";
        }
#endif
    }
}

MauiProgram.cs

      static MauiAppBuilder RegisterLocationServices(this MauiAppBuilder builder)
        {
#if IOS
    builder.Services.AddSingleton<IGpsManager, Shiny.Locations.GpsManager>();
#elif ANDROID
    builder.Services.AddSingleton<IActivityStateManager>(ActivityStateManager.Default);
    builder.Services.AddSingleton<IGpsManager, Shiny.Locations.GpsManager>();
#endif

    builder.Services.AddGps<ShinyGpsDelegate>();

            return builder;
        }

Project


Code of Conduct

  • I have supplied a reproducible sample that is NOT FROM THE SHINY SAMPLES!
  • I am using the LATEST STABLE version from nuget (v3.3.3)
  • I am Sponsor OR My GitHub account is 30+ days old
  • I acknowledge that this is not technical support and that the maintainer(s) are not here as teachers
  • I understand that this is not a paid product, it is FREE code that I am using, and that documentation may not be perfect.
  • I acknowledge that any form of swearing or general abuse towards the maintainer(s) will result in my immediate ban.
@DohanResponse24 DohanResponse24 added bug Something isn't working unverified This issue has not been verified by a maintainer labels Jan 10, 2025
@aritchie
Copy link
Member

Only works during the first app launch

How are you starting it? What's the error/console messaging? You have to submit some sort of reproducible sample to show this! I'm working with an app right now that starts/stops GPS repeatedly and across app sessions without issue.

Should publish to api when iOS is in the background

This doesn't have anything to do with Shiny or the bg service unfortunately. It isn't like we do anything to block it. If your GPS pings are coming in slowly, the cancellation token could be called if the call is taking too long. Again... submit an actual repro so I can understand the issue.

Stops tracking when app is terminated

This is how iOS works. Shiny has to operate within the rules of the platform. Shiny will try to restart GPS automatically when the app is restarted.

@DohanResponse24
Copy link
Author

I just want to confirm — when the app is terminated, it won’t be able to trigger a location change even if the device is moved within a short period, like 10 minutes. The reason I’m asking is that this is a security app, and we need to receive constant location updates. On Android, background tracking works perfectly.

Also added you to the repo ;), thank you

@aritchie
Copy link
Member

I just want to confirm — when the app is terminated, it won’t be able to trigger a location change even if the device is moved within a short period, like 10 minutes. The reason I’m asking is that this is a security app, and we need to receive constant location updates. On Android, background tracking works perfectly.

If you want a more thorough explanation than my answer, please read the iOS documentation on corelocation. I'm just offering code to help with cross platform.

Also added you to the repo ;), thank you

Please make repos publicly available on github. Also, this isn't a repro that you've sent to me, this is looking more like a full app. I'm happy to fix a bug within the library if you can clearly show it in a small repro.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working unverified This issue has not been verified by a maintainer
Projects
None yet
Development

No branches or pull requests

2 participants