Building Twilio Client for Android
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!