Building Twilio Client for Android

MAY 17

Building software for a client-side device is quite a bit different than building for the cloud. In the cloud, deployment intervals are measured in days, and sometimes hours. But when you're installing software to potentially millions of users' devices, upgrade cycles turn into weeks, months, and in some cases even years. In this post, I'll talk about challenges related to application lifecycle and incoming call handling on Android.

Preparing a product as close to 1.0 perfection as possible is essential, and Android has been a great example of our journey to try to do just that. Our API design decisions are driven by a desire to hide the complex details of telecom from the application developer.

Fortunately, we started work on Twilio Client for iOS several months before tackling Android. The beta cycle and release of that product taught us a lot about how people might use the Android version of Twilio Client.

The fact that Android is Android, however, presented some new challenges we had not seen on iOS. Android developers have different naming conventions than iOS developers: while didReceiveIncomingConnection sounds perfectly natural to an iOS developer, onIncomingConnection() makes more sense to an Android developer. These types of changes were simple, but some other differences in the way Android works required much more effort to implement well.

Application Lifecycle

Android, unlike iOS, has a clear split between foreground and background tasks; if you want parts of your application to still run when the user switches to another application, you need to wrap those portions in a Service. Since Twilio Client must be able to process incoming and outgoing audio at all times when a call is running, we had to implement a service under the hood.

Android, all the way through version 4.0, also makes no guarantees about keeping your application running; once it's in the background, all bets are off. If the system is running low on memory that's needed elsewhere, the OS will happily kill your entire application, even if it was in the middle of doing something. If you previously had a service running, it'll helpfully schedule it to auto-restart later, at least, but there's still a period of time when your application isn't running anymore, and when it is restarted, you're responsible for restoring any state that was lost when it was killed.

Handling Incoming Calls

So let's take a simple Listener pattern, common throughout Android's framework. We have our DeviceListener interface, which can be implemented by an application developer. Let's say it has a method called onIncomingConnection() that gets called when someone places a voice call to our device. If the user is interacting with the app when that happens, there's no problem. The application's code gets called, and will handle the incoming call.

But what if the user is doing something else with their phone, or their phone is in their pocket? The app is in the background, and isn't allowed to put up new UI elements or interact with the user. Ok, ok, Android will let us bring ourselves back to the foreground by doing something fairly simple:

@Override
public void onIncomingConnection(Device device, Connection connection)
{
    Intent intent = new Intent(context, MyIncomingCallActivity.class);
    intent.putExtra("incoming-connection", connection);
    context.startActivity(intent);
}

Android will start up our incoming-call-handling activity for us and bring it to the foreground. Sure, MyIncomingCallActivity will have to override onNewIntent() and onResume() and handle the extra data in the passed intent, but that's not much more of a burden.

Unfortunately, though, we can't just stop there. Take the worst case: Android has killed our entire application due to a low-memory situation (which is fairly common, especially on lower-end and older phones). It then scheduled Twilio Client's internal service to restart, and that has come back up. The service remembers the Devices that were previously created by the application, and restores those. However, Twilio Client, being the generic SDK it is, doesn't know anything about the application, so all old application logic and state is gone. Most importantly, the DeviceListener that was previously set doesn't exist anymore. So the newly-restarted service gets an incoming call, and... drops it on the floor. It has no idea what to do with it.

Intents Are Magic

The answer lies with Android's Intent concept. Think back to how we fixed the first problem we found with incoming calls. When onIncomingConnection() came in, we fired off an intent to launch an activity to handle it. Since this is likely the most common pattern for handling incoming calls, we should just assume that this is what the app developer will do, and provide an API that makes it easy.

An Intent is Android's way of abstractly encapsulating some kind of action that needs to be performed. Intents that refer to any Android component type, whether it's an activity, service, or broadcast receiver, can be wrapped in a PendingIntent. Pending intents are valid across application and process boundaries, and Twilio Client's background service can save and restore them if it gets killed and is restarted. Even better, the piece of code that activates the pending intent doesn't need to know what it does.

So we end up with a very simple interface for setting up an incoming call handler:

Intent intent = new Intent(context, MyIncomingCallActivity.class);
PendingIntent pIntent = PendingIntent.getActivity(context, 0, intent,
                                                  PendingIntent.FLAG_UPDATE_CURRENT);
device.setIncomingIntent(pIntent);

The developer still has to override Activity.onNewIntent() to catch the new intent, and Activity.onResume() to act on it, but otherwise that's it. Twilio Client will call send() on the pending intent when an incoming call comes in, and your code can handle the call however it wants. Instead of using PendingIntent.getActivity(), you can use getService() or getBroadcastReceiver() to have a Service or BroadcastReceiver handle incoming calls.

In the end, our Android API is flexible, future-proof, and handles even the worst-case scenario of Android application lifecycle. Most importantly, it continues to hide most of the messy details so developers can focus on building their application without having to jump through hoops to make it behave properly in all situations.

Twilio Client for Android is now publicly available. Go download it and give it a try!