17

I am creating a new Android app using SyncAdapter to handle db sync. I have everything in place and the app is working fine but I noticed that I am logged in twice.

The first login takes place when the AuthenticatorActivity class (it extends AccountAuthenticatorActivity) validates the user and password.

If the user and password are correct the AuthenticatorActivity then does:

  • If the account didn't exist it creates it using mAccountManager.addAccountExplicitly()
  • The authToken is saved using intent.putExtra(AccountManager.KEY_AUTHTOKEN, authToken);

This was basically copied/pasted from the Android samples, so I guess it's ok.

The issue is that when the SyncAdapter launches and uses

authtoken = mAccountManager.blockingGetAuthToken(account,
          AuthenticatorActivity.PARAM_AUTHTOKEN_TYPE, true);

The getAuthToken() method inside the Authenticator class which extends AbstractAccountAuthenticator is called. And inside this method I am hitting the login endpoint once again.

From that point onwards the login endpoint is not hit again until the authToken expires.

This is not something that bothers me a lot but I would like to know if there is a way to avoid doing the login twice.

Kyle Clegg
  • 38,547
  • 26
  • 130
  • 141
Macarse
  • 91,829
  • 44
  • 175
  • 230
  • What about using `AccountManager.setAuthToken()` instead of passing the token back in the bundle? – alexanderblom Jul 25 '12 at 15:53
  • @alexanderblom: I tried that as well. No difference. – Macarse Jul 25 '12 at 16:54
  • 1
    I think the reason behind two logins was to ensure auth tokens are up to date, but I couldn't find the source to back me up. I remember reading it somewhere when I tried to follow C2DM example when it first came out last year in Google I/O... – Yenchi Jul 30 '12 at 16:13

1 Answers1

15

As you've seen, though Authenticator.java in the SampleSyncAdapter says

The interesting thing that this class demonstrates is the use of authTokens as part of the authentication process. ... If we already have an authToken stored in the account, we return that authToken. If we don't, but we do have a username and password, then we'll attempt to talk to the sample service to fetch an authToken.

that's a lie. Authenticator.getAuthToken doesn't to any cache checking, it just hits the network to get a token.

The solution is to add in the missing check:

Authenticator.java:
@Override
public Bundle getAuthToken(AccountAuthenticatorResponse response, Account account,
        String authTokenType, Bundle loginOptions) throws NetworkErrorException {

    // check that authToken type supported
    ...

    // Check if we already have a cached token to return
    final AccountManager am = AccountManager.get(mContext);
    String cachedAuthToken = am.peekAuthToken(account, authTokenType);
    if (cachedAuthToken != null) {
        final Bundle result = new Bundle();
        result.putString(AccountManager.KEY_ACCOUNT_NAME, account.name);
        result.putString(AccountManager.KEY_ACCOUNT_TYPE, Constants.ACCOUNT_TYPE);
        result.putString(AccountManager.KEY_AUTHTOKEN, cachedAuthToken);
        return result;
    }

    // Get new authToken from server
    ...

    // If all else fails, prompt the user for credentials
    ...
}

Note that the rest of your project needs to religiously use AccountManager.invalidateAuthToken when calls fail or else you'll wind up with an infinite loop of calls failing, trying to get a new auth token, and then failing again when the same cached auth token is returned.

blahdiblah
  • 33,069
  • 21
  • 98
  • 152
  • 2
    If you need a full working example, I used the same technique on my sample Sync adapter for my blog post on that subject. You can see the sample app code here: https://github.com/Udinic/SyncAdapter The blog post about sync adapters here: http://udinic.wordpress.com/2013/07/24/write-your-own-android-sync-adapter/ Specifically about Authenticators, explaining this problem, you can read here: http://udinic.wordpress.com/2013/04/24/write-your-own-android-authenticator/ – Udinic Jul 27 '13 at 11:28
  • in the class that extends AccountAuthenticatorActivity, I call am.setAuthToken(String) and am.setPassword(String). In the class that extends AbstractAccountAuthenticator, I call am.peekAuthToken(account, authTokenType); This returns null the first time. It then logs in using the username from the account and the password saved in the account manager (am.getPassword(account)); And then the authtoken is saved in the accountmanager through the bundle returned by getAuthToken. Anyone know why am.setPassword() successfully sets the password but am.setAuthToken(String) does not? – Ethan Jun 04 '16 at 02:17
  • In small world news, I was working on an inherited project that had some very familiar-feeling code. I traced its lineage and discovered that it ultimately came from this post! – blahdiblah Dec 12 '18 at 00:39