Recently I wrote a scary App-Engine back end for an Android app. I wanted it to be secure, which should be easy because Androids have Google accounts and App Engine knows about those. I got it to work, but the process irritated me enough that I decided to package it up as a public service. So now there’s a little open-source library called App Engine REST Client. It offers GET and POST methods, includes an Authenticator class, and tries to be as simple as possible to use.
When it comes to App Engine authentication, the factors that can trip up a literal-minded minded programmer with insufficient attention to detail, like for example me, include:
The most obvious way of using the authent APIs can result in control jumping from one thread to another in a non-obvious way. This makes them harder to sequester into a just-authenticate-dammit method call.
There are reports of bugs in AccountManager implementations on popular handsets out there. It’s on the XDA forums, so it must be right, right?
To conduct an authenticated HTTP conversation you simultaneously need to be in the background off the UI thread, and engage in human interaction to OK the use of a Google account.
The API for authenticating against App Engine is thinly documented. Seems it was an experiment that was never really announced, but rather discovered by enthusiasts in the community and put to good use. App Engine has interesting plans for another run at the problem but for the moment, this is what we have.
Android’s Dalvik engineering group would generally prefer that you use HttpURLConnection, but all the sample authent code I could find out there on the Net didn’t.
The Simplest Thing That Could Possibly Work · First off, let’s find out how much you can do with how little. Here’s a teeny tiny little Android app that checks in with an App Engine app, fetches a secret, and displays it. It has to authenticate properly or the secret wouldn’t be secret.
The work is done in an AsyncTask, because both the authentication and the GET involve network I/O and thus can’t be done on the UI thread. I’ve highlighted the four lines of code where it all happens.
public class CheckinActivity extends Activity {
private Activity activity;
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
activity = this;
(new Worker()).execute();
}
private class Worker extends AsyncTask<Void, Void, Response> {
AppEngineClient client;
protected Response doInBackground(Void... params) {
AccountManager manager = AccountManager.get(activity);
Account[] accounts = manager.getAccountsByType("com.google");
client = new AppEngineClient(App.ROOT_URI, accounts[0], activity);
return client.get(App.SECRET_URI, null);
}
protected void onPostExecute(Response response) {
TextView text = (TextView) activity.findViewById(R.id.text);
if (response == null)
text.setText("Checkin failed: " + client.errorMessage());
else if ((response.status / 100) != 2)
text.setText("Checkin failed: " + new String(response.body));
else
text.setText("The secret is: " + new String(response.body));
}
}
}
First we get a list of all the Google accounts on the device. If there are more than one, you might want to put up a dialogue to let the user choose between them, which you’d want to do on the UI thread; in the example we just use the first in the list.
Next, we create our client object. It needs an account to authenticate with, an app URI to authenticate against, and an Activity to use for interacting with the user to bless using this account with App Engine. The framework is smart, and remembers the user’s choice so they don’t get pestered every time the app runs.
Finally, we GET the data. The Response you get back is a three-member
struct containing the HTTP status code, headers, and body (as a
byte[]
). Anyone who’s worked with Ruby’s
Rack framework will
find this familiar. In another more ideal world, Java methods can return
multiple values; but I digress.
Note that there are two different flavors of potential error: First, the
client couldn’t do the GET at all, because authentication failed or the
network wasn’t there, in which case the response is null and you’re pretty
well hooped; if it’s any consolation there’s a diagnostic in client.errorMessage()
.
Second, you did the
GET but the HTTP status wasn’t in the 2xx range; if you’re
lucky, there might be diagnostics in the response headers or body.
Reaching Higher · The app that actually motivated this work interacts with the user, then conducts a potentially lengthy series of HTTP transactions. The HTTP conversation needs to be in the background so that the user needn’t wait, and in a Service so the system will know it’s important and let it grind away.
This is a little trickier because in a Service, it’s hard to interact with the user to authenticate. To support this, we have another constructor for AppEngineClient which doesn’t ask for an activity, but for an “Auth Token”, an opaque string that represents your sign-in credentials.
So our workflow looks like this:
Authenticate in the background with AsyncTask; set up an Authenticator and get an Auth Token from it.
Start the Service using the IntentService convenience class, passing the Auth Token along in a String extra.
Make a client in the Service using the Auth Token, and start chattering with the server.
Here’s the setup, including Steps 1 and 2:
class PrepareUpload extends AsyncTask<Void, Void, String> {
// ...
@Override
protected String doInBackground(Void... params) {
// ...
Authenticator authent = Authenticator.appEngineAuthenticator(mActivity, account, APP_URI);
String authToken = authent.token();
if (authToken == null)
mErrorMessage = authent.errorMessage();
return authToken;
}
@Override
protected void onPostExecute(String authToken) {
// ...
if (authToken != null) {
Intent intent = new Intent(mActivity, UploadService.class);
intent.putExtra("authtoken", authToken);
startService(intent);
}
The Authenticator is provided via a factory method rather than a constructor, because I’m optimistic and think that this library might be extended in the future to handle other flavors of authentication that Android apps might use, in particular OAuth2.
The token()
call tells the Authenticator to go ahead, get authorized to access the application, and return the credentials. This is two-step dance internally, with potentially a couple of round-trips off to the server, and can never be called from the UI thread.
Now here’s where the work actually happens, in the IntentService:
public class UploadService extends IntentService {
// ...
@Override
protected void onHandleIntent(Intent intent) {
String authToken = intent.getStringExtra("authtoken");
AppEngineClient client = new AppEngineClient(APP_URI, authToken, this);
// ...
private void transmit(String body, AppEngineClient client, URL target)
Response response = client.post(target, null, body.toByteArray());
if (response == null)
error(client.errorMessage());
if ((response.status / 100) != 2)
error(getString(R.string.upload_failed) + response.status);
The only really interesting part here is the AppEngineClient constructor, which just takes the authToken and is guaranteed not to want to talk to the user.
Advanced Features ·
If you want to authenticate, but don’t want to use AppEngineClient’s handy get()
and post()
methods, the Authenticator class also has an authenticate(connection)
method, whose argument is an HttpURLConnection instance. It returns true if authentication succeeded; otherwise there’s errorMessage()
for diagnostics.
Implementation Notes · There’s not much to it. Having said that, the code:
Internally uses HttpURLConnection. But there’s no
reason you couldn’t make a version of the authenticate()
method that operates on an Apache HTTP client.
Has workarounds for the known pre-Froyo bugs, and for some reported Android AccountManager weirdness.
Tries to authenticate in such a way that you can have a reasonably-lengthy client/server conversation without being bitten by an expiring token.
Has a project name that contains “REST” but omits useful HTTP methods and doesn’t actually contain any code designed either to encourage good REST practices or discourage filthy RPC debauchery.
What This Is · At this point I should stop and give thanks to those whose code I studied and learned from. In particular I owe a lot to Nick Johnson, Rodrigo Damazio, and the sample code for the 2011 Google IO session Android + App Engine: A Developer’s Dream Combination, by Xav Ducrohet and Brad Abrams.
AERC is an Android Library Project which you can go get at Google code. It’s Apache-licensed.
If it doesn’t meet your needs, but you still want to do App Engine authentication, you might find it useful to look at the code to steal some ideas.
What This Isn’t · This isn’t an official Google anything. Nobody’s promising that it’ll meet your needs, nor any kind of support, nor even that it’ll work in the long term. There is one possible future in which something like this gets adopted into an official Google library somewhere, but that’s speculative.
It also doesn’t represent anyone’s opinion about a good general-purpose approach to authenticating Android Apps against Google server-side offerings (or anything else). It’s just a bit of glue to help in hooking up Android apps to App-Engine back-ends, as things stand in 2011.
It sure made constructing my Android/App-Engine bridge a whole lot easier.
Comment feed for ongoing:
From: John Cowan (Oct 03 2011, at 00:55)
"literal-minded minded programmer with insufficient attention to detail, like for example me"
You can say that again. You can say that again.
[link]
From: Thomas Broyer (Oct 03 2011, at 00:57)
Why didn't you use OAuth? AppEngine supports it, and it seems like it's rather easy on Android using the google-api-java-client library.
[link]
From: gunnar (Oct 13 2011, at 11:09)
Yes, exactly, oauth. Salesforce fixed this for Android many moons ago and open sourced it as well
http://wiki.developerforce.com/index.php/Building_Android_Applications_with_the_Force.com_REST_API
[link]
From: Google Applications (Oct 25 2011, at 01:56)
Hi Tim, thanks for the guide.
I've submitted this article as featured on GAE Cupboard.
[link]