Android Authenticators II

By | 2013-06-19

Last time we saw how to make Android support our own custom account type. This time we’ll see how to make use of that account type in our own application.

How does our app call getAuthToken()?

  • Get an account name of the appropriate type (i.e. our custom type)
  • If there are no accounts: start an asynchronous account addition in motion.
  • If there is an account: set an asynchronous authentication token retrieval in motion
  • Wait for asynchronous response

First we’ll need some permissions – GET_ACCOUNTS for AccountManager.getAccountsByType() and USE_CREDENTIALS for getAuthToken().

    <uses-permission android:name="android.permission.GET_ACCOUNTS" />
    <uses-permission android:name="android.permission.USE_CREDENTIALS" />

Then we’ll make a new top-level activity as normal, in which we’ll take a number of steps:

  • Establish our unique account type (fetch this as a string resource)
  • Fetch the list of accounts with that type (GET_ACCOUNTS permission)
  • Initiate an authentication token fetch (USE_CREDENTIALS permission)

Account List

In your Activity’s onCreate() method, you’ll do something like this:

    mAccountType = getString(R.string.authenticator_account_type);
    mAccountManager = AccountManager.get(this);

    // TODO: UI to pick account, for now we'll just take the first
    Account[] acc = mAccountManager.getAccountsByType(mAccountType);
    if( acc.length == 0 ) {
        Log.e(null, "No accounts of type " + mAccountType + " found");
        // TODO: add account
        return;
    }
    mAccount = acc[0];
    startAuthTokenFetch();

The error handling is poor here, you should report errors to the user, not just exit; but that would complicate my example, so I haven’t done it. I’ve also not added support for multiple instances of the same account type – instead opting to just use the first one in the returned list for now.

Fetch an Authentication Token

The getAuthToken() call is asynchronous, so we don’t get a token back, we start a process that happens in the background, and we supply callbacks to alert us when that process has completed (or failed):

    private void startAuthTokenFetch() {
        Bundle options = new Bundle();
        mAccountManager.getAuthToken(
                mAccount,
                mAuthTokenType,
                options,
                this,
                new OnAccountManagerComplete(),
                new Handler(new OnError())
            );
    }

mAccount we looked up above; mAuthTokenType is entirely up to you and your application. If you have different levels of access to an API, then you could handle that by passing a name for each of those types to the authenticator Service via this parameter – your service then handles obtaining an appropriate token for that type however you wish, so really this is an entirely opaque parameter to Android other than to use it as a lookup key for the various tokens you store.

Then we just provide the OnAccountManagerComplete() callback class, and we’re done.

    private class OnAccountManagerComplete implements AccountManagerCallback<Bundle> {
        @Override
        public void run(AccountManagerFuture<Bundle> result) {
            Bundle bundle;
            try {
                bundle = result.getResult();
            } catch (OperationCanceledException e) {
                e.printStackTrace();
                return;
            } catch (AuthenticatorException e) {
                e.printStackTrace();
                return;
            } catch (IOException e) {
                e.printStackTrace();
                return;
            }
            mAuthToken = bundle.getString(AccountManager.KEY_AUTHTOKEN);
            Log.d("main", "Received authentication token " + mAuthToken);
        }
    }

What you do with that token is then up to you and your application-specific needs; for example you might include it in a JSON command to your server RPC interface.

Handling Multiple Accounts

Above, we didn’t cope very well if there was more than one account type defined. Let’s do better.

    Account[] acc = mAccountManager.getAccountsByType(mAccountType);
    if( acc.length == 0 ) {
        Log.e(null, "No accounts of type " + mAccountType + " found");
        return;
    } else {
        Log.i("main", "Found " + acc.length + " accounts of type " + mAccountType);

        Intent intent = AccountManager.newChooseAccountIntent(
                null,
                null,
                new String[]{getString(R.string.authenticator_account_type)},
                false,
                null,
                getString(R.string.auth_token_type_default),
                null,
                null);
        startActivityForResult(intent, ACCOUNT_CHOOSER_ACTIVITY);
    }

We’re using startActivityForResult(), so we’ve defined a private code to identify this remote call, ACCOUNT_CHOOSER_ACTIVITY, and we’ll have to implement onActivityResult() to catch the answer when it comes back.

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        if( resultCode == RESULT_CANCELED)
            return;
        if( requestCode == ACCOUNT_CHOOSER_ACTIVITY ) {
            Bundle bundle = data.getExtras();
            mAccount = new Account(
                    bundle.getString(AccountManager.KEY_ACCOUNT_NAME),
                    bundle.getString(AccountManager.KEY_ACCOUNT_TYPE)
                    );
            Log.d("main", "Selected account " + mAccount.name + ", fetching");
            startAuthTokenFetch();
        }
    }

If there is one account only, Android is sensible enough to just return that account, if there are multiple accounts, you’ll get a popup list from which you can pick, and then the result returned. We’ve just constructed an Account object for our mAccount member with the returned value, and continued as previously with fetching the token. In the end though, we’re still just calling startAuthTokenFetch() with an account.

No Accounts Configured

We’ve now dealt with multiple accounts being registered, but what if there are none? Rather than give the user instructions that your application only works if they go to Settings->Add Account, why don’t we just forward them there? Let’s rewrite our “no account” code:

    if( acc.length == 0 ) {
        Log.d("main", "No suitable account found, directing user to add one");
        // No account, push the user into adding one.  We use addAccount
        // rather than an Intent so that we can specify our own account
        // type -- requires MANAGE_ACCOUNTS permission
        mAccountManager.addAccount(
                getString(R.string.authenticator_account_type),
                getString(R.string.auth_token_type_default),
                null,
                new Bundle(),
                this,
                new OnAccountAddComplete(),
                null);
    } else {
        // ... we covered this above ...
    }

Just as with the getAuthToken() call, AccountManager operations are always asynchronous, so we have to finish with the callback.

    private class OnAccountAddComplete implements AccountManagerCallback<Bundle> {
        @Override
        public void run(AccountManagerFuture<Bundle> result) {
            Bundle bundle;
            try {
                bundle = result.getResult();
            } catch (OperationCanceledException e) {
                e.printStackTrace();
                return;
            } catch (AuthenticatorException e) {
                e.printStackTrace();
                return;
            } catch (IOException e) {
                e.printStackTrace();
                return;
            }
            mAccount = new Account(
                    bundle.getString(AccountManager.KEY_ACCOUNT_NAME),
                    bundle.getString(AccountManager.KEY_ACCOUNT_TYPE)
                    );
            Log.d("main", "Added account " + mAccount.name + ", fetching");
            startAuthTokenFetch();
        }
    }

Done. If there is no account defined when our Activity starts, AccountManager is triggered to create one. We saw last time how that actually ends up back in our application by our provision of an authenticator Service; that service tells Android’s account manager to run our LoginActivity, which then returns to the account manager, which then calls this OnAccountAddComplete, which then runs the same end point: startAuthTokenFetch(). All roads lead to Rome.

Next time I’ll be looking at how we tap in to Android’s synchronisation infrastructure to keep our app up-to-date with a remote server.

Leave a Reply