April 11th, 2014 by Blake Lucchesi
This week at SendHub I spent a lot of time trying to fine-tune the way our app alerts users when someone is calling them. The goal is to provide our users with an experience that matches the default iPhone ringing experience as close as possible.
Apple gives us two options for showing alerts on the iPhone when our application is in the background. The first is a Push Notification, a message sent via network connection from our server, to Apple’s servers and then to the iPhone. The other option is a Local Notification (UILocalNotification), initiated by code running inside our application. While either of these two methods will work, we’d actually prefer to use a local notification for a few reasons but the main one is timely delivery.
Being a registered VoIP application, we already have a low latency connection between our server and our app that we can use to trigger a local notification. This would be much faster and more reliable than push notifications which have no guarantee of delivery and can be delayed to conserve network activity.
Both Push and Local Notifications give us the ability to accomplish this. So it’s a push! (I know, I couldn’t help it).1
Because we’re registered as a VoIP app, iOS allows us to play audio while the app is in the background via CoreAudio. So at first, this seemed like a great way to make our phone ring. We would have full control of when we start and stop playing the sound file; we could even determine how loud we want to play it. However, it falls short for us in a few critical areas as outlined in #4 and #5 below. Therefore, we go back to the Push vs Local notification decision because they both allow us to play an audio file bundled with our application.
Apple has done us a favor here by providing us with only a single method to vibrate the phone.2 The approach is to use a notification with a sound. By specifying a sound file as part of the notification, iOS will determine whether it should play the audio or vibrate the phone. This is determined by the position of the hardware mute button on the side fo the phone.
If we use a push or local notification to play the incoming ring audio, iOS adheres to the default muting behavior automatically. This means that a simple tap on one of the volume buttons or the power button will silence the incoming ring sound. If we had tried to play our incoming ring sound using CoreAudio the user would have to hold the volume down button much longer until the volume was completely muted, just as if they were listening to their music using Pandora or Spotify.
Once a push notification has been received by the operating system, there is no way for us to stop the audio played along with the notification. I thought that I might be able to trick the phone into canceling previous notifications from our app by sending another notification with the badge key set to 0, but my efforts were fruitless.
Luckily, if we generate the notification via UILocalNotification we have the flexibility to stop specific notifications or all the local notifications we’ve initiated using either of the two methods supplied via UIApplication.
And when local notifications are cancelled via those methods, the banner alert disappears and the audio silences immediately. Exactly what we’re looking for.
Just like above in #6 we must use a local notification so that we can stop the ringing sound before the end of the sound clip.
After a week of ups and downs playing with the different Apple APIs I’m quite satisfied with the results of my work. As we look back through our list of desired behaviors it becomes quite clear that using a single Local Notification provides our users with a very nice incoming call experience.
Added a few inline footnotes with corrections based on more investigation. My overall approach has gone unchanged but I thought it would be important to clarify a few points that I originally made.
After doing some more thorough testing, there appears to be a bug in iOS 7. When you swipe/act on a local notification from the lock screen the audible sound that is played with the local notification cannot be cut short. It will continue playing until the whole audio clip is played. This bug occurs in many other VoIP applications I’ve tested as well (Skype, Google Hangouts, etc). This results in a short overlap of the ringing sound and the phone call, which is certainly less than ideal. See full details from the radar report I filed.↩
After digging in a bit further I was able to find a way to make the phone vibrate using the System Sound Services located in the AudioToolbox framework. AudioServicesPlaySystemSound(kSystemSoundID_Vibrate);↩