Recently, I added the remote push capabilities to an iPhone/iPad application at work. It turned out to be a bit more involved process than I expected so I would document key steps of the process.
Overview of Remote Push Notification
The Apple Push Notification works like SMS, where the application providers can send short messages to the users (upto 256 bytes). The message consists of JSON structure as follows:
{ "aps" : { "alert" : "your-message", "sound" : "filename" : "badge" : 5, }, // optionally more structures, e.g. {"acme1" : "bar"} }
The alert key defines the contents of the message, which is displayed to the user. You can play sound by specifying the sound filename (which is already bundled with the app), however you can leave it blank or use ‘default’ for playing default sound. Finally, the badge will allow notification to show a number next to your application icon. Also, you can optionally add action buttons that can take user to a particular screen in your application (though I won’t show it here).
The format for sending notification is as follows:
You can read Apple documentation for more inforamtion on the binary format.
The architecture of Push Notification consists of three key pieces:
- iOS Application – that enables push notification and registers the device for notification
- Application Server – is responsible for generating the messages and publishing them to the Apple Push Notification Service (APNS)
- Apple Push Notification Service (APNS) – is responsible for delivering the messages to the devices where the application is installed
Here is the architecture diagram from Apple documentation on Push Notification:
Enabling Push Notification for the application
I assume you have already created an application with developer certificate, app-id and provisioning profile. However, Apple Push Notification requires another set of certificates for development and production. So, login to the iPhone developer site and select iPhone Provisioning Portal from the upper right side and then choose App IDs link.
Select “Enable for Apple Push Notification service” checkbox, then click on Configure link:
It will guide you through creating certificate, where you will have to create a certificate request by opening “Keychain Access”, then selecting “Certificate Assistant” option from the menu and choosing “Request a Certificate from Certificate Authority”. You can then upload the generated request file to the portal. Once the certificate is generated you should see the option to download the certificate, e.g.
After downloading the certificate, you can double click or drag it to the “Keychain Access” and it will add the certificate to the Keychain.
Publishing Push Notifications on the Application Server
The push notification requires a server that connects to the APNS service and sends notification messages. You will need to use same set of keys and certificates that you created on the Apple development portal. So open “Keychain Access” and select Keys from the left options, then select the private key for the push notification. You can then export the key by right clicking, which will create a file with .12 file extension. Note that it will ask you for a password for encrypting the key (type in anything as we will remove it later). Next, select “My Certificates” from the left options and select the certificate for push notification. Then repeat the process of exporting by right clicking and saving the certificate. You will see two files with .12 extensions, which can be converted into .pem files as follows:
openssl pkcs12 -clcerts -nokeys -out apns_cert.pem -inopenssl pkcs12 -nocerts -out apns_key.pem -in
You can remove password with the the following command:
openssl rsa -in apns_key.pem -out apns_key_unenc.pem
Then merge two files with following command:
cat apns_cert.pem apns_key_unenc.pem > apns.pem
I used Rails on the server side, so I defined a few models and controllers to store devices and messages, where the device model had an attribute ‘token’ and the message model had three attributes: alert, badge and sound. Each device also has an attribute deactivated_at, which is set to the date when the device is disabled. Next, I created a short Ruby library for sending notifications. The library provides two methods send_message for sending notifications and get_feedbacks for checking disabled devices so that you can remove them from your database. The send_message method accepts a message model as an argument and then opens a connection to the APNS service. Apple recommends using a single connection for publishing notifications to all devices, so it adds all devices within a single a connection.
require 'socket' require 'openssl' class ApnsPublisher def self.send_message(apns_message) json_payload = {"aps" => {"alert" => apns_message.alert, "sound" => apns_message.sound, "badge" => apns_message.badge}}.to_json.to_s open_connection(APNS_SERVER, 2195) do |conn, sock| ApnsDevice.find_in_batches(:batch_size => 500 ) do |devices| devices.each do |device| next unless device.deactivated_at.nil? unless ApnsDevicesMessage.find_by_apns_device_id_and_apns_message_id(device.id, apns_message.id) token = device.token.gsub(/\s+/,'') byte_token = [token].pack("H*") message = "\0\0 #{byte_token}\0#{json_payload.length.chr}#{json_payload}" raise "message #{message} is too big" if message.size.to_i > 256 conn.write(message) f.write(message) ApnsDevicesMessage.create(:apns_device_id => device.id, :apns_message_id => apns_message.id, :delivered_at => Time.new.utc) end end end end end def self.get_feedbacks() open_connection(APNS_FEEDBACK_SERVER, 2196) do |conn, sock| while line = sock.gets line.strip! feedback = line.unpack('N1n1H140') token = feedback[2].scan(/.{0,8}/).join('').strip device = ApnsDevice.find_by_token(token) if device device.update_attribute(deactivated_at, Time.at(feedback[0]) end end end end private def self.open_connection(host, port, passphrase='') cert = File.read(APNS_CERT_FILE) ctx = OpenSSL::SSL::SSLContext.new ctx.key = OpenSSL::PKey::RSA.new(cert, passphrase) ctx.cert = OpenSSL::X509::Certificate.new(cert) sock = TCPSocket.new(host, port) ssl = OpenSSL::SSL::SSLSocket.new(sock, ctx) ssl.sync = true ssl.connect yield ssl, sock ssl.close sock.close end end
You can use delayed_job plugin to run the get_feedbacks method periodically. Also, I added a service for registering and adding the devices, which is called from the iPhone devices (but not shown here).
Configuration
You will need to define following properties for development:
APNS_SERVER = "gateway.sandbox.push.apple.com" APNS_FEEDBACK_SERVER = "feedback.sandbox.push.apple.com" APNS_CERT_FILE = File.join(RAILS_ROOT, 'config', 'apns_dev.pem')
and following properties for production:
APNS_SERVER = "gateway.push.apple.com" APNS_FEEDBACK_SERVER = "feedback.push.apple.com" APNS_CERT_FILE = File.join(RAILS_ROOT, 'config', 'apns_prod.pem')
Registering devices for notification
You can register the device by adding following method in your didFinishLaunchingWithOptions of the primary delegate class:
[[UIApplication sharedApplication] registerForRemoteNotificationTypes:( UIRemoteNotificationTypeAlert |UIRemoteNotificationTypeBadge | UIRemoteNotificationTypeSound)];
and then registering with following callback methods:
#pragma mark push notifications - (void)application:(UIApplication *)app didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)devToken { NSString *token = [[devToken description] stringByTrimmingCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@"<> "]]; token = [token stringByReplacingOccurrencesOfString:@" " withString:@""]; DeviceRegisterer *registrar = [[DeviceRegisterer alloc] init]; [registrar registerDeviceWithToken:token]; } - (void)application:(UIApplication *)app didFailToRegisterForRemoteNotificationsWithError:(NSError *)err { NSLog(@"failed to regiser %@", err); } - (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo { NSLog(@"notification options %@", userInfo); }
I used ASIHTTPRequest library for invoking REST service I wrote for registering devices (on Rails side), e.g.
DeviceRegisterer.h
@class ASIFormDataRequest; @interface DeviceRegisterer : NSObject { ASIFormDataRequest *request; } @property (retain, nonatomic) ASIFormDataRequest *request; - (void)registerDeviceWithToken:(NSString *)token; @end
DeviceRegisterer.m
#import "DeviceRegisterer.h" #import "ASIFormDataRequest.h" @implementation DeviceRegisterer @synthesize request; - (void)registerDeviceWithToken:(NSString *)token { if (self.request == nil) { NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"%@/admin/apns_devices.json", API_BASE_DOMAIN]]; [self setRequest:[ASIFormDataRequest requestWithURL:url]]; [request setPostValue:token forKey:@"token"]; [request setTimeOutSeconds:30]; [request setDelegate:self]; [request setDidFailSelector:@selector(registerFailed:)]; [request setDidFinishSelector:@selector(registerFinished:)]; [request startAsynchronous]; } } - (void)registerFailed:(ASIHTTPRequest *)theRequest { NSLog(@"registerFailed %@", [theRequest error]); } - (void)registerFinished:(ASIHTTPRequest *)theRequest { NSLog(@"registerFinished %d",[theRequest postLength]); } - (void)dealloc { [request cancel]; [request release]; [super dealloc]; } @end
Summary
In nutshell, push notification is powerful feature that can help your users engage with your application, though it must be used with caution so that users are not annoyed and in turn remove your application or disable it. It also requires a lot of moving parts for sending notification, registering, getting feedback on the devices. I found that Apple does not provide a great debugging options when testing the push notification. For example, first problem I encoutered with testing was that my profile on XCode was old and wasn’t updated after I enabled push notification. I had to delete my old profile and then refresh it from the Organizer. Also, when you send notifications to the APNS, you don’t get any response code or errors. This caused some frustration when I wasn’t getting messages due to slightly wrong JSON format. Fortunately, when your device is connected you can select the device from the XCode Organizer and view the Console tab for debugging information. I was able to view the error (which a bit vague) and then got everything working after fixing the JSON structure.