読者です 読者をやめる 読者になる 読者になる

Pocket (Formerly Read It Later) のOAuthログインサンプルを作りました

全てのソースコードは下記より
https://github.com/noboru-i/ReadItNow/tree/pocket-login-sample

下記のように取得できると思います。

git clone https://github.com/noboru-i/ReadItNow.git
cd ReadItNow
git checkout -b pocket-login-sample refs/tags/pocket-login-sample

取得後、
/res/values/common_strings.xml

<string name="pocket_api_key" translatable="false">10633-60f45438c2b0e5f6a98387fe</string>

のようにAPIキーを設定します。

基本的に、
http://getpocket.com/developer/docs/authentication
を実装しただけです。

APIキーの取得

http://getpocket.com/developer/
より、「CREATE NEW APP」を押下。

  • Application Name

アプリケーション名を指定します。
http://getpocket.com/developer/docs/branding にあるように、「Pocket」・「Read It Later」は利用できないようです。

  • Application Description

説明文を指定します。
認証画面で表示されます。

  • Permissions

参照だけであれば、「Retrieve」だけでよいと思います。

  • Platforms

Androidアプリであれば、「Android - Mobile」だけでよいと思います。

上記を入力し、「CREATE APPLICATION」をクリックすると、CONSUMER KEYが表示されます。

更新時には下記を追加で指定できます。

  • URL

play storeのURLなどでよいかと思います。紹介ページがあればそちらを。

  • Application Icon

アイコンを指定します。
認証画面などで表示されます。

  • Categories

カテゴリを指定します。
http://getpocket.com/apps/
に表示される際に利用されるようです。

認証を解除する

ブラウザより
http://getpocket.com/connected_accounts
にアクセスし、「Remove access」をタップ

処理フロー

まず、MainActivity#onCreateが呼び出されます。
ランチャーから起動された場合、intent.getAction()には"android.intent.action.MAIN"が入っていることになります。
なので、36行目のif文の中には入らずに72行目のtaskを起動します。
getRequestTokenで、request tokenを取得し、preferenceに保存します。
startOauthActivityで、外部ブラウザを起動し、認証してもらいます。
認証から戻ってきた場合、36行目のif文に入ります。
preferenceよりrequest tokenを取得し、43行目のtaskを起動します。
getAccessTokenで、access tokenを取得する。

/src/hm/orz/chaos114/android/readitnow/MainActivity.java
https://github.com/noboru-i/ReadItNow/blob/pocket-login-sample/src/hm/orz/chaos114/android/readitnow/MainActivity.java

package hm.orz.chaos114.android.readitnow;

import java.io.IOException;
import java.io.UnsupportedEncodingException;

import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.protocol.HTTP;
import org.apache.http.util.EntityUtils;
import org.json.JSONObject;

import android.app.Activity;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.util.Log;
import android.view.Menu;
import android.widget.TextView;
import android.widget.Toast;

public class MainActivity extends Activity {
	private static final String TAG = MainActivity.class.getSimpleName();

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

		Intent intent = getIntent();
		if (Intent.ACTION_VIEW.equals(intent.getAction())) {
			Uri data = intent.getData();
			if (data != null && "authorizationFinished".equals(data.getEncodedAuthority())) {
				SharedPreferences preferences = PreferenceManager
						.getDefaultSharedPreferences(getApplicationContext());
				final String requestToken = preferences.getString(
						"REQUEST_TOKEN", null);
				AsyncTask<Void, Void, Boolean> task = new AsyncTask<Void, Void, Boolean>() {
					@Override
					protected Boolean doInBackground(Void... params) {
						try {
							getAccessToken(requestToken);
						} catch (Exception e) {
							return false;
						}
						return true;
					}
					@Override
					protected void onPostExecute(Boolean result) {
						if (!result) {
							// 認証失敗
							Toast.makeText(MainActivity.this, "認証に失敗しました", Toast.LENGTH_LONG).show();
							return;
						}
						SharedPreferences preferences = PreferenceManager
								.getDefaultSharedPreferences(getApplicationContext());
						String username = preferences.getString("USERNAME", null);
						TextView view = (TextView)MainActivity.this.findViewById(R.id.main_test);
						view.setText("Hello " + username);
					}
				};
				task.execute((Void) null);
				return;
			}
		}

		AsyncTask<Void, Void, Void> task = new AsyncTask<Void, Void, Void>() {
			@Override
			protected Void doInBackground(Void... params) {
				// request tokenを取得する
				String requestToken = getRequestToken();
				SharedPreferences preferences = PreferenceManager
						.getDefaultSharedPreferences(getApplicationContext());
				Editor editor = preferences.edit();
				editor.putString("REQUEST_TOKEN", requestToken);
				editor.commit();

				// 認証画面を表示する
				startOauthActivity(requestToken);
				return null;
			}
		};
		task.execute((Void) null);
	}

	private String getRequestToken() {
		String url = getString(R.string.url_v3_request);
		JSONObject param = new JSONObject();
		try {
			param.put("consumer_key", getString(R.string.pocket_api_key));
			param.put("redirect_uri", "readitnow://authorizationFinished");
			String response = postJson(url, param);
			JSONObject respJson = new JSONObject(response);
			String requestToken = respJson.getString("code");
			return requestToken;
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}

	private void startOauthActivity(String requestToken) {
		String url = getString(R.string.url_authrize);
		Uri.Builder builder = new Uri.Builder();
		builder.appendQueryParameter("request_token", requestToken);
		builder.appendQueryParameter("redirect_uri",
				"readitnow://authorizationFinished");
		String queryString = builder.build().toString();
		url = getString(R.string.url_authrize);
		Intent intent = new Intent();
		intent.setAction(Intent.ACTION_VIEW);
		intent.setData(Uri.parse(url + queryString));
		startActivity(intent);
	}

	private void getAccessToken(String requestToken) {
		String url = getString(R.string.url_v3_authrize);
		JSONObject param = new JSONObject();
		try {
			param.put("consumer_key", getString(R.string.pocket_api_key));
			param.put("code", requestToken);
			String response = postJson(url, param);
			JSONObject respJson = new JSONObject(response);
			String accessToken = respJson.getString("access_token");
			String username = respJson.getString("username");

			SharedPreferences preferences = PreferenceManager
					.getDefaultSharedPreferences(getApplicationContext());
			Editor editor = preferences.edit();
			editor.putString("USERNAME", username);
			editor.putString("ACCESS_TOKEN", accessToken);
			editor.commit();
			Log.d(TAG, "accessToken = " + accessToken);
			Log.d(TAG, "username = " + username);
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}

	@Override
	public boolean onCreateOptionsMenu(Menu menu) {
		getMenuInflater().inflate(R.menu.activity_main, menu);
		return true;
	}

	/**
	 * APサーバにPOSTリクエストを発行する。
	 * 
	 * @param endpoint リクエストURL
	 * @param params リクエストパラメータ
	 * @return レスポンス文字列
	 * @throws IOException 通信例外
	 */
	private static String postJson(final String endpoint,
			final JSONObject params) throws IOException {
		Log.i(TAG, "endpoint = " + endpoint);
		DefaultHttpClient httpClient = new DefaultHttpClient();
		HttpPost httpPost = new HttpPost(endpoint);

		try {
			httpPost.setHeader(HTTP.CONTENT_TYPE, "application/json");
			httpPost.setHeader("X-Accept", "application/json");
			StringEntity se = new StringEntity(params.toString());
			httpPost.setEntity(se);
		} catch (UnsupportedEncodingException e1) {
			e1.printStackTrace();
		}
		HttpResponse response = httpClient.execute(httpPost);
		String responseString = EntityUtils.toString(response.getEntity());
		Log.i(TAG, "response = " + responseString);
		int statusCode = response.getStatusLine().getStatusCode();
		if (statusCode != 200) {
			throw new IOException("Post failed. statusCode=" + statusCode);
		}

		return responseString;
	}
}

/res/values/url_strings.xml
https://github.com/noboru-i/ReadItNow/blob/pocket-login-sample/res/values/url_strings.xml

<resources>

    <string name="url_v3_request" translatable="false">https://getpocket.com/v3/oauth/request</string>
    <string name="url_authrize" translatable="false">https://getpocket.com/auth/authorize</string>
    <string name="url_v3_authrize" translatable="false">https://getpocket.com/v3/oauth/authorize</string>

</resources>