2

With the release of Oreo/Android 8.0, I'm wanting to replace my GPS foreground service with a scheduled job.

I've looked at the Xamarin Example for Job Scheduler, and honestly I find it quite confusing.

Here is all I want to do:

  1. User taps a button
  2. A job is created and scheduled.
  3. The job is called instantly, and every 30 seconds afterwards until the user taps the button again

The job will be the replacement for my GPS service (which implementes ILocationListener).

Could anyone help clear up what I need to do to achieve this?

I have tried .SetPeriodic(30000), which doesn't seem to work on my test device running Oreo.

The example uses .SetMinimumLatency() and .SetOverrideDeadline() to set some sort of schedule range. Can I set both of these to 30000 (30 seconds) to achieve what I need? There also seems to be a 15 minute minimum time for scheduled jobs.

The example uses handlers and passes parameters, etc. Is this required for a Job Scheduler? There's a lot going on for a basic example project, and I'm just getting confused about the requirements.

Thanks in advance, hoping someone can help me clear this up a bit.

Cheers.

EDIT - EXAMPLE CODE

private void StartTracking()
{
    Log.Debug(TAG, "Starting Tracking");
    var component = new ComponentName(Context, Java.Lang.Class.FromType(typeof(LocationJobService)));

    //// This will run a service as normal on pre-Oreo targets, and use the Job Scheduler on Oreo+.
    //// Unfortunately we can't use it as there is no way to cancel it.
    //var locationJobIntent = new Intent(Context, typeof(LocationJobIntentService));
    //JobIntentService.EnqueueWork(Context, component, LocationJobIntentService.JobId, locationJobIntent);

    var deadline = 15 * 1000;
    var retryTime = 60 * 1000;

    var builder = new JobInfo.Builder(LocationJobService.JobId, component)
                                .SetMinimumLatency(0)
                                .SetOverrideDeadline(deadline)
                                .SetBackoffCriteria(retryTime, BackoffPolicy.Linear)
                                .SetRequiredNetworkType(NetworkType.Any);

    Log.Debug(TAG, "Scheduling LocationJobService...");

    var result = _jobScheduler.Schedule(builder.Build());

    if (result != JobScheduler.ResultSuccess)
    {
        Log.Warn(TAG, "Job Scheduler failed to schedule job!");
    }
}

And likewise, this is hit when a the user taps the button again to stop the location updates

private void StopTracking()
{
    Log.Debug(TAG, "Stopping Tracking");

    _jobScheduler.CancelAll();
}

And here's my Job Service

    [Service(Name = "my.assembly.name.etc.LocationJobIntentService", Permission = "android.permission.BIND_JOB_SERVICE")]
    public class LocationJobService : JobService, ILocationListener
    {
        public const int JobId = 69;

        private const long LOCATION_UPDATE_INTERVAL = 5 * 1000;
        private const string NOTIFICATION_PRIMARY_CHANNEL = "default";
        private const int NOTIFICATION_SERVICE_ID = 261088;
        private static readonly string TAG = typeof(LocationJobService).FullName;

        private LocationManager _locationManager;
        private string _locationProvider;

        #region Base Overrides

        public override void OnCreate()
        {
            base.OnCreate();

            Log.Debug(TAG, "OnCreate called.");

            _locationManager = (LocationManager)GetSystemService(LocationService);

            //// TODO: Start the Foreground Service, and display the required notification.
            //var intent = new Intent(this, typeof(LocationJobService));

            //if (AndroidTargetHelper.IsOreoOrLater())
            //{
            //    StartForegroundService(intent);
            //}
            //else
            //{
            //    StartService(intent);
            //}

            //CreateForegroundNotification("It started! yay!");
        }

        /// <summary>Called to indicate that the job has begun executing.</summary>
        /// <remarks>This method executes on the main method. Ensure there is no blocking.</remarks>
        public override bool OnStartJob(JobParameters jobParameters)
        {
            Log.Debug(TAG, "OnStartJob called.");

            Task.Run(() =>
            {
                Log.Debug(TAG, "Starting location updates...");

                StartLocationUpdates();

                Log.Debug(TAG, "Location updates started. Stopping job.");

                JobFinished(jobParameters, true);
            });

            // TODO: We need a way to cancel the SERVICE (eg. Foreground notifications, etc) if required. This needs to happen on both logging out, and when the user disables tracking. 
            // Perhaps the notifications need to happen on the Fragment, as opposed to here in the job service?

            // Will this job be doing background work?
            return true;
        }

        /// <summary>This method is called if the system has determined that you must stop execution of your job even before you've had a chance to call JobFinished().</summary>
        public override bool OnStopJob(JobParameters @params)
        {
            // The OS has determined that the job must stop, before JobFinished() has been called.
            // TODO: Here, we want to restart the Foreground service to continue it's lifespan, but ONLY if the user didn't stop tracking.
            Log.Debug(TAG, "OnStopJob called.");

            // Reschedule the job?
            return false;
        }

        public override void OnDestroy()
        {
            base.OnDestroy();

            // TODO: StopForeground(true);

            Log.Debug(TAG, "OnDestroy called.");
        }

        #endregion

        #region ILocationListener Members

        public void OnLocationChanged(Location location)
        {
            Log.Debug(TAG, String.Format("Location changed to '{0}, {1}'", location.Latitude, location.Longitude));

            StopLocationUpdates();

            // TODO: Do we finish the job here? Or right after we ASK for the location updates? (job params would have to be accessible)
            //JobFinished(_jobParameters, true);

            // THIS IS WHERE THE LOCATION IS POSTED TO THE API
            await PostLocationPing(location.Latitude, location.Longitude);
        }

        public void OnProviderDisabled(string provider)
        {
            Log.Debug(TAG, String.Format("Provider '{0}' disabled by user.", provider));

            // TODO: Get new provider via StartLocationupdates()?
        }

        public void OnProviderEnabled(string provider)
        {
            Log.Debug(TAG, String.Format("Provider '{0}' enabled by user.", provider));

            // TODO: Get new provider via StartLocationupdates()?
        }

        public void OnStatusChanged(string provider, [GeneratedEnum] Availability status, Bundle extras)
        {
            Log.Debug(TAG, String.Format("Provider '{0}' availability has changed to '{1}'.", provider, status.ToString()));

            // TODO: Get new provider via StartLocationupdates()?
        }
        #endregion

        private async Task<string> PostLocationPing(double latitude, double longitude, bool isFinal = false)
        {
            var client = new NetworkClient();
            var locationPing = new LocationPingDTO()
            {
                TimestampUtc = DateTime.UtcNow,
                Latitude = latitude,
                Longitude = longitude,
            };

            return await client.PostLocationAndGetSiteName(locationPing);
        }

        #region Helper Methods

        private void StartLocationUpdates()
        {
            // TODO: Specify the criteria - in our case we want accurate enough, without burning through the battery
            var criteria = new Criteria
            {
                Accuracy = Accuracy.Fine,
                SpeedRequired = false,
                AltitudeRequired = false,
                BearingRequired = false,
                CostAllowed = false,
                HorizontalAccuracy = Accuracy.High
            };

            // Get provider (GPS, Network, etc)
            _locationProvider = _locationManager.GetBestProvider(criteria, true);

            // Start asking for locations, at a specified time interval (eg. 5 seconds to get an accurate reading)
            // We don't use RequestSingleUpdate because the first location accuracy is pretty shitty.
            _locationManager.RequestLocationUpdates(_locationProvider, LOCATION_UPDATE_INTERVAL, 0.0f, this);
        }

        private void StopLocationUpdates()
        {
            Log.Debug(TAG, "StopLocationUpdates called.");

            _locationManager.RemoveUpdates(this);
        }

        #endregion
    }
}
  • Xamarin blog article covers it: https://blog.xamarin.com/replacing-services-jobs-android-oreo-8-0/ – SushiHangover Mar 04 '18 at 21:33
  • I appreciate the link Sushi, but I have already gone through that. Job Scheduling doesn't seem to be suitable for jobs running at intervals less than 15 minutes though? –  Mar 04 '18 at 21:43
  • That is correct, if you look as `JobInfo.MinPeriodMillis` on an Oreo device, you will find it to be 900,000ms (at least on an ASOP/Android One based device). Jobs are meant to be grouped by the OS so that when the OS wakes up a job, then all jobs that need the same requirements are also run and were never designed to run at such a fast cycle (30secs) since at that rate a (foreground) Service is what would be needed as the app really would never sleep/be idle. – SushiHangover Mar 04 '18 at 21:50
  • Thanks, that makes more sense. The problem for me is this app is a "set and forget" solution. If I use a Foreground service, it needs to be running until the user disables it. Android Oreo moves it to the background (even if started it using StartForeground), where the GPS updates completely stop. It looks like my only options is to: Have a short running Foreground service (eg. User has to continue enabling the GPS updates), or only check for GPS updates every 15 minutes. Is that correct? –  Mar 04 '18 at 21:57
  • In terms of Location updates, hopefully you are using the Google Fused location provider vs Location (in the Android framework), at that point you can defined things like `setSmallestDisplacement` so you are notified when the users locations changes by X meters vs. a preset time interval and the fused provider is battery/cpu aware. – SushiHangover Mar 04 '18 at 22:01
  • When you say Oreo moves your service to be background, what actually is the OS doing? i.e. `logcat` would show the OS doing something to your app process. (Personally I have not ever seen Oreo "kill" my foregrounded service unless I had a memory leak, the OS need "my" memory for a foregrounded **app**, or my service was on a constant crash/restart cycle.) – SushiHangover Mar 04 '18 at 22:01
  • No, I'm using the standard LocationManager. Should I be using Google Fused? It seemed like more overhead. For my situation it's completely time based as the app has to send a ping back to the API. I can't see the logcat unfortunately as it only seems to happen when field testing, and at random intervals (I guess when listening to music, and performing memory intensive tasks, etc.). The OS removes the Notification generated by the Foreground Service, and displays a message (paraphrased) "App is now running in the background", which is when all GPS pings to the API completely stop. –  Mar 04 '18 at 22:14
  • Also thank you for your help so far @SushiHangover! –  Mar 04 '18 at 22:24
  • Ah, the famous "XXX is running in the background". You could use a scheduled Job that checks if your service is still running and if not restart it (StartForegroundService can be called again even if the app itself is not in the foreground), of course doing this you might have 15m "gaps" in "pings". As far as the "API ping", you could use a remote notification to force the OS to awaken the app if your API server has not received a ping from a device in X minutes. – SushiHangover Mar 04 '18 at 22:42
  • This is how I did it for a dedicated fleet tracker app using the Fuse provider, if the truck did not move for 5 mins, we send a remote notification that request its position, ie. is the same location as the last ping (i.e. the driver is sleeping, eating, etc.). Once the truck is moving, the Fuse provider notifies the app on every X meters of travel based up a sampling of the speed of the truck (calc'd from X prior locations), so less updates at highways speeds and more when slower (inner-city) and thus the delivery schedule is updated, client is notified when the truck is X minutes away. – SushiHangover Mar 04 '18 at 22:49
  • Thanks @SushiHangover, checking to restart the service every 15 minutes would work for my solution. Luckily not many people are on 8.0 just yet! I'll continue looking into options for the long term. –  Mar 04 '18 at 23:52
  • Hey @SushiHangover, still having a lot of trouble with Job Scheduling. Do you know if it's suitable for actually performing the location updates, or would I be better off using it ONLY for checking if the service is running, and then calling StartForegroundService again? (while having a normal Service running for performing the location updates) Also, I like your Remote Notification idea. Is that Firebase Push Notifications? Thanks. –  Mar 09 '18 at 04:41
  • A `JobService` is not really designed to run continuously, in the `OnStartJob` you could spin off a thread (Task.Run will work) and grab the current location and report it to your remote API and you would call `JobFinished` to inform the OS that you are done otherwise the OS can/will kill your `JobService` that that jobs requirements changes. Personally I would start the foreground service and then call JobFinished, read the onStartJob & jobFinished sections @ https://developer.android.com/reference/android/app/job/JobService.html – SushiHangover Mar 09 '18 at 19:25
  • Thanks @SushiHangover, I appreciate your help so far. I have updated my post with example code - any chance you could take a look at let me know if I'm on the right track, or doing anything silly? Cheers. –  Mar 11 '18 at 23:24
  • @SushiHangover The blog article you posted no longer exists. – Kris Craig Aug 15 '19 at 07:21
  • 1
    @KrisCraig Are you talking about this one? https://devblogs.microsoft.com/xamarin/replacing-services-jobs-android-oreo-8-0/ (they changed to an MSFT dev blog, just use the search ...) – SushiHangover Aug 15 '19 at 08:10

1 Answers1

0

For those still trying to achieve this, there are many issues regarding continuous tracking. One article you should read regarding this is https://developer.android.com/training/location/background

Thys
  • 149
  • 1
  • 5