开发者

OAuth instance state in Android

I'm trying to use OAuth in an Android app. I have it working correctly but have sometimes run into a problem during the authentication phase. In Android, I launch the browser for the user to login and authenticate. Then the callback url will redirect back to my application.

Here is the problem. My application has a OAuth consumer and provider as members of my main class. When the browser is launched for authentication, sometimes my main Activity is discarded to save m开发者_开发问答emory. When the callback url relaunches my main Activity, the provider and consumer are new instances and therefor don't work when I try to make a request to the api. If the main Activiy was not freed during the authentication phase, then everything works correctly because I'm still working with the original consumer and provider.

I tried using onSaveInstanceState() and onRestoreInstanceState(), but haven't been successful. It seems the onRestoreInstanceState() is not called when my callback url is handled. Seems to go straight to the onResume().

What is the correct method for persisting the consumer and provider in this case?


Complete Save / restore solution

Apart from the request_token and token_secret, the isOauth10a() state is important to be restored in the provider. There could be more state information in the future. Hence, I like the persist and load solution the best.

I extended GrkEngineer's solution to be more complete. It saves / restores both the provider and consumer, handles all exceptions, and sets the httpClient while restoring.

protected void loadProviderConsumer()
{
  try {
    FileInputStream fin = this.openFileInput("tmp_provider.dat");
    ObjectInputStream ois = new ObjectInputStream(fin);
    provider = (CommonsHttpOAuthProvider) ois.readObject();
    provider.setHttpClient(httpClient);
    ois.close();
    fin.close();

    fin = this.openFileInput("tmp_consumer.dat");
    ois = new ObjectInputStream(fin);
    consumer = (CommonsHttpOAuthConsumer) ois.readObject();
    ois.close();
    fin.close();

    Log.d("OAuthTwitter", "Loaded state");
  } catch (FileNotFoundException e) {
    e.printStackTrace();
  } catch (StreamCorruptedException e) {
    e.printStackTrace();
  } catch (IOException e) {
    e.printStackTrace();
  } catch (ClassNotFoundException e) {
    e.printStackTrace();
  }
}

protected void persistProviderConsumer()
{

  try {
    FileOutputStream fout = this.openFileOutput("tmp_provider.dat", MODE_PRIVATE);
    ObjectOutputStream oos = new ObjectOutputStream(fout);
    oos.writeObject(provider);
    oos.close();
    fout.close();

    fout = this.openFileOutput("tmp_consumer.dat", MODE_PRIVATE);
    oos = new ObjectOutputStream(fout);
    oos.writeObject(consumer);
    oos.close();
    fout.close();

    Log.d("OAuthTwitter", "Saved state");
  } catch (FileNotFoundException e) {
    e.printStackTrace();
  } catch (IOException e) {
    e.printStackTrace();
  }
}

I have tested this code and it works.


You can read my old post here. Generally what I've done was to use static reference and using Activity with WebView instead of standalone browser to display authentication form


I solved this by persisting the provider object to a file. I'm using the signpost library and both the provider and consumer are serializable.

protected void loadProvider()
{
    FileInputStream fin = this.openFileInput("provider.dat");
    ObjectInputStream ois = new ObjectInputStream(fin);
    this.provider = (DefaultOAuthProvider) ois.readObject();
    ois.close();
    consumer = this.provider.getConsumer(); 
}

protected void persistProvider()
{
    FileOutputStream fout = this.openFileOutput("provider.dat", MODE_PRIVATE);
    ObjectOutputStream oos = new ObjectOutputStream(fout);
    oos.writeObject(this.provider);
    oos.close();
}

I call persist provider just before launching the browser view intent for authentication, and I restore the provider in onResume() just before calling provider.retrieveAccessToken(). If you call persistProvider() and loadProvider() in a couple more locations, you can also have it save the proper tokens post-authentication. This would remove the need to re-authenticate (as long as the token is valid).

I do still wish I knew which fields in the provider class are actually needed to persist. Might be kind of slow to serialize the entire object.


Possibly the reason why the original poster had problems maintaining instance state is because the default behaviour for Android is to start a new activity for each new intent. That's why GrkEngineer was not seeing onRestoreInstanceState being called after the web callback.

Storing your request token as a shared preference is one solution so that it can be acccessed from the new activity that is started after the OAuth web callback.

I originally tried using shared preferences and it seemed to work ok. However, I don't think that is the best solution. Ideally, you want to force Android to deliver the callback to your original activity (I'll explain why below).

I tried using the singleTask, and singleInstance launch modes to accomplish this with partial success, but it felt wrong, and the Android docs hint that those modes are not recommended for general use.

After much digging through the documentation and testing I found that using the following flags when creating the intent, causes Android to deliver the intent to an existing instance of the activity (recreating it if it's been killed).

intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);

The reason why I needed to get the callback to be handled by the original activity was so I could integrate with android AccountManager. I used the following sample to get me started:

http://developer.android.com/resources/samples/SampleSyncAdapter/index.html

One of the key parts of integrating with the AccountManager authenticator mechanism is the AccountAuthenticatorResponse that is passed into your activity to start the authentication process.

I found the big problem with implementing this was keeping a reference to the AccountAuthenticatorResponse object. This is passed into your AuthenticatorActivity and you need to call methods on it once authentication is complete so that the standard accounts UI is left in the correct state. However, I hit the same problem that GrkEngineer initially hit. When I tried to restart my OAuth authenticator activity after the OAuth callback, I was always getting a new instance which had lost the reference to the AccountAuthenticatorResponse object and I couldn't see any way to persist that object.

The key was to use the intent flags I described above.

AuthenticatorActivity is started using FLAG_ACTIVITY_NEW_TASK by my AbstractAccountAuthenticator. It fetches the request token (using AsyncTask) and starts the browser to ask the user to authorize.

OAuthCallbackHandlerActivity is registered to handle my custom callback scheme. When it gets called after the user grants access, it makes a call to AuthenticatorActivity using the flags Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP intent.

This causes my original AuthenticatorActivity to be reactivated. The AccountAuthenticatorResponse object is still available (as are the request token/secret, which I saved in OnSaveInstanceState). The activity can now get the access token (again using AsyncTask) and then call the completion methods on the AccountAuthenticatorResponse object.

The key to making this work was using the intent flags I mentioned, and also making sure that the AuthenticatorActivity is started in your application task rather than the account manager task. FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_SINGLE_TOP will only cause an existing instance of an activity to be reused if they are in the same task. So if the activity that you want to get back to is started in some other task, then the original instance would not be re-used.

I tested this on an emulator using Dev Tools to kill my AuthenticatorActivity immediately so I could test the recreation process. It worked great using onSaveInstanceState/onRestoreInstanceState for the request token/secret. And I didn't even have to worry about restoring the AccountAuthenticatorResponse object. That was restored by Android itself - magic!


You only need to persist consumer.getToken() and consumer.getTokenSecret()

Later you can simply recreate a new consumer(customerKey,customerKeySecret) and consumer.setTokenWithSecret(token, tokenSecret)

What was tricky is to find out was the following:

  1. Use CommonsHttpOAuthConsumer and CommonsHttpOAuthProvider (on android) the DefaultOAuthProvider will not work.

  2. When using HttpURLConnection, you cannot sign POST requests that carry query parameters in the message payload


I had the same problem. All you need to persist is the requestToken and the tokenSecret that you get after calling retrieveRequestToken. In your onResume() method, recreate the consumer and the provider object as described here. That way you don't need to persist the whole consumer and provider objects and will still be able to retrieve the accessToken.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜