CMMotionActivityManager, NSOperationQueues and Async Behaviour

I’ve just deleted a couple of articles from the blog for the first time in five years because, having spent quite a few hours going over the example code I’d posted, I realised that it was rubbish.

There is a characteristic of some of the CMMotionActivityManager methods which gets you into some serious threading detail: they return results to an NSOperationsQueue asynchronously. What I was doing with the simpler of the two, queryStepCountStartingFrom, was to wrap a looping call to the inner results queue in an outer queue. So: I have an ‘inner’ results queue, which is being called as a parameter to the queryStepCountStartingFrom method, effectively adding results which return asynchronously. I then force this to be a prerequisite by making the entire loop the first add operation on an outer queue. When the loop is done I make the call to the Core Plot charting methods, because all of the async data will have returned before the charting operation is added. Right?

Wrong.

It’s pure chance that the async operations return in time. The queue will merrily accept new operations regardless of whether the previous tasks have completed.

These problems emerged when I tried to do the same thing with the slightly more complex queryActivityStartingFromDate method, which returns a richer data set, and so the async calls take significantly longer to return. I’ve been round the houses on this: the nested queues approach didn’t work, nor did a GCD async dispatch queue wrapped around the loop. I also tried messing around with adding dependencies between nested operations. No joy.

The fundamental problem [so far as I can see] is that the first port of call for identifying the end of some queue activity, waitUntilAllOperationsAreFinished, is a synchronous animal and therefore has no purview on the results coming back asynchronously on the queue.

What I really want to avoid is what I’ve done elsewhere when I’ve understood the problem less clearly, which is to mess around with timers. This is a pretty fragile approach.

Googling for this has led me to the conclusion that the ‘right’ way – i.e., to reliably detect the async results coming back – is to do something pretty low level with either threading or GCD.

As a compromise, what I’ve come up with is to set an observer on the queue, which I first serialise [maxConcurrentOperationCount = 1]. In the observeValueForKeyPath method, I can test for the queue being empty, and increment a counter. When the counter hits the threshold for the number of day’s worth of data / loops round the CMMotionActivityManager methods for [i.e., 7], I am done. It ain’t pretty but it works.
Today.

FWIW, here’s the code:

- (void) getDailyActivityTotals
{
    NSLog(@"Entering getDailyTotals");
    // activityQueue is declared as a property as we need to refer to it in the observer method:
    activityQueue.maxConcurrentOperationCount = 1;
    activityQueue.name =@ "activityQueue";
    [activityQueue addObserver:self forKeyPath:@"operations" options:0 context:NULL];
    
    NSLog(@"starting to loop in calActivityOpn");
    for (int dayOffset = 6; dayOffset >=0; dayOffset--)
    {
        NSMutableArray *dailyResultsTmpArray = [[NSMutableArray alloc] init];
        // startAndEndDates is a method which uses NSCalendar and NSDateComponents to
        // create some NSDate objects based on an offset: 24 hour periods - start and end dates:
        dailyResultsTmpArray = [self startAndEndDates:dayOffset];
        [motionActivity queryActivityStartingFromDate:dailyResultsTmpArray[1]
                                               toDate:dailyResultsTmpArray[0]
                                              toQueue:activityQueue
                                          withHandler:^(NSArray *activities, NSError *error)
        {
            NSInteger walkingCount = 0;
            NSInteger runningCount = 0;
            NSInteger carCount = 0;
            NSInteger staticCount = 0;
            NSInteger actionItemCount = 0;
            // This returns a lot of data, so we are just counting the number of events
            // per activity category:
            for (CMMotionActivity *actItem in activities)
            {
                if (actItem.walking == 1)
                    walkingCount++;
                if (actItem.running == 1)
                    runningCount++;
                if (actItem.automotive == 1)
                    carCount++;
                if (actItem.stationary == 1)
                    staticCount++;
                actionItemCount++;
            }
            //NSLog(@"walking: %i", walkingCount);
            DailyActivityObject *todaysResults = [[DailyActivityObject alloc] initWithNumWalkCount:walkingCount numRun:runningCount nummAuto:carCount numStatic:staticCount numAction:actionItemCount forDay:dailyResultsTmpArray[0]];
            [dailyResults addObject:todaysResults];
            NSLog(@"adding dailyResults");
                
        }];

    }
}

and the KVO method:

- (void) observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object
                         change:(NSDictionary *)change context:(void *)context
{
    if (object == activityQueue && [keyPath isEqualToString:@"operations"]) 
    {
        // if the queue is empty - remember we have serialised:
        if (activityQueue.operationCount == 0)
        {
            oberserverCount ++;
            NSLog(@"queue has completed %i iterations", oberserverCount);
            if (oberserverCount == 7)
            {
                // we have all the charting data so....
                [activityQueue addOperationWithBlock:^{
                    [[NSOperationQueue mainQueue] addOperationWithBlock:^{
                        [self plotCompoundChart:dailyResults];
                    }];
                }];
            }
        }
    }
    else {
        [super observeValueForKeyPath:keyPath ofObject:object
                               change:change context:context];
    }
}

GeoFencer – Update

I’ve fixed a bug that was having a fundamental impact on reliability. I mentioned this in my initial post about the release, and did some more digging. When the device is under resource pressure, the operating system can intervene and kill off apps. My understanding of the registering for location data isn’t a true background mode, and this is quite deliberate in order to minimise resource consumption. Assuming that the app is suspended, the operating system will make a decision on which apps to send the location data to via the appropriate delegate based on the registered mode in the plist file. At this point, the app will launch in the background, and you have a short amount of time to process. [I think it’s at this point that the OS can tidy up based on resource constraints.] Before hitting the location oriented delegate, didFinishLaunchingWithOptions is called, at which point you can check for a prior termination through:

if ([launchOptions objectForKey:UIApplicationLaunchOptionsLocationKey])

at which point I restart the location and region monitoring, based on a recommendation here. This does raise the question as to why you need to restart the location monitoring, given that the app is identified as wanting to receive location data in the plist. Having to register in code introduces volatility, which has to then ride through the various running  mode changes. Perhaps it’s to have convergent functionality [and readability] with doing the same in the foreground. Either way it works.

One other change in the AppDelegate is that I no longer use the same property based UILocalNotification. If more than one delegate is hit on restart, trying to use the same notification will cause an error.

I’ve also noticed a ‘feature’ in the GeoFence creation view controller. I thought it would be more elegant to automatically save the location data based on the pin drop event rather than a hard-wired button, as the map is already cluttered. One downside of this approach is that changing the radius of the region after the pin is down updates the screen but isn’t saved. It would be a relatively simple change to make but I’m toying with the idea of a podcast app [using some of the new background features].

GeoFencer – Open Source App

I have just put the source code for an app that I’ve been working on for a couple of months  up on Github. The idea is similar to existing functionality in the iPhone, where you can schedule location based reminders.

My app allows you create the geofence based on mapping functionality, set a  radius [which I’ll back to later] and then an action. Currently there are three supported actions: local notification, email, and Wake on LAN.

The app runs in the background but using a power saving mechanism that Apple has implemented. Rather than running all of the time, the app is registered to receive ‘significant change’ events based on cell tower locations. As this isn’t very regular, and depends on cell tower density in the area that you happen to be in, I’ve set the geofence regions to be anything up to 500 metres in diameter.

The part that I had the most sport with was the Wake on LAN functionality, which I’ve written about before. One thing that initially stumped me was why an IP was sometimes in the arp table, and sometimes not. The penny dropped that it will be in there if the device has tried to make a connection previously. So in order to guarantee this, I send a dummy UDP packet – 100 times, just to be sure.

This introduces an interesting problem: the regions are – well, on paper they are circular, but in reality, due to the significant change implementation, are actually going to have ragged boundaries. The operating system will [hopefully] recognise that you have entered the region, at which point you will probably not be on your home network. The use case for this was to be able to turn on my NAS device when I got in the front door. So if the action is a Wake on LAN event, I request 10 minutes running time from the operating system using beginBackgroundTaskWithExpirationHandler, and then every 30 seconds try to identify if we are on the home network. I repeat this 19 times [rather than 20], because the delay I use didn’t seem to be very precise [sleepForTimeInterval]. If the home network is detected, I then send a flood of WoL packets at a previously configured IP address.

While the construction of the WoL ‘magic packet’ is all fairly standard stuff, unfortunately the only way I could find of converting the IP into the MAC address required the use of private APIs. This, and the fact that the app can send emails in the background, meant that the final result was never going to be heading towards the App Store.

I use my phone very heavily, and it’s also getting on for a couple of years old now, so it’s hard to make a definitive call on what the battery life impact is. I’d estimate that it’s probably about a 5% penalty, something of that order of magnitude.

Reliability is another matter. The delegates to trigger the significant change seem to ‘go away’ every now and then, and I haven’t gotten to the bottom of this yet. This is why one of the features I’ve implemented is to log ever significant change event – something my 45 minute train journey is quite useful for.

My working assumption is that the operating system makes decisions about whether or not it will honour the delegates, based on resource consumption at a given time.

So it’s done. It’s pretty rough around the edges [the UI is something of a tragedy], and my code probably breaks every convention that Objective C requires, but it works.