Monday, September 27, 2010

Getting the APNS device token

As alluded to in my previous post, this time I'm covering how to get the APNS device token for a given iOS client. Actually, it is pretty straightforward. First, call registerForRemoteNotificationTypes from your application's didFinishLaunchingWithOptions UIApplicationDelegate callback. You need to specify which type of notifications your application will accept. Here is an example:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
...

// Register with the Apple Push Notification Service
// so we can receive notifications from our server
// application. Upon successful registration, our
// didRegisterForRemoteNotificationsWithDeviceToken:
// delegate callback will be invoked with our unique
// device token.
UIRemoteNotificationType allowedNotifications = UIRemoteNotificationTypeAlert
| UIRemoteNotificationTypeSound
| UIRemoteNotificationTypeBadge;
[[UIApplication sharedApplication] registerForRemoteNotificationTypes:allowedNotifications];

...

return YES;
}


As mentioned in the code comments, the application will then talk to Apple's Push Notification Service in the background and, when the device's unique token has been issued, your application delegate's didRegisterForRemoteNotificationsWithDeviceToken callback is invoked. This is where you actually get the device token.
- (void)application:(UIApplication *)application
didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken
{
NSLog(@"didRegisterForRemoteNotificationsWithDeviceToken: %@", deviceToken);
// Stash the deviceToken data somewhere to send it to
// your application server.
...
}

Once your iOS client application knows its own device token, it needs to send it to your application server so that the application server can push notifications back to the client later. How you do this depends on the architecture of your client-server communication.

At this point, I should add the device tokens can change. So, I recommend repeating the above logic every time your iOS client application starts so your application server always gets the latest device token for the user's terminal.

I would be remiss to not mention error handling. It is possible that the registerForRemoteNotificationTypes call will fail. The most obvious way it could fail is if the user does not have access to the 3G or WiFi networks and, as a result, cannot communicate with Apple's Push Notification Service; for example, when the device is in Airplane Mode with the wireless signals turned off.

In this case, the didFailToRegisterForRemoteNotificationsWithError delegate callback is invoked instead of didRegisterForRemoteNotificationsWithDeviceToken. In which case, you probably want to retry the registration later when network connectivity is restored.

Monday, September 20, 2010

Using pyapns with django

There is a handy daemon for sending push notifications to iOS-based mobile clients via Apple's Push Notification Service; it is called pyapns. It is implemented in python but, since it runs as a standalone XML-RPC server process, that fact is largely irrelevant. The important facts are that:
  • It properly and fully implements the client interface to APNs, including the requirement for maintaining a persistent connection with Apple's servers rather than repeatedly setting-up and tearing-down SSL connections.

  • It includes client libraries for communicating with the pyapns daemon from python and ruby, although any language that can speak XML-RPC (including C) will work too.
The way it works is: you first start the pyapns daemon process. This process acts as a XML-RPC server handling requests from your application(s), packing them into Apple's binary APNS protocol, and sending them to Apple to deliver to the iPhone, iPad, or iPod.

In order for your applications to send a push notification request, though, they must first tell pyapns which client certificate it should use to authenticate with the APNS servers. Here is a decent guide on how to obtain a client certificate. Once you have a certificate, you have to use the pyapns client library's configure and provision APIs to tell the pyapns daemon process to use your certificate.

If you are implementing your application in django, you can accomplish the configuration and provisioning directly from your django settings.py file like so:
# Configuration for connecting to the local pyapns daemon,
# including our certificate for pushing notifications to
# mobile terminals via APNS.
PYAPNS_CONFIG = {
'HOST': 'http://localhost:7077/',
'TIMEOUT': 15,
'INITIAL': [
('MyAppName', 'path/to/cert/apns_sandbox.pem', 'sandbox'),
]
}

The pyapns python client library will automatically configure and provision itself from these settings. So, assuming you know the APNS device token of the mobile device you want to send a notification to, all you need to do to send a push notification is to call the pyapns.client.notify() function.

If only it were so easy. One complication arises in that the pyapns provisioning and configuration state is split between the client library and the pyapns daemon process. As a result, there are two scenarios to be wary of:

  1. The django application is restarted. In this case, the client library, which is part of your django application, loses its state and tries to re-configure and re-provision itself from your django settings. Luckily, since the client library will re-read the configuration and provisioning settings from settings.py and seamlessly resume communication with the pyapns daemon.

    However, as noted in the pyapns documentation, "attempts to provision the same application id multiple times are ignored." As a result, if you change pyapns configuration in the settings.py file and restart django, you need to restart the pyapns daemon too for the new settings to take effect. Otherwise, if the settings are unchanged, the client library will seamlessly resume communication with the pyapns daemon.

  2. The pyapns daemon is restarted. In this case, the client library thinks it has already configured and provisioned the daemon, but the daemon has lost this configuration due to restart. As a result, any attempt to send a push notification will fail as the daemon does not know how to establish the connection with Apple's Push Notification service.

As I mentioned above, the first scenario isn't a big deal. If you have to restart your web application or the web server for some reason, the connection between the pyapns client library and the daemon process will automatically resume right where it left off. In the rare case that you changed the pyapns settings in your django settings.py file, you need to restart both the django application and the pyapns daemon process for the new settings to take effect.

The latter scenario, though, is a bigger problem because it is impossible to detect until it is too late: that is, it doesn't manifest itself until you try to send a push notification and fail. Luckily, however, we can catch the failure condition and resolve the problem automatically. Specifically, if the pyapns client library fails to send a push notification to the daemon process due to the daemon process not being configured or provisioned, we can force the client library to re-configure and re-provision and retry.

So here you go, a wrapper around the pyapns client library to automatically recover when the backend pyapns daemon has been restarted:
"""
Wrappers for the pyapns client to simplify sending APNS
notifications, including support for re-configuring the
pyapns daemon after a restart.
"""

import pyapns.client
import time
import logging

log = logging.getLogger('APNS')

def notify(apns_token, message, badge=None, sound=None):
"""Push notification to device with the given message

@param apns_token - The device's APNS-issued unique token
@param message - The message to display in the
notification window
"""
notification = {'aps': {'alert': message}}
if badge is not None:
notification['aps']['badge'] = int(badge)
if sound is not None:
notification['aps']['sound'] = str(sound)
for attempt in range(4):
try:
pyapns.client.notify('MyAppId', apns_token,
notification)
break
except (pyapns.client.UnknownAppID,
pyapns.client.APNSNotConfigured):
# This can happen if the pyapns server has been
# restarted since django started running. In
# that case, we need to clear the client's
# configured flag so we can reconfigure it from
# our settings.py PYAPNS_CONFIG settings.
if attempt == 3:
log.exception()
pyapns.client.OPTIONS['CONFIGURED'] = False
pyapns.client.configure({})
time.sleep(0.5)

Since I glossed over it in this post, I'll cover how to get the APNS device token for a mobile device in my next post. The device token acts as an address, telling Apple's Push Notification service which mobile device it should deliver your notification message to.

Tuesday, September 7, 2010

Calculating degree deltas for distances on the surface of the Earth

Here is the scenario: you've got your GPS coordinates (latitude & longitude in degrees) of your current position and you want to find the number of degrees north/south and east/west needed to contain an area of some size around you. I know, this sounds contrived, but it comes up if you use the iPhone's MapKit framework and want to zoom a MKMapView to a level where only a certain distance around a location is displayed. In that case, you need a MKCoordinateRegion to pass to the setRegion:animated: method.

One might think that if you know the rate of conversion between meters (or if you prefer, miles) and degrees, this would be a straightforward conversion. The problem is that it isn't so simple. Since the earth is a sphere, the number of meters in one degree of longitude depends on your latitude. For example, at the equator, there are 111.32 kilometers per degree of longitude; at the poles, however, there are 0 meters per degree.

A MKCoordinateRegion is comprised of two components: the coordinates of the center of the region and a span of latitudinal and longitudinal deltas. Let's assume the center is known; for example, it could be your user's current location. To calculate the span, here is a simple function that takes into account the curvature of the earth:
/*!
* Calculate longitudinal and latitudinal deltas in
* degrees for the given linear horizontal and vertical
* distances in kilometers. Longitudinal degrees per
* kilometer vary with latitude, so a coordinate is
* needed as a frame of reference.
*
* @param coord - point of reference.
* @param xDistance - east-west distance in kilometers.
* @param yDistance - north-south distance in kilometers.
* @return MKCoordinateSpan representing the distances
* in degrees at the given coordinate.
*/
static
MKCoordinateSpan
spanForDistancesAtCoordinate(CLLocationCoordinate2D coord,
double xDistance,
double yDistance)
{
const double kilometersPerDegree = 111.0;

MKCoordinateSpan span;

// Calculate the latitude and longitude deltas that
// correspond to the distance (in kilometers) at
// the given coordinate. Note that the longitude
// degrees calculation is complicated by virtue of
// the fact that the number of meters per degree
// varies depending on the coordinate's latitude.
span.latitudeDelta = xDistance / kilometersPerDegree;
span.longitudeDelta = yDistance / (kilometersPerDegree * cos(coord.latitude * M_PI / 180.0));
return span;
}

The user's current location, which will become the center of the MKCoordinateRegion, should be passed as the point-of-reference coord argument. This is used to calculate the number of meters per degree at the user's current latitude.

The xDistance and yDistance parameters are the number of kilometers east-west and north-south, respectively, that define the region.

Note that the constant kilometersPerDegree represents the number of kilometers per degree of latitude or the number of kilometers per degree of longitude at the equator; it is only an estimate. Since the Earth isn't a perfect sphere, the actual number varies, but for the sake of most iPhone apps, the estimate of 111.0 kilometers/degree should be sufficient.

Tuesday, August 17, 2010

RFC 3339-compliant Unicode date format pattern

Here is a quick note just to say that if you need to generate RFC 3339 timestamps or ISO 8601-compliant combined date/time representations, here is the Unicode date format pattern to do so:
yyyy-MM-dd'T'HH:mm:ss.SSSSZ

This could come in handy if, for example, you are using Apple's NSDateFormatter class. NSDateFormatter has no predefined style corresponding to RFC 3339 / ISO 8601 format so you'll need to use a format specifier string instead; NSDateFormatter format strings comply with the Unicode date format patterns. So you can use the format pattern above to parse or output strings that can be exchanged with other RFC 3339 / ISO 8601 compliant systems.

For example, the following Objective-C code will print out the current timestamp as an RFC 3339 / ISO 8601 compliant combined date/time string:
NSDateFormatter *rfc3339 = [[NSDateFormatter alloc] init];
[rfc3339 setDateFormat:@"yyyy-MM-dd'T'HH:mm:ss.SSSSZ"];

NSDate *now = [NSDate date];
NSLog(@"%@", [rfc3339 stringFromDate:now]);
[rfc3339 release];

At the time of this writing, the output looked like "2010-08-17T16:38:21.9640-0700".

Friday, July 16, 2010

HTML encoding of form inputs

I suppose this is common knowledge amongst professional web developers but I just discovered myself that if a user enters characters into a HTML form input that is not representable in the character set of the page the form is in, browsers will HTML-encode the non-representable characters when the form is submitted. I just spent over an hour assisting a coworker to track down a bug in one of our web applications that was due to this poorly-documented -- but reasonable -- behavior.

I say "reasonable" because, as obscure as it is, this is really the best thing I think a browser can do given the situation.

To recap, here is the scenario:
  • You have a web page with a form in it that is served using some locale-specific encoding. In our case it was Shift-JIS, but the default ISO8859-1 encoding leads to the same problem.
  • The user enters text into a form input field that is not representable in the displayed page's character set or encoding. For example, entering Cyrillic characters into a form displayed on an ISO8859-1 page.
  • When the user submits the form, the browser tries to convert the inputs to the encoding of the page. Any character not representable in that page's character set or encoding has its Unicode character code point encoded as an HTML numeric character reference (e.g. DŽ).
  • The web application or CGI receiving this input needs to a) know the character encoding of the page that was used to submit the form data so it knows how to interpret the data as characters and b) be prepared to convert any embedded HTML numeric character references back to their corresponding characters.

I like that last part where web applications (or CGIs) have to know the encoding of an HTML page served to the client in order to be able to properly parse input from that client. This fact shatters any remaining fantasies I had of HTTP being stateless.

Anyway, the real surprise is that a web application or CGI needs to be prepared to unencode HTML entities in form input. I quick check of perl's CGI.pm and python's cgi module indicates that neither of them do entity decoding of inputs automatically. And considering that information on the web regarding this behavior is sparse , I suspect that most web developers are unaware of it. At the time of writing, I can only find two references [1][2] that document HTML character reference encoding in the scenario described above.

Luckily, there is a really simple solution: always serve pages in UTF8 encoding and always expect form input to be in UTF8 encoding. One of the many great things about UTF8 encoding is that all characters are representable, so you never have to worry about the browser resorting to HTML character reference encoding.

Monday, July 12, 2010

Sharp pointy sticks

This past Saturday my wife and I bought recurve bows with the intent to make a hobby out of archery. It turns out there are a number of parks with free-to-the-public archery ranges in the Bay Area. A couple of the ranges we have found are:
We actually got started a couple of weeks ago when we went for a free lesson from the Kings Mountain Archery Club. That was a lot of fun and our instructors were very friendly, helpful, and patient. They give free two-hour introductory lessons about once a month; information about how to signup is available on their web site.

We picked up our bows and arrows at a shop intimidatingly called Predator's Archery down in Gilroy. Much like our experience with Kings Mountain Archery, the staff at Predator's Archery were pretty friendly and helpful and they offer a great "starter package" which includes everything you need as well as 5 lessons.

Friday, July 2, 2010

In other news: Subversion still sucks

OK, I've had 4 months to get used to Subversion now. And it is growing on me. Or perhaps it is Stockholm Syndrome. But there are still a lot of annoyances.

After being bashed as a "troll" by one of Subversion's authors after daring to suggest it wasn't all ponies and rainbows, I thought I would check to see if others where sharing my pain transitioning from CVS to Subversion.

Not surprisingly, I did. I found a wonderful summary of all the frustrations I've been experiencing, thoughtfully compiled by no less than David O'Brien of the FreeBSD community.

I would add to his list, as my friend John pointed out in comments to my previous post, that it is really annoying to have to depend on external tools (ironically, CVS) to see commits across branches.