今日の役に立たない一言 - Today’s Trifle! -

古い記事ではさまざまなテーマを書いていますが、2007年以降はプログラミング関連の話がほとんどです。

AndroidからGoogle OAuthでプロフィール情報にアクセスする方法

AndroidからGoogle OAuthで情報を取得するときに、いろいろとうまくいかなくて試行錯誤したので、メモっとく。
OAuthのライブラリは scribe-java を使用した。
まず、ボタンをタップしてOAuth認証を開始しようとした時に NetworkOnMainThreadException が出て怒られる。これは、UIのスレッドからネットワークにアクセスすんなって意味らしい。Runnableを作って別スレッドでアクセス。
次に結果を受け取る。callbackのURLに oauth://callback/ を指定する。
このURLでActivityを起動するように設定するとき、マニフェストがミスっててなかなか動かなかった。
マニフェストを書くと、Exported activity does not require permission とかいう警告が出てたので、それを消すために android:exported="false" ってのを追加してた。これを書くと、外部からのインテントで呼び出せなくなる。そこに気付かなくて小一時間ほど。。。
警告を消すために tools:ignore="ExportedActivity" を追加して、二つ目の Intent-filter を追加すると、onCreate()かonNewIntent()が呼び出される。

        <activity
            android:label="@string/app_name"
            android:name=".HogeActivity"
            android:screenOrientation="portrait"
            android:launchMode="singleTask"
            tools:ignore="ExportedActivity" >
            <intent-filter >
                <action android:name="android.intent.action.MAIN" />
            </intent-filter>
            <intent-filter >
                <action android:name="android.intent.action.VIEW" />
                <category android:name="android.intent.category.DEFAULT" />
                <category android:name="android.intent.category.BROWSABLE"/>
                <data android:scheme="oauth" />
            </intent-filter>
        </activity>

次に、コールバックしてきたActivityからGoogleに再度アクセスする所で例外が発生してなかなか原因がわからなかった。

org.scribe.exceptions.OAuthConnectionException: There was a problem while creating a connection to the remote service.

これを調べてみると、Evernote SDK for Androidサンプルを自分のプロジェクトで実装する時の注意点がヒット。ここでは targetSDKVersion を変更するように書かれてるけど、その原因がやっぱりUIスレッド上でのネットワークアクセス。なので、targetSDKVersionを変えなくても、別スレッドで動かせば問題ない。

というわけで、Google OAuth用にクラスを作った。
GoogleOAuth.java

public class GoogleOAuth {
	private static final String PROTECTED_GOOGLE_RESOURCE_URL = "https://www.googleapis.com/oauth2/v1/userinfo";
	private static final String SCOPE = "https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email";

	private Activity activity;

	public GoogleOAuth(Activity activity) {
		this.activity = activity;
	}

	public void sendOAuthRequest() {
		Thread t = new Thread(new OAuthRequestAction());
		t.start();
	}

	public void login(Uri uri) {
		Thread t = new Thread(new OAuthLoginAction(uri));
		t.start();
	}

	class OAuthRequestAction implements Runnable {
		@Override
		public void run() {
	    		OAuthService service = new ServiceBuilder()
			  .provider(GoogleApi.class)
			  .apiKey("anonymous")
			  .apiSecret("anonymous")
			  .callback("oauth://callback/")
			  .scope(SCOPE)
			  .build();
	    		Token requestToken = service.getRequestToken();
	    		String oauthSecret = requestToken.getSecret();
	    		// 取得した oauthSecret はどこかに覚えておく。
	    		String url = service.getAuthorizationUrl(requestToken);
	    		Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
	    		activity.startActivity(intent);
		}
	}

	class OAuthLoginAction implements Runnable {
		private Uri uri;
		public OAuthLoginAction(Uri uri) {
			this.uri = uri;
		}

		@Override
		public void run() {
			String v = uri.getQueryParameter("oauth_verifier");
			String t = uri.getQueryParameter("oauth_token");
			Verifier verifier = new Verifier(v);
	    		OAuthRequest request = new OAuthRequest(Verb.GET, PROTECTED_GOOGLE_RESOURCE_URL);
			String oauthSecret = ""; // oauthSecret を保存したところから持ってくる
	    		Token requestToken = new Token(t, oauthSecret());
	    		OAuthService service = new ServiceBuilder()
			  .provider(GoogleApi.class)
			  .apiKey("anonymous")
			  .apiSecret("anonymous")
			  .scope(SCOPE)
			  .build();
	    		try {
	    			Token accessToken = service.getAccessToken(requestToken, verifier);
	    			service.signRequest(accessToken, request);
	    		} catch (OAuthException e) {
	    			e.printStackTrace();
	    		}
	    		request.addHeader("GData-Version", "3.0");
	    		Response response = request.send();
	    		String xml = response.getBody();
	    		try {
	    			xml = URLDecoder.decode(xml, "utf-8");
	    			// xmlにはJSONでプロフィール情報などが入ってるので、あとはお好みのログイン処理を書く
	    		} catch (UnsupportedEncodingException e) {
	    			e.printStackTrace();
	    		}
	    		// ログイン後の画面に遷移
	    		Intent intent = new Intent(activity, WelcomeActivity.class);
	    		activity.startActivity(intent);
	    		activity.finish();
		}
	}
}

これを使うActivity側のコードはこんな感じ。

	// ログインボタンが押されたら動く
	@Override
	public void onClick(View v) {
		GoogleOAuth go = new GoogleOAuth(this);
		go.sendOAuthRequest();
	}

	// コールバックで呼び出される
	@Override
	public void onNewIntent(Intent intent) {
		super.onNewIntent(intent);
		Uri uri = intent.getData();
		if (uri == null) return;
		GoogleOAuth go = new GoogleOAuth(this);
		go.login(uri);
	}

oauthSecret を保存したりするのは、アプリ全体でリソースを共有するには - 今日の役に立たない一言 − Today’s Trifle! −を使うと楽だと思うし、作成中のアプリでもそうやった。