AndroidでTwitterクライアントを作成(OAutn認証、PINコードを自動取得)

AndroidTwitterクライアントアプリを開発する際、Twitterの認証後にブラウザ上に表示される
PINコードをAndroid側に知らせる必要があります。
その際、表示されたPINコードをEditTextに入力してもらうのは面倒なので
ブラウザ上のHTMLから自動取得するサンプルを作ってみました。
カスタムURLスキームのコールバックURLを指定する方法でも実現可能かと思われますが、
こちらは本記事の最後に補足として記述しています。


AndroidのブラウザコンポーネントであるWebViewには、タイトルを取得するメソッドはありますが
表示されているページのHTMLソースを取得するメソッドはありません。
今回、HTMLソースをAndroid側へ渡すための方法として
WebChromeClient#onJsAlert()メソッドを利用しました。
これはjavascriptのalertをAndroid側でハンドリングするための仕組みですが
当該メソッドをオーバーライドすることによりHTMLソースを取得する手段として利用することができます。


ちょと長いですがサンプルソース。Twitter4jを使用しています。

package sample.twitter.oauth;

import org.apache.commons.lang3.StringUtils;
import twitter4j.Twitter;
import twitter4j.TwitterException;
import twitter4j.TwitterFactory;
import twitter4j.auth.RequestToken;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.ProgressDialog;
import android.content.DialogInterface;
import android.graphics.Bitmap;
import android.os.Bundle;
import android.util.Log;
import android.webkit.JsResult;
import android.webkit.WebChromeClient;
import android.webkit.WebView;
import android.webkit.WebViewClient;

public class TwitterOAuthSampleActivity extends Activity {
	private WebView webView;

	@Override
	public void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.main);

		Twitter twitter = new TwitterFactory().getInstance();
		RequestToken requestToken = null;
		try {
			requestToken = twitter.getOAuthRequestToken();
		} catch (TwitterException e) {
			Log.e("Exception", "", e);
		}

		webView = (WebView) findViewById(R.id.webView);
		webView.setWebViewClient(new CustomWebViewClient());
		webView.getSettings().setJavaScriptEnabled(true);
		webView.loadUrl(requestToken.getAuthenticationURL());
		webView.setWebChromeClient(new CustomWebChromeClient());

	}

	private class CustomWebViewClient extends WebViewClient {
		private ProgressDialog progress;

		public CustomWebViewClient() {
		}

		@Override
		public boolean shouldOverrideUrlLoading(WebView view, String url) {
			view.loadUrl(url);
			return true;
		}

		@Override
		public void onPageStarted(WebView view, String url, Bitmap favicon) {
			// 格好をつけるためにスピナー型プログレスダイアログを表示
			if (progress == null) {
				progress = new ProgressDialog(view.getContext());
				progress.setMessage("通信中");
				progress.setProgressStyle(ProgressDialog.STYLE_SPINNER);
			}
			progress.show();
		}

		@Override
		public void onPageFinished(WebView view, String url) {
			progress.hide();

			// HTMLソース上のpinコードを取得するためのJavaScript
			String script = "javascript:var elem = document.getElementsByTagName('code')[0]; if(elem) alert(elem.childNodes[0].nodeValue);";
			view.loadUrl(script);
		}

	}

	// JavaScript:alertをAndroid側でハンドリングするための仕組みである
	// WebChromeClient#onJsAlert()メソッドをオーバーライドして
	private class CustomWebChromeClient extends WebChromeClient {

		@Override
		public boolean onJsAlert(WebView view, String url, String message,
				JsResult result) {
			// とりあえずAlertDialogでPINコードを表示し、ボタン押下によりアプリ終了
			if (StringUtils.isNotEmpty(message)) {
				AlertDialog.Builder builder = new AlertDialog.Builder(
						view.getContext());
				builder.setMessage(message)
						.setTitle("pin")
						.setPositiveButton("OK",
								new DialogInterface.OnClickListener() {

									@Override
									public void onClick(DialogInterface dialog,
											int which) {
										Activity activity = (Activity) getCurrentFocus()
												.getContext();
										activity.finish();
									}
								});

				AlertDialog dialog = builder.create();
				dialog.show();
			}

			result.confirm();

			// アラートダイアログをこのメソッド内で処理したか
			// falseを返すとAndroid APIによるデフォルトのアラートダイアログが表示されるため、今回はtrueを返す
			return true;
		}
	}
}

※追記
上記コードではProgressDialogのインスタンスを使いまわしてページ読み込みの度にshow()とhide()を読んでいますが
このままだとActivityをfinish()した際にExceptionが発生してしまいます。
finish()の前に、ProgressDialog#dismiss()を呼び出す必要があります。


layout/main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical" >

    <WebView
        android:id="@+id/webView"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent" />

</LinearLayout>

WebViewClientとWebChromeClientをそれぞれ拡張し必要な処理を追記したものをWebViewオブジェクトに設定しています。

HTMLを解析しPINコードを取得

現状、Twitter認証後のPINコード表示箇所のHTMLは以下のようになっています。

<div id="oauth_pin">
  <p>
    <span id="code-desc">val90 devに戻り、次のPINを入力して認証を完了してください。</span>
    <kbd aria-labelledby="code-desc"><code>xxxxxxx</code></kbd>
  </p>
</div>

codeタグ内にPINコードが記述されています。
よって、codeタグ要素の内容をAndroid側で取得するようにします。
URLからJavaScriptを実行する仕組みを利用します。
ページ読み込み完了時、HTML内にcode要素があるかを調べ、code要素が存在した場合その内容をalert関数に渡します。

// HTMLソース上のpinコードを取得するためのJavaScript
String script = "javascript:var elem = document.getElementsByTagName('code')[0]; if(elem) alert(elem.childNodes[0].nodeValue);";
view.loadUrl(script);


上記JavaScriptでcodeタグが見つかりalert関数が呼ばれると、Android側のWebChromeClient#onJsAlertメソッドに制御が移ります。
第三引数のStringオブジェクトmessage変数に、JavaScriptのalert関数に渡された値が設定されています。
今回のサンプルではAlertDialogでPINコードを表示するのみでアプリケーションを終了していますが、
ここにAccessTokenの取得、保存などの処理を実装すれば良いでしょう。

@Override
public boolean onJsAlert(WebView view, String url, String message,
		JsResult result) {

	// とりあえずAlertDialogでPINコードを表示し、ボタン押下によりアプリ終了
	if (StringUtils.isNotEmpty(message)) {


〜 略 〜
	

	}
	
	result.confirm();

	// アラートダイアログをこのメソッド内で処理したか
	// falseを返すとAndroid APIによるデフォルトのアラートダイアログが表示されるため、今回はtrueを返す
	return true;
}

注意点としては、万一Twitter認証画面のHTMLソースが変更され、codeタグ内にPINコードが記述されなくなるような
事があった場合はAndroidアプリ側も改修する必要があります。




※補足
今回はHTMLソース上のPINコードを自動取得するための参考を示しましたが、
Twitter認証の自動化(PINコード入力の煩わしさを解消する)という目的であれば
カスタムURLスキームを用いる方法でも実現できるかと思います。
たとえば、AndroidManifest.xmlファイルのintent-filterにおいて


などと設定し、Twitter認証時のコールバックURLを以下のように指定すればアプリケーション側で
oauth_verifierを取得することができるのではと思われます。
・当方では未確認
・コールバックを使用する場合はTwitter側でアプリの管理画面でコールバックURLに何らかのURLを指定する必要がある
・管理画面でcallback urlを入力しておかないと、oauth_verifierが渡ってこない。

mytwitterclient://oauth/callback

参考
http://developer.android.com/guide/topics/manifest/data-element.html