AccountManagerのgetAuthToken()をReflectionでヤッてみた
AccountManager#getAuthToken()を利用することでAndroid端末に登録したアカウント情報のTokenが取得できます。普通の使い方はこちらあたりに詳しく載っています。
これを利用することで例えばGoogle Serviceへのアクセス時認証に利用できます。取得したTokenをClientLoginの際のAuthに設定してやればOKです。
と、普通はこれで話が済んでしまうのですが、AccountManagerはAPI Level 5以降じゃないと利用できない。これより古い環境でこのコードを実行するとjava.lang.VerifyErrorが起きてしまいます。
これは後方互換性の問題です。Androidアプリを開発する際に気をつけなければいけないのがこの問題。詳しい対処方法はこちらに掲載されています。これらの対処方法のうち、今回はReflectionを利用することで回避してみました。当然のことながら、Reflectionを使ったからといって古い環境でAccountManagerの機能が使えるようになるわけではありません。実行時のVerifyErrorが回避されるだけです。同等のことを古い環境でやる場合は、Google Data APIのClientLoginなりをHTTPリクエスト送ってやればOKです。Java(Android)向けのライブラリも公開されているのでそれを使っても良いと思います。まぁそれはそれで置いといて。
今回主題のやり方はこちら。
InvocationHandlerとかProxy.newProxyInstance()とか初めて使ったでござる。
こんなのあったら何でもアリじゃねぇ?
いじょ。
これを利用することで例えばGoogle Serviceへのアクセス時認証に利用できます。取得したTokenをClientLoginの際のAuthに設定してやればOKです。
と、普通はこれで話が済んでしまうのですが、AccountManagerはAPI Level 5以降じゃないと利用できない。これより古い環境でこのコードを実行するとjava.lang.VerifyErrorが起きてしまいます。
これは後方互換性の問題です。Androidアプリを開発する際に気をつけなければいけないのがこの問題。詳しい対処方法はこちらに掲載されています。これらの対処方法のうち、今回はReflectionを利用することで回避してみました。当然のことながら、Reflectionを使ったからといって古い環境でAccountManagerの機能が使えるようになるわけではありません。実行時のVerifyErrorが回避されるだけです。同等のことを古い環境でやる場合は、Google Data APIのClientLoginなりをHTTPリクエスト送ってやればOKです。Java(Android)向けのライブラリも公開されているのでそれを使っても良いと思います。まぁそれはそれで置いといて。
今回主題のやり方はこちら。
public class AuthenticatorAfterForoyo extends AuthenticatorBase {
private static final String ACCOUNT_TYPE_GOOGLE = "com.google";
/**
* コンストラクタ
* @param context
*/
public AuthenticatorAfterForoyo(Context context)
{
super(context);
}
/**
* トークンの取得
* @param account
* @param passwd 不使用
* @param service
*/
public void getAuthToken(String acc, String passwd, String service)
{
try {
// AccountManagerクラス取得
Class accountManager = Class.forName("android.accounts.AccountManager");
// Accountクラス取得
Class account = Class.forName("android.accounts.Account");
// AccountManagerCallbackのクラス取得
Class accountManagerCallback = Class.forName("android.accounts.AccountManagerCallback");
InvocationHandler handler = new MyInvocationHandler();
@SuppressWarnings("rawtypes")
Class[] proxyInterfaces = new Class[] { accountManagerCallback };
Object accountManagerCallbackInstance = Proxy.newProxyInstance(
accountManagerCallback.getClassLoader(),
proxyInterfaces,
handler);
if ( accountManagerCallbackInstance == null )
{
Log.w(Define.TAG, "accountManagerCallbackInstance is null");
return;
}
// getメソッド
Method get = accountManager.getMethod("get", Context.class);
if ( get == null )
{
Log.w(Define.TAG, "get is null");
return;
}
// AccountManagerクラスインスタンス取得
Object accountManagerInstance = (Object)get.invoke(accountManager, context);
if ( accountManagerInstance == null )
{
Log.w(Define.TAG, "accountManagerInstance is null");
return;
}
// AccountManager.getAuthToken()メソッド
Method getAuthToken = null;
Method[] accountManagerMethods = accountManager.getMethods();
for ( int i = 0; i < accountManagerMethods.length; i++ )
{
Method method = accountManagerMethods[i];
if ( method.getName().equals("getAuthToken") )
{
// 引数の数とタイプが一致する関数を取得
// TODO もうちょっとスマートな解決方法はないものか?
// APILevel4以前の環境では引数の型特定にAccountだのAccountManagerCallbackだのを利用できないため、
// こんな無様な方法で関数を取得している。
Class[] types = method.getParameterTypes();
if ( types.length != 6 )
break;
if ( !types[0].getName().equals("android.accounts.Account") )
break;
if ( !types[1].getName().equals("java.lang.String") )
break;
if ( !types[2].getName().equals("android.os.Bundle") )
break;
if ( !types[3].getName().equals("android.app.Activity") )
break;
if ( !types[4].getName().equals("android.accounts.AccountManagerCallback") )
break;
if ( !types[5].getName().equals("android.os.Handler") )
break;
//Log.d("TAG", "Found getAuthToken() method");
getAuthToken = method;
}
}
if ( getAuthToken == null )
{
Log.w(Define.TAG, "getAuthToken is null");
return;
}
// AccountManager.getAccountsByType()メソッド取得
Method getAccountsByType = accountManager.getMethod("getAccountsByType", String.class);
if ( getAccountsByType == null )
{
Log.w(Define.TAG, "getAccountsByType is null");
return;
}
// Accountクラスインスタンス取得
Object[] accountObject = (Object[])getAccountsByType.invoke(accountManagerInstance, ACCOUNT_TYPE_GOOGLE);
java.lang.reflect.Field name = account.getField("name");
if ( name == null )
{
Log.w(Define.TAG, "name is null");
return;
}
for ( Object ac : accountObject )
{
if ( name.get(ac).toString().equals(acc) )
{
//Log.d(Define.TAG, "getAuthToken.invoke() name : " + name.get(ac).toString());
getAuthToken.invoke(
accountManagerInstance,
ac,
service,
null,
context,
accountManagerCallbackInstance,
null);
return;
}
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (SecurityException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalArgumentException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
}
}
class MyInvocationHandler implements InvocationHandler {
@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
try {
//Log.d(Define.TAG, "MyInvocationHandler#invoke() method : " + method.getName() + ", args.length : " + args.length);
if ( method.getName().equals("run") && args.length == 1)
{
Class accountManagerFeature = Class.forName("android.accounts.AccountManagerFuture");
Method getResult = accountManagerFeature.getMethod("getResult", null);
if ( getResult == null )
return null;
Bundle bundle = (Bundle)getResult.invoke(args[0], null);
String token = bundle.getString("authtoken");
listener.onGotToken(token);
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
return null;
}
}
InvocationHandlerとかProxy.newProxyInstance()とか初めて使ったでござる。
こんなのあったら何でもアリじゃねぇ?
いじょ。
コメント
コメントを投稿