AndroidアプリでNetworkOnMainThreadExceptionを避けつつTwitter4Jを使用したアプリ連携を行う #Twitter4J

MarkdownとQiitaの練習として投稿します。この内容は[私のブログ記事](http://reisato.plala.jp/rsato/weblog/2014/01/02/1036.html)と同等の内容です。[追記]2か所の更新は大変なので、Qiitaのみ更新としました。

----

Androidアプリ作成でTwitterを使用する際に欠かせない[Twitter4J](http://twitter4j.org/ja/index.html)ですが、アプリの連携（認証認可）処理を適当に処理を書いていくと``NetworkOnMainThreadException``が発生します。``onCreate``などのメインスレッドで``HttpURLConnection``などの通信をする処理がある場合に発生する例外です。

今日はこの例外を避けつつ、アプリ連携を実現する方法の一つをご紹介します。

## アプリ連携の方法

今回は、アプリ外のブラウザでPINを入力して連携する方法で作ってみました。他には``WebView``を用いてアプリ内のブラウザで処理する方法(WebViewの脆弱性に注意)や、コールバックURLを用いて処理する方法があります。

||PIN|コールバックURL|
|:----:|:----:|:----:|
|外部ブラウザ|**今回はここ**|(``intent-filter``を使用)|
|アプリ内のブラウザ|(省略)|(``WebView``でURLを監視等)|

[追記]マルチユーザー環境でブラウザ類をすべて制限すると、外部ブラウザでの連携ができなくなりますね…

## 連携のためのコード8行

連携のための一連の処理を見てみます。

```java:
// 準備
Twitter twitter = new TwitterFactory().getInstance();
twitter.setOAuthConsumer(consumerKey, consumerSecret);
twitter.setOAuthAccessToken(null);
RequestToken requestToken = twitter.getOAuthRequestToken(); // ここで通信
// このURLでブラウザで認証
String url = requestToken.getAuthorizationURL();
```

ここでユーザーが認証してPINコードを得ます。

```java:
// PIN入力
AccessToken accessToken = twitter.getOAuthAccessToken(pin); // ここでも通信
// トークンの取得。これらを保管しておく。
String token = accessToken.getToken();
String tokenSecret = accessToken.getTokenSecret();
```

これらのコード中に通信が発生する箇所が2つあります。これらを[``Loader``](http://developer.android.com/reference/android/support/v4/content/Loader.html)と[``LoaderManager.LoaderCallbacks``](http://developer.android.com/reference/android/support/v4/app/LoaderManager.LoaderCallbacks.html)を使用して別のスレッドにしてみました。

[追記]Android Support Libraryを使用するようにして、Android2.x系にも対応しました。

## 解決のためのコード例

### リクエストトークンを取得する

まず、リクエストトークンを取得するところ([``Twitter#getOAuthRequestToken()``](http://twitter4j.org/javadoc/twitter4j/auth/OAuthSupport.html#getOAuthAccessToken()))を切り出すために、``AsyncTaskLoader``を継承したクラスを作成します。バインドするのは[``RequestToken``](http://twitter4j.org/javadoc/twitter4j/auth/RequestToken.html)です。コンストラクタで[``Twitter``](http://twitter4j.org/javadoc/twitter4j/Twitter.html)を渡し、[``loadInBackground()``](http://developer.android.com/reference/android/support/v4/content/AsyncTaskLoader.html#loadInBackground())でリクエストトークン取得を実行します。実行の戻り値をそのまま返します。

```java:
public class TwitterOAuthRequestTokenLoader extends AsyncTaskLoader<RequestToken> {

    private Twitter mTwitter;

    public TwitterOAuthRequestTokenLoader(Context context, Twitter twitter) {
        super(context);
        mTwitter = twitter;
    }

    @Override
    public RequestToken loadInBackground() {
        RequestToken requestToken = null;
       try {
            requestToken = mTwitter.getOAuthRequestToken();
        } catch (TwitterException e) {
            requestToken = null;
        }
        return requestToken;
    }
}
```

上のローダークラスを実行し、結果を処理するために、今度は[``LoaderCallbacks``](http://developer.android.com/reference/android/support/v4/app/LoaderManager.LoaderCallbacks.html)を実装したコールバッククラスを作成します。こちらもコンストラクタでAPIキーをセットした[``Twitter``](http://twitter4j.org/javadoc/twitter4j/Twitter.html)を渡し、[``onCreateLoader()``](http://developer.android.com/reference/android/support/v4/app/LoaderManager.LoaderCallbacks.html#onCreateLoader(int, android.os.Bundle))でそのままローダークラスのインスタンス生成時に渡します。[``onLoadFinished()``](http://developer.android.com/reference/android/support/v4/app/LoaderManager.LoaderCallbacks.html#onLoadFinished(android.support.v4.content.Loader<D>, D))の第2引数の[``RequestToken``](http://twitter4j.org/javadoc/twitter4j/auth/RequestToken.html)が処理結果なので、[``RequestToken#getAuthorizationURL()``](http://twitter4j.org/javadoc/twitter4j/auth/RequestToken.html#getAuthenticationURL())でTwitter認証のURLを取得し、ブラウザへ``Intent``で渡します。ユーザーはここで認証し、PINコードを得ます。

```java:
public class TwitterOAuthRequestTokenCallbacks implements LoaderCallbacks<RequestToken> {

    private Context mContext;
    private Twitter mTwitter;

    public TwitterOAuthRequestTokenCallbacks(Context context, Twitter twitter) {
        mContext = context;
        mTwitter = twitter;
    }

    @Override
    public Loader<RequestToken> onCreateLoader(int id, Bundle args) {
        Loader<RequestToken> loader = new TwitterOAuthRequestTokenLoader(mContext, mTwitter);
        loader.startLoading();
        return loader;
    }

    @Override
    public void onLoadFinished(Loader<RequestToken> arg0, RequestToken requestToken) {
        Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(requestToken.getAuthorizationURL()));
        mContext.startActivity(intent);
    }
}
```

### アクセストークンを取得する

ブラウザが表示され、ユーザーが認証をすると、PINが表示されます。

PINからアクセストークンを取得するところ([``Twitter#getOAuthAccessToken()``](http://twitter4j.org/javadoc/twitter4j/auth/OAuthSupport.html#getOAuthAccessToken()))についても上記のリクエストトークンを取得する場合と同様です。

### 使う

[``Twitter``](http://twitter4j.org/javadoc/twitter4j/Twitter.html)はConsumer keyを設定して用意しておきます。

```java:
mTwitter = new TwitterFactory().getInstance();
mTwitter.setOAuthConsumer(KEY), KEY_SECRET);
mTwitter.setOAuthAccessToken(null);
```

あとはコールバッククラスのインスタンスを生成し、

[``FragmentActivity#getLoaderManager()``](http://developer.android.com/reference/android/support/v4/app/FragmentActivity.html#getSupportLoaderManager())で[``LoaderManager``](http://developer.android.com/reference/android/support/v4/app/LoaderManager.html)を取得し、[``LoaderManager#initLoader()``](http://developer.android.com/reference/android/support/v4/app/LoaderManager.html#initLoader(int, android.os.Bundle, android.support.v4.app.LoaderManager.LoaderCallbacks<D>))にコールバッククラスを指定するだけです。

```java:
LoaderManager.LoaderCallbacks<RequestToken> requestTokenCallbacks = new TwitterOAuthRequestTokenCallbacks(this, mTwitter);
getSupportLoaderManager().initLoader(0, null, requestTokenCallbacks);
```

[追記]上記のように2.x系の互換性のためにAndroid Support Library内の``Loader``を使用するように変更する場合は、``LoaderManager``の不一致があるので、[``Activity``](http://developer.android.com/reference/android/app/Activity.html)は継承先を[``FragmentActivity``](http://developer.android.com/reference/android/support/v4/app/FragmentActivity.html)に変更する必要があります。

## サンプルプロジェクト

この方法でアプリ連携を行うアプリのプロジェクトをGitHubに置きました。

 - [rsato/android-sample-twitter4j-auth](https://github.com/rsato/android-sample-twitter4j-auth)