Account Managerについて

Android SDKのサンプルSampleSyncAdapterを読んでみたのだが、結構初心者には難解なコードだった。SampleSyncAdapter自身はAndroidが提供する2つの機能を使って実現している。1つはインターネット上のサービスなどにログインするアカウントの管理機能を提供するAccount Managerと、もう一つはデータの同期を実現するSynchronization Manager。サンプルプログラムとしてはこれらの2つの機能を組み合わせた方が現実的なサンプルになるのだが、両方の機能の詳細を理解していない初心者には難解になってしまう。

補足:2010/10/17
このサンプルがとっつき難い理由として、どうやって動かすか解らない、という面もあった。 SampleSyncAdapterの動かし方については“Android SDKのSampleSyncAdapterの使い方”に詳しく書いておいた。

そこで、2つの機能を切り分けて別々に独自のサンプルを作ってみることにした。先ずは、Account Managerから。

Account ManagerはAndroid端末内でユーザの持つオンライン・アカウントを一元的に管理する機能。ユーザは一度IDとパスワードを入力すれば、以降はイチイチ、ID・パスワードを入力することなく、ワンクリックでアクセスできるようになる。

勿論、サービスが異なれば認証の仕方も異なるので、AccountManagerにはauthenticatorモジュールをプラグインできるようになっている。サービスごとのauthenticatorを追加することでGoogleだけでなくFacebookMicrosoft Exchangeのサービスにも対応する。

また、一般に多くのサービスで“認証トークン”を扱えるが、Account Managerでもトークンを生成・管理できる。トークンはサービスへアクセスするたびに実際のパスワードを送信せず、(最初の認証の後は)一定期間有効なトークンを送ることで、認証できるようにする。ただし、トークンを無効にするタイミングはサービスに任せられる。

サービスにアクセスするアプリケーションは一般に次のような手順を踏む(Androidの開発ページより):

  1. AccountManager.get(Context)メソッドを使ってインスタンスを獲得する。
  2. getAccountsByType(String)などを使って利用可能なアカウントのリストを獲得する。特定のアプリケーションが必要とするのは通常は特定のタイプのアカウントに限定される。この“アカウントタイプ”とはauthenticatorを識別する。例えば、Googleのサービスであれば“com.google”というアカウントタイプとしてauthenticatorを指定する。アカウントタイプはauthenticatorに依存し、アプリケーションは利用するauthenticatorのアカウントタイプを知っていなければならない。(アカウントタイプの他にアカウントフィーチャもあるが、初歩的な利用ということでここでは割愛した。)
  3. getAccountsByType(String)などを使って取得したアカウントのリストは通常、ユーザに提示して、その一つを選択してもらう。もし利用したいアカウントがリストに無ければ、addAccountメソッドを使ってユーザに新たにアカウントを作るように促す。
  4. 重要:前回の選択したアカウントをアプリケーションが再利用する際には、そのアカウントがgetAccountsByTypeメソッドで返されるリストにまた存在していることを確認しなければならない。既にデバイスに存在しないアカウントを使うと未定義エラーとなる。
  5. getAuthTokenメソッドや関連ヘルパを使って、選択したアカウントの認証トークンを要求する。
  6. 認証トークンと使ってサービスを要求する。認証トークンの形式、サービス要求のフォーマット、プロトコルなどはアクセスしようとしているサービスに依存する。アプリケーションはネットワークやプロトコルのライブラリを利用することになるだろう。
  7. 重要:認証エラーでサービス要求が失敗した場合は、入手したトークンは期限切れであり、サーバではもう受け入れられない可能性がある。アプリケーションはinvalidateAuthTokenメソッドを呼び出してキャッシュされたトークンを取り除かなければならない。それを行わないとサービス要求は失敗し続けることになる。現在もっている認証トークンを無効化した後に、上の“認証トークンの要求”を再実行する。それでも認証が失敗する場合は、“本格的な”認証の失敗として、ユーザに取るべきアクションを訪ねることになる。

Account Managerを使ったサンプル

いきなりSDKのサンプル、SampleSyncAdapterのようなことをやってもわからないので、次のような非常に単純なサンプルを作ってみた。このサンプルでやりたいことは次のようなことである。

  1. googleアカウントの情報を使って、Googleカレンダへアクセスするためのトークンを取得する。
  2. 取得した認証トークンを使ってGoogleアカウントにアクセスする。
  3. (認証が通れば)ログインした状態でGoogleカレンダーのWebページをGETする。

これを実現するために次のような手順を踏んだ:

  1. 前準備:Googleアカウント(Gmailのアカウント)を取得する。そして、そのID(Gmailアドレス)とパスワードをAndroidバイスの[設定]−[アカウントと同期]からデバイスGoogleアカウントを登録する。以上を設定してからアンプルアプリケーションを起動する。
  2. サンプルアプリケーションでは、アカウントマネージャのインスタンスを獲得(AccountManager.get(this))する。
  3. Googleサービスに関連したアカウントのリストを取得する(getAccountsByType("com.google"))。本来でここで取得したリストを画面に表示して、どのアカウント(Gmailアドレス)を使うかユーザが選択するが、ここでは1つのアカウントしか登録していないので、決め打ちでリストの先頭のアカウントを使う(accounts[0])。
  4. アカウントの情報と“認証トークンタイプ”としてGoogleカレンダーを表す“cl”を使って、認証トークンを取得する(getAuthToken(accounts[0], "cl", false, new GetAuthTokenCallback(), null))。このメソッドは非同期のメソッドなので、トークンを取得した時に呼び出されるCallbackを指定している。Callbackを指定せずに、getAuthTokenの直後にgetResultメソッドを呼び出せばブロック型の利用ができるらしい。またblockingGetAuthTokenメソッドを使ってもブロック型でトークンを獲得できる。(ブロック型=トークンが獲得できるまでメソッドからは制御が返ってこない。)
  5. トークンが取得できると、Callback(GetAuthTokenCallbackインスタンスのrunメソッドが呼び出されるので、そこで得られたトークンを使ってGoogleアカウントを呼び出すURLを生成して、HttpGetする。

基本は押さえたと思ったのだが、上手く行かない。認証トークンを取得するところまでは上手く行くのだが、そのトークンを使ってGoogle Accountへアクセスすると、“The page you requested is invalid(指定されたページは無効です)”画面が返って来てしまう。トークンの入手方法に問題があるのか(=トークン自身が無効なのか)、トークンを使ったGoogle Accountへのアクセスに問題あるのか。色々とやってみたが2日程進展がないので、一旦、ここまでの成果をメモっておくこととした。(このサンプルを作るにあたって次のWebページを参考にした:http://blog.notdot.net/2010/05/Authenticating-against-App-Engine-from-an-Android-app

なお、getAuthTokenメソッドの引数、authTokenTypeについて一言書いておく。authTokenTypeはその名前の通り認証トークンの型を指定する。authTokenTypeはauthenticator依存、つまり認証の種類ごとにことなる。この例ではGoogleアカウントサービスのauthenticatorに対して、“cl”型を指定しているが、これはgoogleカレンダーとして認識されるようだ。色々と試してみると、次のような文字列がGoogleアカウントのauthenticatorでは次のような文字列を認識していると思われる。(Androidのauthenticatorでこれらの文字列を認識していることは分かるのだが、これらを使って実際にサービスにアクセスするところまでは至っていない。)

サンプルプログラム

メイン・アクティビティ:

package com.example.android.test_accountmanager_1;

import java.io.IOException;

import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.util.EntityUtils;

import android.accounts.Account;
import android.accounts.AccountManager;
import android.accounts.AccountManagerCallback;
import android.accounts.AccountManagerFuture;
import android.accounts.AuthenticatorException;
import android.accounts.OperationCanceledException;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;

public class TestAccountManager1 extends Activity {
	
	static final String TAG="+++ TestAccountManager1";
	AccountManager mAccountManager = null;

	/** Called when the activity is first created. */
	@Override
	public void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.main);
	}

	@Override
	protected void onResume() {
		super.onResume();
        
		Account[] accounts = null;
		if (mAccountManager == null) mAccountManager = AccountManager.get(this);
		accounts = mAccountManager.getAccountsByType("com.google");
		for (Account ac : accounts) Log.d(TAG, ac.toString());
		AccountManagerFuture<Bundle> accountManagerFuture = 
			mAccountManager.getAuthToken(accounts[0], 
        			"cl", 
//					"android", 
//					"ah",		// Google AppEngine 
//					"cl", 		// Google Calendar
//					"mail", 	// Gmail
//					"reader", 	// Google Reader
//					"talk", 	// Gtalk
//					"youtube",	// YouTube 
					false, new GetAuthTokenCallback(), null);

/* 以下は同期型(ブロック型)のgetAuthTokenを使った例。
		try {
			token = mAccountManager.blockingGetAuthToken(accounts[0], "cl", false);
		} catch (OperationCanceledException e) {
			e.printStackTrace();
		} catch (AuthenticatorException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}
		Log.d(TAG, "Token = " + token);
*/
	}
    
	private class GetAuthTokenCallback implements AccountManagerCallback<Bundle> {

		static final String TAG="+++ GetAuthTokenCallback";
		@Override
		public void run(AccountManagerFuture<Bundle> arg0) {
			Bundle bundle;
			try {
				bundle = arg0.getResult();
				Intent intent = (Intent) bundle.get(AccountManager.KEY_INTENT);
				if (intent != null) {
					Log.d(TAG, "User Input required");
					startActivity(intent);
				} else {
					Log.d(TAG, "Token = " + bundle.getString(AccountManager.KEY_AUTHTOKEN));
					loginGoogle(bundle.getString(AccountManager.KEY_AUTHTOKEN));
				}
			} catch (OperationCanceledException e) {
				e.printStackTrace();
			} catch (AuthenticatorException e) {
				e.printStackTrace();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
    	
		private void loginGoogle(String token) {
			DefaultHttpClient http_client = new DefaultHttpClient();
			HttpGet http_get = new HttpGet(
					// TokenAuthの他にも Login、ServiceLgoin、ClientLoginがあるがどれもNG
					"https://www.google.com/accounts/TokenAuth?auth=" 
					+ token
					+ "&continue=http://www.google.com/calendar/"
					);
			HttpResponse response = null;
			try {
				response = http_client.execute(http_get);
			} catch (ClientProtocolException e) {
				e.printStackTrace();
			} catch (IOException e) {
				e.printStackTrace();
			}
			if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
				try {
					String entity = EntityUtils.toString(response.getEntity());
					Log.d(TAG, entity);
					if (entity.contains("The page you requested is invalid")) {
						Log.d(TAG, "The page you requested is invalid");
						mAccountManager.invalidateAuthToken("com.google", token);
					}
				} catch (IllegalStateException e) {
					e.printStackTrace();
				} catch (IOException e) {
					e.printStackTrace();
				}
			} else
				Log.d(TAG, "Login failure");
		}
	}
}

マニフェスト(uses-permissionの追加が必要):

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
	package="com.example.android.test_accountmanager_1"
	android:versionCode="1" android:versionName="1.0">
	<uses-permission android:name="android.permission.GET_ACCOUNTS" />
	<uses-permission android:name="android.permission.USE_CREDENTIALS"/>
	<uses-permission android:name="android.permission.INTERNET"/>
	<uses-permission android:name="android.permission.MANAGE_ACCOUNTS"/>
	<application android:icon="@drawable/icon" android:label="@string/app_name">
		<activity android:name=".TestAccountManager1" android:label="@string/app_name">
			<intent-filter>
				<action android:name="android.intent.action.MAIN" />
				<category android:name="android.intent.category.LAUNCHER" />
			</intent-filter>
		</activity>
	</application>
</manifest> 

【追記:2011/03/25】
tknringさんのご指摘で android.permission.MANAGE_ACCOUNTS を追加。