Gunosy Tech Blog

Gunosy Tech Blogは株式会社Gunosyのエンジニアが知見を共有する技術ブログです。

Android におけるアカウント管理

こんにちは。Android アプリ開発担当の黒川です。最近、久しぶりに艦これを再開したのですが、大和型も大鳳もなしに 5-5 に挑んで早くも挫けそうです。

今回は、Android アプリケーションを開発するときの、アカウント管理(そのアプリケーションのユーザーをどのように覚えておくか)について解説します。

Android アプリケーションを開発していると、利用するユーザーを覚えておきたい場合があります。たとえば、ユーザーが細かい設定を行ってから端末を変えたとします。そのときに、いままでの設定がすべて消失してしまったら、ユーザーに大変な不便を強いることになります。他にも、PC 向けのアプリケーションと Android 向けのアプリケーションの間で同期をとりたい場合も、この Android アプリケーションを使っているユーザーは誰か知りたいでしょう。

この要望を解決する方法としては以下のようなものが考えられます。

  • a) 端末固有の ID をユーザーの識別子として用いる
  • b) アプリケーションの起動時にユーザー名を入力してもらい、それをアプリケーション内に保存しておく
  • c) AccountManager を利用する

まず、a) については、ユーザーが端末を変えない限りにおいてはうまくいきますが、ユーザーが端末を変えた場合には使えません。b) については、そのアカウントが1つのアプリケーションでしか使われないなら十分な解決方法です。しかし、たとえば、同じアカウントを複数のアプリケーションで利用したい場合は、だいぶややこしいことになるでしょう。もしかすると、アプリケーションごとに同じユーザー名とパスワードを何回も入力させることになってユーザーを苛つかせる羽目になってしまうかもしれません。

実は Android には、 AccountManager という、サービスごとのユーザー情報(アカウント)を管理する仕組みがあります。これが第三の選択肢 c) です。ここで言うアカウントは、たとえば Google アカウントや Facebook アカウントのような、Android のアプリケーションに限定されない、サービスごとに用意されているアカウントのことです。

皆さんが Android をセットアップするとき、必ず Google アカウントを少なくとも1つ指定するはずです。この Google アカウントは AccountManager によって管理されています。Android に標準搭載されている Gmail アプリケーションや Google カレンダーアプリケーションは、この Google アカウントを使ってメールの送受信やカレンダーの保存などを行います。これによって、ユーザーはいちいちアプリケーションごとにメールアドレスやパスワードを聞かれることはありません。これが AccountManager を使うメリットの1つです。

試しに、お持ちの端末の「設定」> 「アカウント」という欄を見れば、どのようなアカウントが管理されているか分かるでしょう(図1)。Twitter や Facebook を使っている方は、それらのアカウントも並んでいるはずです。

図1: 設定画面のアカウント一覧 各アプリケーションは、 AccountManager

のメソッドを呼び出すことで、Google アカウントのように端末に既に登録されているアカウントを利用することができます。たとえば、Google アカウントのユーザー名でユーザーを区別したい場合などです。

あるいは、Twitter や Facebook がそうしているように自分達のサービス独自のアカウントを AccountManager で登録することも可能です。しかしながら、この独自アカウントをいざ実践してみようとすると、意外と大変なことに気付くでしょう。資料を読むと、 AbstractAccountAuthenticator を継承して Authenticator を実装しろと書いてありますが、具体的な情報が少なくて途方に暮れる方も多いのではないでしょうか。

本稿では、そのような方に向けて Android のアカウント管理の仕組みと、具体的にどのように Authenticator を実装すれば良いのかを解説したいと思います。

参考資料

本題に入る前に、今回の記事に関連する資料を紹介します。特に最初の4つの Google のドキュメントを読んだことがない方は、実装される前にざっと目を通しておくことをおすすめします。

  • Remembering Users | Android Developers : Authenticator の実装の詳細には触れられていないのですが、 AccountManager を使うメリットや、どのように使うかということが解説されています。
  • Creating a Stub Authenticator | Android Developers : SyncAdapter の利用例の一部として、単純な Authenticator の実装例が載っています。ただし、これは認証が必要ない場合のスタブ実装であって、認証が必要な場合にどのようにすべきかについては詳しく述べられていません。
  • AbstractAccountAuthenticator | Android Developers : AbstractAccountAuthenticator のJavaDocです。各メソッドの役割が説明されています。また、冒頭には、 Authenticator の実装に必要なことが詳しく説明されています。 Authenticator を実装する前に一通り目を通しておくと良いです。
  • AccountManager | Android Developers : `AccountManager* のJavaDocです。実は、アプリケーションのコードから実際にアカウントを登録したり利用する場合に使うのは、こちらのAccountManagerのみで、Authenticator のメソッドを直接呼び出すことはありません。この JavaDoc も Authenticator を実装する前に目を通しておくことで、全体像がつかみやすくなると思います。
  • Write your own Android Authenticator | udinic : Authenticator の実装方法について解説してある記事です。英語ですが、私が探した範囲では一番詳しく分かりやすかったです。

Android のアカウント管理

Android でアカウント管理を行うには、 AccountManager を利用することになります。 AccountManager を使うと、以下のようなことができます。

  • 指定したタイプのアカウントの一覧
  • アカウントの追加/削除
  • 認証トークンの保存/取得/無効化
  • パスワードの保存/更新

まずは、 Authenticator のことは忘れて、アプリケーションが AccountManager を使うときは、どのように使うかおさらいしてみましょう。アプリケーションが AccountManager を使うときのおおまかな流れは以下のようになります。

  1. AccountManager#getAccountsByType で、そのアプリケーションが利用するアカウント一覧を取得する
  2. 既存アカウントが存在するなら、それを使う。もし複数存在するならユーザーに選ばせる。その後、ステップ6に移動する
  3. 既存アカウントが存在しない場合は、ユーザーに認証情報(ユーザー名/パスワード)を入力させてアカウントを新規に作成する
  4. 入力された情報をもとにサーバーと通信して認証を行う
  5. 認証が成功したら、 AccountManager#addAccount() メソッドでアカウントを作成し、認証トークンをセットする
  6. アカウントの認証トークンを AccountManager#getAuthToken で取得する。もし存在しない場合は、ステップ4と5を行う
  7. 取得した認証トークンを用いて、サーバーと通信して何らかの処理を行う

また、ステップ6の認証トークンの取得についてさらに詳細に見ると図1のようになります。まず、アカウントを取得したら AccountManager#getAuthToken で保存されている認証トークンを取得しようとします。

このとき、2つの場合が考えられます。

1つは、認証トークンが存在しない場合です。この場合は、KEY_INTENTというパラメータが返ってきて(詳細については後で説明します)、この値で指定されたActivityが起動します。たいていは、ユーザー名とパスワードを入力するフォーム画面などになります。このActivityが無事に終わった場合は、もう一度 AccountManager#getAuthToken を呼べば、今度は正しく認証トークンが取得できるはずです。

もう1つの場合は認証トークンが存在する場合です。この値は KEY_AUTHTOKEN というパラメータで渡されます。しかし、値が返ってきたから安心とはいきません。実際にサーバーに問い合わせてみないと、その認証トークンが使えるかは分かりません。もし認証トークンが期限切れの場合は、 AccountManager#invalidateAuthToken を呼んで、 AccountManager にこの認証トークンがもう使えないことを通知して、再度 AccountManager#getAuthToken を呼ぶ必要があります。

図2: AccountManager から認証トークンを取得するときの流れ(Authenticating to OAuth2 Services | Android Developersより)

当たり前ですが、これらの操作が正しく動くためには、実際にサーバーに接続してトークンを取得する処理や、ユーザーにパスワードの入力をうながすActivityが必要になります。それでは、これらの実装は誰が行うのでしょうか?

はい。もちろん、自分で行う必要があります。開発者は AbstractAccountAuthenticator を継承した Authenticator を実装することで、上記の操作を AccountManager 経由で行えるようにします。

AccountManagerAuthenticator の関係はちょっと分かりにくいのですが、基本的にはメソッド名が同じメソッドが1対1で対応しています。例えば、 AccountManager#addAccount が呼ばれると、 Authenticator#addAccount が呼ばれる、という感じです。

Account Manager を使うメリット

では、このような苦労をしてどのようなメリットがあるのでしょうか。これについてはRemembering Users Android Developersでも議論されていますが、私が思うメリットは以下のようなものです。

  • 標準の UI を使ってアカウントの管理を行える
    • AccountManager でアカウント管理をすると、「設定」> 「アカウント」で自分のアカウントが表示されます。ここでは、さらにビューをカスタマイズすることでアカウントの追加/削除や編集が行えます。
  • SyncAdapter のようなバッググラウンドで定期的にプロセスを実行する仕組みを利用できる
    • これが大部分の動機ではないかと思います。Authenticator を実装すると、 SyncAdapter を利用して、定期的にデータを取得してストレージにキャッシュを保存することができます。また、同期のタイミングも Android OS で最適なタイミングを選ぶことで電池消費量を抑えられたりと様々なメリットも享受できます。
  • 認証トークンを利用するための機能がそろっている
    • AccountManager の良いところは、認証トークンを管理できるところです。多くのウェブサービスでは、API 実行のたびにユーザー名とパスワードを要求するのではなく、一時的な認証トークンによって認証しています。これによって、安全性を高めているのです。 AccountManager を使えば、ユーザーには一度だけパスワード入力を求め、以降は認証トークンを使って自社サービスにアクセスする、というアプリケーションを作ることができます。また、認証トークンも場合によって読取専用、フルアクセスなどの異なるタイプのトークンを取得して管理することができます。
  • 他のアプリケーションと連携することができる
    • AccountManager を利用することで、他のアプリケーションが自分のアプリケーションのアカウントにアクセスすることが可能になります。もちろん、利用されたくない場合はそのような実装も可能です。詳細については、Security Tips | Android Developers の Handling Credentialsを参照してください。

上記の要求を満たすようなアプリケーションを自分で実装を行うこともできますが、標準に合わせることで SyncAdapter などの機能を利用できます。また、ユーザーから見ればすべてのアカウントが設定画面で一覧できる方が使いやすいでしょう。それらを考えると、できるだけ AccountManager を使った方が良いのではないかと思います。

Authenticator を実装する際にやるべきこと

それでは、 AbstractAccountAuthenticator を継承して、自身の Authenticator を作りましょう。手順は以下のようになります。

  • AbstractAccountAuthenticator を継承したクラス(ここでは Authenticator と呼びます)を作成して、各メソッドをオーバーライドする
  • (必要なら)認証情報を入力するための AccountAuthenticatorActivity を継承したクラス(ここでは Authenticator Activity と呼びます)を作成する
  • Authenticator 用に Service を実装したクラス(ここでは Authenticator Service と呼びます)を作成する
  • Authenticator ServiceAndroidManifest.xml に記述する。このときに、 authenticator.xml という XML ファイル(実際のファイル名はなんでも構いません)を指定する。
  • authenticator.xml ファイルを記述する。そのアカウント用の設定などが必要な場合は、それらの画面を表す XML ファイルを指定する。
  • その設定画面を表す XML ファイルを記述する。

かなり抽象的に書いてしまいましたが、1つ1つ見ていけば、それほど難しくありません。順に見ていきましょう。

1. Authenticator の作成

Authenticator の作成する際には、 AbstractAccountAuthenticator の各メソッドをオーバーライドしていけば良いのですが、ひとまず以下の2つのメソッドを理解すれば十分です。

  • addAccount メソッド
  • getAuthToken メソッド

addAccount メソッド

addAccount メソッドは、AccountManager#addAccountが呼ばれたときに呼ばれます。ここで行うべきことは、アカウントの作成と(必要なら)作成したアカウントに認証トークンをセットすることです。アカウントの作成は、AccountManager#addAccountExplicitlyメソッドで行います。また、認証トークンはAccountManager#setAuthTokenメソッドで行います。

ここで注意しなければならないのは、addAccount メソッドは、アプリケーションがユーザーにアカウントの追加に必要な認証情報を入力してもらうときに呼ばれる、ということです。このメソッドの引数にaccountTypeは渡ってきますが、ユーザー名やパスワードは渡ってきません。ユーザーにそれら認証情報を入力してもらうのも、このメソッドの役目なのです。多くの場合、ユーザー名とパスワードを入力するフォーム画面を表示したいと考えるでしょう。そういうときは、戻り値のBundleにAccountManager#KEY_INTENTというキーで Activity を起動するIntentをセットしておきます。そうすると、Android 側で自動的に指定された Activity が起動されます。勘の良い方はお気付きだと思いますが、このActivityこそが、さきほど説明した Authenticator Activity になります。

@Override
public Bundle addAccount(AccountAuthenticatorResponse response, String accountType, String authTokenType, String[] requiredFeatures, Bundle options) throws NetworkErrorException {
  final Intent intent = new Intent(mContext, AuthenticatorActivity.class);
  
  // ここで、Authenticator Acitivity に必要な情報をセットします。
  // 作成したアカウントは response 経由で伝達するので必ずセットしておきます
  intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response);
  
  // 戻り値には KEY_INTENT が含まれていれば十分です。
  final Bundle bundle = new Bundle();
  bundle.putParcelable(AccountManager.KEY_INTENT, intent);
  return bundle;
}

また、もしユーザーが認証情報を入力する必要がない場合は、このメソッド内でAccountManager#addAccountExplicitlyメソッドでアカウントを追加し、認証トークンをセットしておきます。また、responseもしくは戻り値にKEY_ACCOUNT_NAMEとKEY_ACCOUNT_TYPEをセットします。どちらにセットするかは、非同期で処理するかどうかによります。非同期で処理する場合は戻り値にnullを指定して、非同期処理の中でresponseにセットします。

@Override
public Bundle addAccount(AccountAuthenticatorResponse response, String accountType, String authTokenType, String[] requiredFeatures, Bundle options) throws NetworkErrorException {
    final String accountName;
    final String authToken;
    final String authTokenType;
    try {
      // 何らかの手段でユーザー名と認証トークンを取得します。
      // 時間がかかる場合は非同期処理にします
      accountName = ...;
      authToken = ...;
      authTokenType = ...;
    } catch (Exception e) {
      final Bundle result = new Bundle();
      // エラーが起きた場合は、KEY_ERROR_CODE と KEY_ERROR_MESSAGE を指定します
      result.putInt(AccountManager.KEY_ERROR_CODE, ERR_INVALID_AUTH_TOKEN_TYPE);
      result.putString(AccountManager.KEY_ERROR_MESSAGE, "invalid authTokenType: "+ authTokenType);
      return result;
    }
    
    // アカウントを追加します
    final Account account = new Account(accountName, accountType);
    final AccountManager manager = AccountManager.get(this.mContext);
    manager.addAccountExplicitly(account, null, null);
    manager.setAuthToken(account, authTokenType, authToken);
    
    // 作成したアカウントの情報を返します
    final Bundle result = new Bundle();
    result.putString(AccountManager.KEY_ACCOUNT_NAME, accountName);
    result.putString(AccountManager.KEY_ACCOUNT_TYPE, accountType);
    result.putString(AccountManager.KEY_AUTHTOKEN, authToken);
    return result;
}

getAuthToken メソッド

getAuthToken メソッドは、AccountManager#getAuthTokenが呼ばれたときに呼ばれます。ここで行うべきことは、指定されたアカウントの認証トークンの取得です。認証トークンタイプも指定されているので、それに沿った認証トークンが必要になります。

@Override
public Bundle getAuthToken(AccountAuthenticatorResponse response, Account account, String authTokenType, Bundle options) throws NetworkErrorException {
  Bundle result = new Bundle();
  // 指定された認証トークンがサポートされていない、などのエラーが起きたときは、KEY_ERROR_CODE と KEY_ERROR_MESSAGE を返します。
  if (!authTokenType.equals(AUTH_TOKEN_TYPE_READ_ONLY) && !authTokenType.equals(AUTH_TOKEN_TYPE_FULL_ACCESS)) {
    result.putInt(AccountManager.KEY_ERROR_CODE, ERR_INVALID_AUTH_TOKEN_TYPE);
    result.putString(AccountManager.KEY_ERROR_MESSAGE, "invalid authTokenType: "+ authTokenType);
  } else {
    final AccountManager manager = AccountManager.get(mContext);
    final String authToken = manager.peekAuthToken(account, authTokenType);

    // 認証トークンが存在しない場合で、かつパスワードがアカウントに紐付いている場合は、それを使って認証トークンを取得しなおします
    if (authToken == null || authToken.isEmpty()) {
      final String password = manager.getPassword(account);
      if (password != null) {
      // サーバーに接続して認証トークンを取得します
        authToken = ...;
      }
    }

    // 認証トークンが存在する場合は、その情報を返します
    if (authToken != null && !authToken.isEmpty) {
      result.putString(AccountManager.KEY_ACCOUNT_NAME, account.name);
      result.putString(AccountManager.KEY_ACCOUNT_TYPE, account.type);
      result.putString(AccountManager.KEY_AUTHTOKEN, authToken);
    } else {
      // ユーザーに認証情報を入力してもらう必要がある場合は、addAccount のときと同様に Authenticator Activity を起動します
      final Intent intent = new Intent(mContext, AuthenticatorActivity.class);

      // ここで、Authenticator Acitivity に必要な情報をセットします。
      // 作成したアカウントは response 経由で伝達するので必ずセットしておきます
      intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response);

      // 戻り値には KEY_INTENT が含まれていれば十分です。
      result.putParcelable(AccountManager.KEY_INTENT, intent);
     }
  }
  return result;
}

2. Authenticator Activity の作成

この Activity は、ユーザーに認証情報を入力してもらうときに起動されます。必須ではありませんが、AccountAuthenticatorActivityを継承しておくと便利です。この Activity では、ユーザーに認証情報を入力してもらって、その情報を、起動時に渡されたAccountAuthenticatorResponseにセットすることが目的となります。

ここでは、コードをすべて載せると長大になってしまうので、例として、ユーザーがフォームに認証情報を入力し、サブミットボタンを押すような Activity を想定して、そのボタンが押されたときに実行されるコードを載せます。認証情報の入力方法はアプリケーションによって異なると思いますので、これはあくまで一例です。

// サブミットボタンが押されたら、このメソッドが呼ばれるものとします
public void submit() {
  // ユーザーが入力したユーザー名とパスワードを取得します
  final String name = ...;
  final String password = ...;

  new AsyncTask<Void, Void, Intent>() {
    @Override
    protected Intent doInBackground(Void... params) {
      // サーバーに接続してユーザーの追加を行い認証トークンを取得します
      final String authToken = ...;
      
      final Intent res = new Intent();
      res.putExtra(PARAM_NAME, name);
      res.putExtra(PARAM_AUTHTOKEN, authToken);
      res.putExtra(PARAM_PASSWORD, password);
      return res;
    }
    @Override
    protected void onPostExecute(Intent intent) {
      final String accountName = intent.getStringExtra(PARAM_NAME;
      final String accountPassword = intent.getStringExtra(PARAM_PASSWORD);
      final String authToken = intent.getStringExtra(PARAM_AUTHTOKEN);
      final Account account = new Account(accountName, ACCOUNT_TYPE);

      // なんらかの方法で、この Activity が認証トークンの再取得のためか、アカウントの新規作成のために起動されたのかを区別します
      // よくある方法は、この Activity の起動時にパラメータとして boolean を渡す方法です
      final boolean newAccount = ...;
      if (newAccount) {
        // アカウントを追加して認証トークンをセットしておきます
        // 必要なら入力されたパスワードを保存しておきます 
        manager.addAccountExplicitly(account, accountPassword, null);
        manager.setAuthToken(account, authtokenType, authToken);
      } else {
        // 既存アカウントの場合、必要なら新しく入力されたパスワードを保存しておきます 
        manager.setPassword(account, accountPassword);
      }

      final Bundle bunlde = new Bundle();
      bundle.putString(AccountManager.KEY_ACCOUNT_NAME, accountName);
      bundle.putString(AccountManager.KEY_ACCOUNT_TYPE, ACCOUNT_TYPE);
      bundle.putString(AccountManager.KEY_AUTHTOKEN, authToken);

      // AccountAuthenticatorActivity#setAccountAuthenticatorResult を呼んで結果を保存します
      setAccountAuthenticatorResult(bundle);
      setResult(RESULT_OK, intent);
      finish();
    }
  }.execute();
}

3. Authenticator Service の作成

さて、これまでで Authenticator の実装はできました。この Authenticator を使うには、Authenticator を呼び出すサービスが必要になります。幸い、Authenticator#getIBinder() というメソッドがあるので、この実装は非常に簡単です。

public class GenericAccountService extends Service {
  private MyAuthenticator mAuthenticator;
  
  @Override
  public void onCreate() {
    this.mAuthenticator = new MyAuthenticator(this);
  }

  @Override
  public void onDestroy() {
  }

  @Override
  public IBinder onBind(Intent intent) {
    return this.mAuthenticator.getIBinder();
  }

}

4. AndroidManifest.xml の記述

さきほど作成したサービスを登録します。meta-dataタグで次の節で説明する authenticator.xml を指定します。

        <service android:name="com.example.GenericAccountService" >
            <intent-filter>
                <action android:name="android.accounts.AccountAuthenticator" />
            </intent-filter>

            <meta-data
                android:name="android.accounts.AccountAuthenticator"
                android:resource="@xml/authenticator" />
        </service>

5. authenticator.xml の記述

この XML には、アカウント管理画面でどのようにアカウントを表示するのかを記述します。android:accountTypeで指定したアカウントタイプは、いままでのコードで指定されたaccountTypeと一致している必要があります。

<account-authenticator xmlns:android="http://schemas.android.com/apk/res/android"
    android:accountType="typeOfAuthenticator"
    android:icon="@drawable/icon"
    android:smallIcon="@drawable/miniIcon"
    android:label="@string/label"
    android:accountPreferences="@xml/account_preferences"
 />

また、このアカウントの設定画面が必要な場合はandroid:accountPreferencesで設定画面のレイアウトを指定します。詳細についてはAbstractAccountAuthenticator Android Developersを参照してください。

 <PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
    <PreferenceCategory android:title="@string/title_fmt" />
    <PreferenceScreen
         android:key="key1"
         android:title="@string/key1_action"
         android:summary="@string/key1_summary">
         <intent
             android:action="key1.ACTION"
             android:targetPackage="key1.package"
             android:targetClass="key1.class" />
     </PreferenceScreen>
 </PreferenceScreen>

おわりに

いかがでしたでしょうか。Authenticator の実装は、かなり長い道程となりますが、1つ1つ見ていけば、それほど複雑なことはしていません。AccountManager と Authenticator の関係をつかんでしまえば、それほど難しくないのではないかと思います。

本稿が、Android アプリケーション開発者の皆様にいくばくなりともお役に立てれば幸いです。

最後になりましたが、Gunosy では Android エンジニアを募集しております。ご興味のある方はぜひご連絡ください。