Reverse geocoding in my background agent causes my live tile not to update when processing times for rest of code called by agent is fast

0

I'm having a problem with my Windows Phone 8 weather app's background agent.

Whenever the background agent is run, a new http weather request is made when certain conditions (that are not relevant to the problem I'm having) are met. When these conditions are unmet, cached weather data is used instead.

Furthermore, if you have set your live tile's location to your "Current Location", the background agent will use reverse geocoding to determine the name of the area for the location you're currently at. This is done whether new or cached data is used i.e. every time my app's background agent is run.

The problem I'm having is that whenever cached data is used, the live tile is not updating. But it doesn't appear to cause an exception to occur because the app's background agent never gets blocked, even if there's been more than two times where the live tile fails to update.

This is the relevant excerpt from the background agent's view model's "public async Task getWeatherForTileLocation()" method that's called from the scheduled agent:

Scheduled agent excerpt:

protected async override void OnInvoke(ScheduledTask task)
{
    LiveTileViewModel viewModel = new LiveTileViewModel();
    await viewModel.getWeatherForTileLocation();

    // Etc.
}

getWeatherForTileLocation() excerpt:

// If the default location is 'Current Location', then update its coordinates.
if ((int)IsolatedStorageSettings.ApplicationSettings["LocationDefaultId"] == 1)
{
    try
    {
        // Get new coordinates for current location.
        await this.setCoordinates();;
    }
    catch (Exception e)
    {

    }
}

// Depending on the time now, since last update (and many other factors),
// must decide whether to use cached data or fresh data
if (this.useCachedData(timeNow, timeLastUpdated))
{
    this.ExtractCachedData(); // This method works absolutely fine, trust me. But the live tile never updates when it's run outside debugging.
                              // Not because of what it does, but because of how fast it executes.
}
else
{
    // a httpClient.GetAsync() call is made here that also works fine.
}

The setCoordinates method, as well the reverse geocoding related methods that are called from it:

public async Task<string> setCoordinates()
{
    // Need to initialise the tracking mechanism. 
    Geolocator geolocator = new Geolocator();

    // Location services are off.
    // Get out - don't do anything.
    if (geolocator.LocationStatus == PositionStatus.Disabled)
    {
        return "gps off";
    }
    // Location services are on.
    // Proceed with obtaining longitude + latitude.
    else
    {
        // Setup the desired accuracy in meters for data returned from the location service.
        geolocator.DesiredAccuracyInMeters = 50;

        try
        {
            // Taken from: http://bernhardelbl.wordpress.com/2013/11/26/geolocator-getgeopositionasync-with-correct-timeout/
            // Because sometimes GetGeopositionAsync does not return. So you need to add a timeout procedure by your self.

            // get the async task
            var asyncResult = geolocator.GetGeopositionAsync();
            var task = asyncResult.AsTask();

            // add a race condition - task vs timeout task
            var readyTask = await Task.WhenAny(task, Task.Delay(10000));
            if (readyTask != task) // timeout wins
            {
                return "error";
            }

            // position found within timeout
            Geoposition geoposition = await task;

            // Retrieve latitude and longitude.
            this._currentLocationLatitude = Convert.ToDouble(geoposition.Coordinate.Latitude.ToString("0.0000000000000"));
            this._currentLocationLongitude = Convert.ToDouble(geoposition.Coordinate.Longitude.ToString("0.0000000000000"));

            // Reverse geocoding to get your current location's name.
            Deployment.Current.Dispatcher.BeginInvoke(() =>
            {
                this.setCurrentLocationName();
            });

            return "success";
        }
        // If there's an error, may be because the ID_CAP_LOCATION in the app manifest wasn't include. 
        // Alternatively, may be because the user hasn't turned on the Location Services.
        catch (Exception ex)
        {
            if ((uint)ex.HResult == 0x80004004)
            {
                return "gps off";
            }
            else
            {
                // Something else happened during the acquisition of the location.
                // Return generic error message.
                return "error";
            }
        }
    }
}

/**
 * Gets the name of the current location through reverse geocoding.
 **/
public void setCurrentLocationName()
{
    // Must perform reverse geocoding i.e. get location from latitude/longitude.
    ReverseGeocodeQuery query = new ReverseGeocodeQuery()
    {
        GeoCoordinate = new GeoCoordinate(this._currentLocationLatitude, this._currentLocationLongitude)
    };
    query.QueryCompleted += query_QueryCompleted;
    query.QueryAsync();
}

/**
 * Event called when the reverse geocode call returns a location result.
 **/
void query_QueryCompleted(object sender, QueryCompletedEventArgs<IList<MapLocation>> e)
{
    foreach (var item in e.Result)
    {
        if (!item.Information.Address.District.Equals(""))
            this._currentLocation = item.Information.Address.District;
        else
            this._currentLocation = item.Information.Address.City;

        try
        {
            IsolatedStorageSettings.ApplicationSettings["LiveTileLocation"] = this._currentLocation;
            IsolatedStorageSettings.ApplicationSettings.Save();
            break;
        }
        catch (Exception ee)
        {
            //Console.WriteLine(ee);
        }
    }
}

I've debugged the code many times, and have found no problems when I have. The http request when called is good, cached data extraction is good, reverse geocoding does always return a location (eventually).

But I did notice that when I'm using cached data, the name of the current location is retrieved AFTER the scheduled task has created the updated live tile but before the scheduled task has finished.

That is, the name of the location is retrieved after this code in the scheduled agent is run:

extendedData.WideVisualElement = new LiveTileWideFront_Alternative()
{
    Icon = viewModel.Location.Hourly.Data[0].Icon,
    Temperature = viewModel.Location.Hourly.Data[0].Temperature,
    Time = viewModel.Location.Hourly.Data[0].TimeFull.ToUpper(),
    Summary = viewModel.Location.Hourly.Data[0].Summary + ". Feels like " + viewModel.Location.Hourly.Data[0].ApparentTemperature + ".",
    Location = IsolatedStorageSettings.ApplicationSettings["LiveTileLocation"].ToString().ToUpper(),
    PrecipProbability = viewModel.Location.Hourly.Data[0].PrecipProbabilityInt
};

But before:

foreach (ShellTile tile in ShellTile.ActiveTiles)
{
    LiveTileHelper.UpdateTile(tile, extendedData);
    break;
}

NotifyComplete();

Obviously due to memory constraints I can't create an updated visual element at this point.

For comparison, when I'm not using cached data, the reverse geocoding query always manages to return a location before the http request code has finished.

So as the view model's getWeatherForTileLocation() method is using "await" in the scheduled agent, I decided to make sure that the method doesn't return anything until the current location's name has been retrieved. I added a simple while loop to the method's footer that would only terminate after the _currentLocation field has received a value i.e. the reverse geocoding has completed:

// Keep looping until the reverse geocoding has given your current location a name.
while( this._currentLocation == null )
{

}

// You can exit the method now, as you can create an updated live tile with your current location's name now.
return true;

When I debugged, I think this loop ran around 3 million iterations (a very big number anyway). But this hack (I don't know how else to describe it) seemed to work when I'm debugging. That is, when the target of my build was my Lumia 1020, and when I created a live tile fresh from it, which calls:

ScheduledActionService.Add(periodicTask);
ScheduledActionService.LaunchForTest(periodicTaskName, TimeSpan.FromSeconds(1)); 

To ensure I don't have to wait for the first scheduled task. When I debugged this first scheduled task, everything works fine: 1) a reverse geocoding request is made, 2) cached data extracted correctly, 3) hacky while loop keeps iterating, 4) stops when the reverse geocoding has returned a location name, 5) tile gets updated successfully.

But subsequent background agent calls that do use cached data don't appear to update the tile. It's only when non-cached data is used that the live tile updates. I should remind you at this point the reverse geocoding query always manages to return a location before the http request code has finished i.e. the hacky loop iterates only once.

Any ideas on what I need to do in order to ensure that the live tile updates correctly when cached data is used (read: when the processing of data, after the reverse geocoding query is made, is much faster than a http request)? Also, is there a more elegant way to stop the getWeatherForTileLocation() from exiting than my while loop? I'm sure there is!

Sorry for the long post but wanted to be as thorough as possible!

This has been giving me sleepless nights (literally) for the last 72 hours, so your help and guidance would be most appreciated.

Many thanks.

Bardi

c#
windows-phone-8
scheduled-tasks
reverse-geocoding
background-agent
asked on Stack Overflow Jul 25, 2014 by Barrrdi • edited Jul 27, 2014 by Neil Turner

3 Answers

1

You have done a great job of providing lots of detail, but it is very disconnected so it is a litle hard to follow. I think the root of your problem is the following:

// Reverse geocoding to get your current location's name.
Deployment.Current.Dispatcher.BeginInvoke(() =>
{
    this.setCurrentLocationName();
});

You are attempting to get the location name, but your setCoordinates method will have already completed by the time the setCurrentLocationName method gets around to executing.

Now because you need to be in a UI thread to do any tile updating anyways, I would suggest just dispatching from the begining:

protected async override void OnInvoke(ScheduledTask task)
{
    Deployment.Current.Dispatcher.BeginInvoke(() =>
    {
        LiveTileViewModel viewModel = new LiveTileViewModel();
        await viewModel.getWeatherForTileLocation();
    }
}

This would remove the need to do any other dispatching in the future.

Two more things:

Generally weather data includes the name of the location you are getting data for. If this is the case, just use that data rather than doing the reverse geocode. This will save you some memory and save time.

If you do need to get the location, I might suggest pulling out a "LocationService" that can get data for you. In this class you can use a TaskCompltionSource to await the event rather than having code follow many different paths.

public class LocationService
{
    public static Task<Location> ReverseGeocode(double lat, double lon)
    {
        TaskCompletionSource<Location> completionSource = new TaskCompletionSource<Location>();
        var geocodeQuery = new ReverseGeocodeQuery();
        geocodeQuery.GeoCoordinate = new GeoCoordinate(lat, lon);

        EventHandler<QueryCompletedEventArgs<IList<MapLocation>>> query = null;
        query = (sender, args) =>
            {
                geocodeQuery.QueryCompleted -= query;
                MapLocation mapLocation = args.Result.FirstOrDefault();
                var location = Location.FromMapLocation(mapLocation);
                completionSource.SetResult(location);
            };
        geocodeQuery.QueryCompleted += query;
        geocodeQuery.QueryAsync();
    }
    return completionSource.Task;
}

Using a TaskCometionSource allows you to await the method rather than using the event.

var location = await locationService.ReverseGeocode(lat, lon);

This example uses another Location class that I created do just hold things like City and State.

The key thing with background agents is to ensure that code always flows "synchronously". This doesn't mean code cannot be asynchronous, but does mean that code needs to be called one after the other. So if you have something that has events, you could continue all other code after the event.

Hope that helps!

answered on Stack Overflow Jul 26, 2014 by Shawn Kendrot • edited Jul 26, 2014 by Shawn Kendrot
1

I don't see your deferral call. When you use async you have to tell the task that you're deferring completion till later. Can't remember the method off the top of my head but it's either on the base class of your background task or on the parameter you get. The reason it probably works with cache data is that it probably isn't actually an async operation.

answered on Stack Overflow Jul 26, 2014 by dotMorten
0

I think this is sorted now! Thanks so much Shawn for the help. The setLocationName() method call is now awaited, and it looks like this now:

    public Task<string> setLocationName()
    {
        var reverseGeocode = new ReverseGeocodeQuery();
        reverseGeocode.GeoCoordinate = new System.Device.Location.GeoCoordinate(this._currentLocationLatitude, this._currentLocationLongitude );

        var tcs = new TaskCompletionSource<string>();
        EventHandler<QueryCompletedEventArgs<System.Collections.Generic.IList<MapLocation>>> handler = null;
        handler = (sender, args) =>
        {

                MapLocation mapLocation = args.Result.FirstOrDefault();
                string l;
                if (!mapLocation.Information.Address.District.Equals(""))
                    l = mapLocation.Information.Address.District;
                else
                    l = mapLocation.Information.Address.City;

                try
                {
                    System.DateTime t = System.DateTime.UtcNow.AddHours(1.0);
                    if (t.Minute < 10)
                        IsolatedStorageSettings.ApplicationSettings["LiveTileLocation"] = l + " " + t.Hour + ":0" + t.Minute;
                    else
                        IsolatedStorageSettings.ApplicationSettings["LiveTileLocation"] = l + " " + t.Hour + ":" + t.Minute;
                    IsolatedStorageSettings.ApplicationSettings.Save();

                    this._currentLocationName = IsolatedStorageSettings.ApplicationSettings["LiveTileLocation"].ToString();
                }
                catch (Exception ee)
                {
                    //Console.WriteLine(ee);
                }

                reverseGeocode.QueryCompleted -= handler;
                tcs.SetResult(l);
        };

        reverseGeocode.QueryCompleted += handler;
        reverseGeocode.QueryAsync();
        return tcs.Task;
    }
answered on Stack Overflow Jul 26, 2014 by Barrrdi

User contributions licensed under CC BY-SA 3.0