はてなブログはじめた

Markdownに慣れきってしまうとBloggerで日記を書くのが苦痛になったのではてなブログを始めた。 とりあえずQiitaに書いた記事も転記してみたが、体裁が一部おかしいかもしれない。 Qiitaに投げるまでもない小ネタとかはこっちにまとめていきたい。

コンゴトモヨロシク。

買ってよかったAndroid関連電子書籍まとめ

Androidの技術書、どうせ買うなら電子書籍がいいよね!と電子書籍をあちこちで買ったり買い直したりしてるので知見を共有します。

どこで買うべきか

電子書籍で技術書を購入する際、なるべく制限の少ないものを探すことが重要です。 何も考えずに買ってしまうと、以下の様な制限に引っかかってしまいます。

  • PC/Macで閲覧できない
  • 全文検索ができない
  • 文字列コピーができない
  • 印刷ができない

技術書に限っては閲覧制限のかかっていないPDFを入手できることが最善だと思います。

達人出版会

閲覧制限のないPDFを入手することができます。 ここで購入できる書籍はなるべくここで購入しています。 領収書が発行されないので、必要な人は注意してください。 http://tatsu-zine.com/

技術評論社

閲覧制限のないPDFを入手することができます。 Android本もそこそこあって良いです。 AdobeReaderでコピーすると文字化けするPDFがあるので注意。 https://gihyo.jp/dp

O'Reilly Japan Ebook Store

閲覧制限のないPDFを入手することができます。 ただし、Android本があまりありません…。 1年以内に発売されたAndroid本が「実践 Android Developer Tools」しかない…。 http://www.oreilly.co.jp/ebook/

Impress Japan

あまり使ったことがないですが、書籍によって提供されるフォーマットがまちまちです。 PDFもコピペ制限があったりなかったりで、大抵の場合買うまでわかりません。 http://www.impressjapan.jp/

Google Play ブックス

PCでもブラウザで読めますが、文字列コピーと印刷ができません。 ソースコードを検索した際の精度もいまいち良くない気がします。 https://play.google.com/store/books

Amazon Kindle

Kindle(jp)はPCで読めないという致命的な欠点があるので、技術書の購入には向きません。 http://www.amazon.co.jp/

買ってよかった電子書籍

Smashing Android UI レスポンシブUIとデザインパターン

Smashing Android UI Androidのデザインについて深く考察した上で書かれた良著。 特に「第21章 UIデザインのアンチパターン」は「なぜ」Androidではそのデザインがだめなのか?ということを解決策付きで解説しており、Androidデザインの基本を理解するのに最適です。 サンプルアプリの一部挙動が若干分かり難いのが難点。 達人出版会で閲覧制限のないPDFが買えます。 http://tatsu-zine.com/books/smashing-android-ui

良いAndroidアプリを作る139の鉄則

タイトルの通り、良いAndroidアプリを作るためのノウハウがまとめられた本。 内容もデザインからテスト、リリースと多岐にわたっているので自分が苦手な部分への取っ掛かりをつかむには最適です。 また、コラムの内容も充実していて非常に参考になりました。 コンテンツ数が多い文、個々の内容が若干物足りない感じもしますが、そこは自分で調べればいいかな…。 技術評論社のPDFはAdobeReaderでコピーすると文字化けしますが、Chromeでコピーするとなぜか文字化けしません。 技術評論社で閲覧制限のないPDFが買えます。 https://gihyo.jp/dp/ebook/2014/978-4-7741-6607-0

Effective Android

Effective Android デザインからNDKまで幅広く取り扱っているので、知識の幅を広げるのに非常に良い本。 Androidマスターしたわ!と思ったらこの本でボッコボコにされるといいと思います。 あまり細かい解説がなかったりするので、初級者向けではないです。 達人出版会で閲覧制限のないPDFが買えます。 http://tatsu-zine.com/books/effective-android

Master of Fragment (Android Professional Developerシリーズ)

Master of Fragment Fragmentなんとなく使えるけどこれでいいんだっけ…くらいのときに読むと良い本。 よくある疑問点がまとめられていて非常に助かりました。 達人出版会で閲覧制限のないPDFが買えます。 http://tatsu-zine.com/books/master-of-fragments

Android Pattern Cookbook マーケットで埋もれないための差別化戦略

Android Pattern Cookbook Kiatkat準拠のデザイン本。 解説が細かく、サンプルソースも読みやすいです。 Impress Japan版はコピペ制限がかかっていたので、達人出版会で買い直しました。 達人出版会で閲覧制限のないPDFが買えます。 http://tatsu-zine.com/books/android-pattern-cookbook

Android Security 安全なアプリケーションを作成するために

Android Security Androidアプリのセキュリティについて書かれた稀有な本。 2年以上前の本なので、後述の「Android アプリのセキュア設計・セキュアコーディングガイド」と読み合わせると良い感じ。 達人出版会で閲覧制限のないPDFが買えます。 http://tatsu-zine.com/books/androidsec

Androidアプリのセキュア設計・セキュアコーディングガイド

Androidアプリのセキュリティについて書かれた資料。しかも無料。 AccountManagerとSyncAdapterを学習する上で滅茶苦茶お世話なりました。しかも無料。 ソースコード内にわかりやすくポイントが書いてあり、読みやすいです。しかも無料。 無料でPDFが公開されています http://www.jssec.org/report/securecoding.html

まとめ

達人出版会の宣伝みたいになっていますが、汎用性を考えるとPDF最強になってしまうので仕方ないですね。 技術評論社もかなり充実してきたので今後が楽しみです! O'Reillyはもっと電子書籍化がんばってほしい…。

設定画面のインテントアクションまとめ

はじめに

Androidでは、アプリが必要な権限を持っていれば、アプリから端末の設定(一部)を変更することができます。 この仕組はアプリが必要とする機能を確実に使用するためには有用ですが、インストール時に設定変更権限が必要な旨がダイアログ表示されてしまい、ユーザから敬遠される恐れもあります。 また、端末設定の変更ロジックに問題があった場合、影響範囲がアプリだけに収まらなくなります。

Androidでは上記のようなアプリから直接設定を変更する方法の他に、設定アプリの各画面へ簡単にアクセスする方法も提供されています。 android.provider.SettingsクラスにはAndroidの各機能設定画面を呼び出すIntentActionが定義されており、遷移先の設定画面でユーザ自身に必要な設定を行わせることができます。 この場合には自アプリには権限が必要ないため、インストール時の敬遠リスクは下がり、設定変更ロジックも書かなくて良いため実装コストは軽くなります。

この記事では、android.provider.Settingsクラスのアクション一覧を簡単な解説つきで列挙します。 キャプチャはAPI level 19未満で表示できるものはGalaxy Nexus@Android 4.3、API level 19以降が必要なものについてはNexus5@Android L Developer Previewを使用しています。

参考URL: http://developer.android.com/reference/android/provider/Settings.html

注意点

生成したIntentで呼び出される画面は端末の種類(メーカー)、OSのバージョンなどによって変わります。 同じ画面構成であっても見た目が同じとは限らないので、ヘルプなどを書く際には注意が必要です。

また、基本的にActionを指定した暗黙的インテントになるため、環境によっては複数Activity<intent-filter>に引っかかる場合があります。 逆に、<uses-feature>の設定などによっては対象Activityが見つからないことも考えられます。 どの設定画面への遷移を利用する場合でも、Intentの対象となるアクティビティが存在するかどうかを確認後、遷移するようにしてください。

ACTION_ACCESSIBILITY_SETTINGS

API level 5から追加。 ユーザ補助設定画面のアクション。

    Intent intent = new Intent();
    intent.setAction(Settings.ACTION_ACCESSIBILITY_SETTINGS);
    startActivity(intent);

ACTION_ADD_ACCOUNT

API level 8から追加。 アカウントの追加画面(アカウントの種別選択画面)のアクション。 Settings.EXTRA_ACCOUNT_TYPESaccount_typeを指定(String[]形式)することで追加対象のアカウントをフィルタリング可能。 Settings.EXTRA_AUTHORITIESsyncable="true"ContentProviderauthoritiesを指定(String[]形式)することで追加対象のアカウントをフィルタリング可能。 フィルタリングの結果、追加対象のアカウントが特定された場合は選択画面を経由せず、直接対象アカウントの追加画面を表示する。

    Intent intent = new Intent();
    intent.setAction(Settings.ACTION_ADD_ACCOUNT);
    // 対象をGoogleアカウントに限定
    intent.putExtra(Settings.EXTRA_ACCOUNT_TYPES,new String[]{"com.google"});
    startActivity(intent);

ACTION_AIRPLANE_MODE_SETTINGS

API level 3から追加。 機内モードの設定が可能な画面のアクション。

    Intent intent = new Intent();
    intent.setAction(Settings.ACTION_AIRPLANE_MODE_SETTINGS);
    startActivity(intent);

ACTION_APN_SETTINGS

API level 1から追加。 APNの設定画面のアクション。

    Intent intent = new Intent();
    intent.setAction(Settings.ACTION_APN_SETTINGS);
    startActivity(intent);

ACTION_APPLICATION_DETAILS_SETTINGS

API level9から追加。 指定したアプリケーションの詳細設定画面のアクション。 アプリケーションの指定はパッケージURI形式(package:com.my.app)でsetData()する。

    Intent intent = new Intent();
    intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
    intent.setData(Uri.parse("package:com.android.settings"));
    startActivity(intent);

ACTION_APPLICATION_DEVELOPMENT_SETTINGS

API level 3から追加。 開発者向けオプション画面のアクション。 開発者向けオプションを有効にしていない場合にどうなるかは不明。

    Intent intent = new Intent();
    intent.setAction(Settings.ACTION_APPLICATION_DEVELOPMENT_SETTINGS);
    startActivity(intent);

ACTION_APPLICATION_SETTINGS

API level 1から追加。 アプリケーションに関する設定画面のアクション。

    Intent intent = new Intent();
    intent.setAction(Settings.ACTION_APPLICATION_SETTINGS);
    startActivity(intent);

ACTION_BLUETOOTH_SETTINGS

API level 1から追加。 Bluetooth設定画面のアクション。

    Intent intent = new Intent();
    intent.setAction(Settings.ACTION_BLUETOOTH_SETTINGS);
    startActivity(intent);

ACTION_CAPTIONING_SETTINGS

API level 19から追加。 Kitkatから追加されたユーザ補助設定のうち、ビデオキャプション設定画面のアクション。

    Intent intent = new Intent();
    intent.setAction(Settings.ACTION_CAPTIONING_SETTINGS);
    startActivity(intent);

ACTION_DATA_ROAMING_SETTINGS

API level 3から追加。 データローミング設定画面のアクション。

    Intent intent = new Intent();
    intent.setAction(Settings.ACTION_DATA_ROAMING_SETTINGS);
    startActivity(intent);

ACTION_DATE_SETTINGS

API level 1から追加。 日付と時刻設定画面のアクション

    Intent intent = new Intent();
    intent.setAction(Settings.ACTION_DATE_SETTINGS);
    startActivity(intent);

ACTION_DEVICE_INFO_SETTINGS

API level 8から追加。 端末の状態画面のアクション。 Android L Developer Previewではなぜかクラッシュする。

    Intent intent = new Intent();
    intent.setAction(Settings.ACTION_DEVICE_INFO_SETTINGS);
    startActivity(intent);

ACTION_DISPLAY_SETTINGS

API level 1から追加。 ディスプレイ設定画面のアクション。

    Intent intent = new Intent();
    intent.setAction(Settings.ACTION_DISPLAY_SETTINGS);
    startActivity(intent);

ACTION_DREAM_SETTINGS

API level 18から追加。 JELLY_BEAN_MR1で追加されたDaydream(スクリーンセーバー)設定画面のアクション。

    Intent intent = new Intent();
    intent.setAction(Settings.ACTION_DREAM_SETTINGS);
    startActivity(intent);

ACTION_INPUT_METHOD_SETTINGS

API level 3から追加。 言語と入力設定画面のアクション。

    Intent intent = new Intent();
    intent.setAction(Settings.ACTION_INPUT_METHOD_SETTINGS);
    startActivity(intent);

ACTION_INPUT_METHOD_SUBTYPE_SETTINGS

API level 11から追加。 インプットメソッド(文字入力アプリ)のサブタイプ設定画面のアクション。 Settings.EXTRA_INPUT_METHOD_IDInputMethodInfo#getId()で取得したidを指定することでサブタイプを指定できるらしいが未検証。 Nexus5では、Settings.EXTRA_INPUT_METHOD_IDに何も設定しないとgoogleキーボードの入力言語設定が表示される。

    Intent intent = new Intent();
    intent.setAction(Settings.ACTION_INPUT_METHOD_SUBTYPE_SETTINGS);
    startActivity(intent);

ACTION_INTERNAL_STORAGE_SETTINGS

API level 3から追加。 内部ストレージ設定画面のアクション。 このアクションは端末ごとの差異が激しいかもしれない。

    Intent intent = new Intent();
    intent.setAction(Settings.ACTION_INTERNAL_STORAGE_SETTINGS);
    startActivity(intent);

ACTION_LOCALE_SETTINGS

API level 1から追加。 端末の言語設定画面のアクション。

    Intent intent = new Intent();
    intent.setAction(Settings.ACTION_LOCALE_SETTINGS);
    startActivity(intent);

ACTION_LOCATION_SOURCE_SETTINGS

API level 1から追加。 位置情報アクセス設定画面のアクション。 位置情報の取得モードを切り替えさせるのに使うっぽい。

    Intent intent = new Intent();
    intent.setAction(Settings.ACTION_LOCATION_SOURCE_SETTINGS);
    startActivity(intent);

ACTION_MANAGE_ALL_APPLICATIONS_SETTINGS

API level 9から追加。 すべてのアプリケーション情報画面のアクション。 アプリ設定画面の「すべて」タブのこと。

    Intent intent = new Intent();
    intent.setAction(Settings.ACTION_MANAGE_ALL_APPLICATIONS_SETTINGS);
    startActivity(intent);

ACTION_MANAGE_APPLICATIONS_SETTINGS

API level 3から追加。 アプリ管理画面のアクション。 ACTION_APPLICATION_SETTINGSと同じ画面が表示された。

    Intent intent = new Intent();
    intent.setAction(Settings.ACTION_MANAGE_APPLICATIONS_SETTINGS);
    startActivity(intent);

ACTION_MEMORY_CARD_SETTINGS

API level 3から追加。 外部ストレージ設定画面のアクション。 このアクションは端末ごとの差異が激しいかもしれない。

    Intent intent = new Intent();
    intent.setAction(Settings.ACTION_MEMORY_CARD_SETTINGS);
    startActivity(intent);

ACTION_NETWORK_OPERATOR_SETTINGS

API level 3から追加。 利用可能なネットワーク設定画面のアクション。 表示と同時に利用可能なネットワークの検索が始まる。

    Intent intent = new Intent();
    intent.setAction(Settings.ACTION_NETWORK_OPERATOR_SETTINGS);
    startActivity(intent);

ACTION_NFCSHARING_SETTINGS

API level 14から追加。 ICE_CREAM_SANDWICHから追加されたAndroidビーム設定画面のアクション。 Androidビームの有効/無効はNfcAdapter#isNdefPushEnabled()で確認できる。

    Intent intent = new Intent();
    intent.setAction(Settings.ACTION_NFCSHARING_SETTINGS);
    startActivity(intent);

ACTION_NFC_PAYMENT_SETTINGS

API level 19から追加。 KITKATから追加されたタップ&ペイ設定画面のアクション。

    Intent intent = new Intent();
    intent.setAction(Settings.ACTION_NFC_PAYMENT_SETTINGS);
    startActivity(intent);

ACTION_NFC_SETTINGS

API level 16から追加。 NFC設定画面のアクション。 Nexusなどでは無線とネットワーク画面内のチェックボックスNFCが追加されたのはAPI level 14からだが、この定数はAPI level 16からなので注意。

    Intent intent = new Intent();
    intent.setAction(Settings.ACTION_NFC_SETTINGS);
    startActivity(intent);

ACTION_PRINT_SETTINGS

API level 19から追加。 KITKATで追加された印刷サービス設定画面のアクション。

    Intent intent = new Intent();
    intent.setAction(Settings.ACTION_PRINT_SETTINGS);
    startActivity(intent);

ACTION_PRIVACY_SETTINGS

API level 5から追加。 プライバシー設定画面のアクション。 Nexusではバックアップとリセット画面が相当。

    Intent intent = new Intent();
    intent.setAction(Settings.ACTION_PRIVACY_SETTINGS);
    startActivity(intent);

ACTION_QUICK_LAUNCH_SETTINGS

API level 3から追加。 Searchキーと他キーの同時押しで起動するアプリ設定画面のアクション。 この機能自体を知らなかった。ハードウェアキーボードが存在する端末向きだろうか? Android L Developer Previewではクラッシュする。

    Intent intent = new Intent();
    intent.setAction(Settings.ACTION_QUICK_LAUNCH_SETTINGS);
    startActivity(intent);

ACTION_SEARCH_SETTINGS

API level 8から追加。 検索設定画面のアクション。 NexusではGoogle検索アプリの設定画面が表示される。

    Intent intent = new Intent();
    intent.setAction(Settings.ACTION_SEARCH_SETTINGS);
    startActivity(intent);

ACTION_SECURITY_SETTINGS

API level 1から追加。 端末のセキュリティ設定画面のアクション。

    Intent intent = new Intent();
    intent.setAction(Settings.ACTION_SECURITY_SETTINGS);
    startActivity(intent);

ACTION_SETTINGS

API level 1から追加。 設定アプリのアクション(設定アプリの起動)。

    Intent intent = new Intent();
    intent.setAction(Settings.ACTION_SETTINGS);
    startActivity(intent);

ACTION_SOUND_SETTINGS

API level 1から追加。 端末の音関連設定のアクション。

    Intent intent = new Intent();
    intent.setAction(Settings.ACTION_SOUND_SETTINGS);
    startActivity(intent);

ACTION_SYNC_SETTINGS

API level 3から追加。 アカウントの同期設定のアクション。 Settings.EXTRA_AUTHORITIESSyncAdapterauthoritiesを指定することで直接その同期設定が開けるようだが未検証。 Settings.EXTRA_AUTHORITIES未設定の場合は設定済み同期一覧が表示される。

    Intent intent = new Intent();
    intent.setAction(Settings.ACTION_SYNC_SETTINGS);
    startActivity(intent);

ACTION_USER_DICTIONARY_SETTINGS

API level 3から追加。 ユーザー辞書設定画面のアクション。

    Intent intent = new Intent();
    intent.setAction(Settings.ACTION_USER_DICTIONARY_SETTINGS);
    startActivity(intent);

ACTION_WIFI_IP_SETTINGS

API level 3から追加。 Wi-Fi接続時のIP設定画面のアクション。

    Intent intent = new Intent();
    intent.setAction(Settings.ACTION_WIFI_IP_SETTINGS);
    startActivity(intent);

ACTION_WIFI_SETTINGS

API level 1から追加。 Wi-Fi設定画面のアクション。

    Intent intent = new Intent();
    intent.setAction(Settings.ACTION_WIFI_SETTINGS);
    startActivity(intent);

ACTION_WIRELESS_SETTING

API level 1から追加。 無線とネットワーク設定のアクション。

    Intent intent = new Intent();
    intent.setAction(Settings.ACTION_WIRELESS_SETTING);
    startActivity(intent);

画面回転時のActionBarの挙動まとめ

はじめに

ActionBarの一部の機能は、端末の画面の向きによって挙動が異なる場合があります。 この記事では特に画面回転時に大きく挙動が変わる部分についてまとめました。

MenuItemの表示数が変わる

ActionBarに表示されるMenuItemの数は画面幅のdpによって決定されています。 ActionBarでタイトルなどが表示されるバーのことをメインアクションバーと呼びますが、 このメインアクションバーにMenuItemを表示する場合、全幅の50%を超えない範囲で表示数が決められています。 メインアクションバーにおけるdpMenuItem表示数の関係は以下のようになっています。

dp Menu表示数
-359 2
360-499 3
500-599 4
600- 5

http://developer.android.com/design/patterns/actionbar.html

下記スクリーンショットGalaxyNexusでの例です。 縦向きでは(オーバーフローを含めて)3つ、横向きでは4つ表示されています。

 

タイトル非表示の場合

ロゴおよびタイトルを非表示にした場合でもメインアクションバーのメニューの表示数は増えません。

final ActionBar actionbar = getSupportActionBar();
actionbar.setDisplayShowTitleEnabled(false);
actionbar.setDisplayShowHomeEnabled(false);

 

分割アクションバーの統合

AndroidManifest.xmlに設定を記述することで、縦向き時のActionBarの要素を分割することができます。 これにより画面下部では横幅いっぱいにMenuItemを表示することができますが、横向き時にはすべてメインアクションバーに格納されてしまいます。

<manifest >
    <activity uiOptions="splitActionBarWhenNarrow"  >
        <meta-data android:name="android.support.UI_OPTIONS"
                   android:value="splitActionBarWhenNarrow" />
    </activity>
</manifest>

 

タブが格納される

ActionBarのナビゲーションモードをNAVIGATION_MODE_TABSにしている場合、画面が縦向きであればメインアクションバーの下にタブバーが表示されます。 画面が横向きの場合、このタブバーはメインアクションバーに格納されます。 また、横向き時にメインアクションバーにタブの内容が表示しきれない場合、NAVIGATION_MODE_LISTに似た表示に変更されます。

    final ActionBar actionbar = getSupportActionBar();
    actionbar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS);
    actionbar.addTab(actionbar.newTab().setText("ほげほげ").setTabListener(listener));
    actionbar.addTab(actionbar.newTab().setText("ぴよぴよ").setTabListener(listener));
    actionbar.addTab(actionbar.newTab().setText("かにかに").setTabListener(listener));
    actionbar.addTab(actionbar.newTab().setText("えびえび").setTabListener(listener));

タブ要素を表示できる場合

メインアクションバーにタブの内容が入りきる場合、タブバーの内容はそのままメインアクションバーに表示されます。

 

タブ要素を表示できない場合

タイトルやMenuItemによりメインアクションバーにタブの内容が入りきらない場合、タブバーの内容はドロップダウンリストとして表示されます。 このとき、MenuItemapp:showAsAction="ifRoom"としていてもMenuItemの表示のほうが優先されます。

 

タブ+分割

アクションバーの分割とタブ表示を両方行っていた場合、どちらの要素もメインアクションバーに格納されます。

 

タイトル非表示の場合

ロゴおよびタイトルを非表示にした場合、タブ要素は左詰めで表示されます。 画像では分割アクションバーですが、分割していない場合でも同様です。

final ActionBar actionbar = getSupportActionBar();
actionbar.setDisplayShowTitleEnabled(false);
actionbar.setDisplayShowHomeEnabled(false);

 

ドロップダウンは特に変化なし

ActionBarのナビゲーションモードをNAVIGATION_MODE_LISTにしている場合、画面の向きにかかわらずメインアクションバーにドロップダウンメニューが表示されます。

    final ActionBar actionbar = getSupportActionBar();
    actionbar.setNavigationMode(ActionBar.NAVIGATION_MODE_LIST);
    actionbar.setListNavigationCallbacks(new ArrayAdapter<String>(
            MyActivity.this,R.layout.list_item,
                new String[]{"ほげほげ","ぴよぴよ","かにかに","えびえび"}),
            new ActionBar.OnNavigationListener() {
        @Override
        public boolean onNavigationItemSelected(int i, long l) {
            return false;
        }
    });

 

参考資料

ActionBarの表示に関する挙動は以下のURLに書いてあります。 http://developer.android.com/design/patterns/actionbar.html http://developer.android.com/guide/topics/ui/actionbar.html

SyncAdapterの実装メモ

SyncAdapterとは

SyncAdapterとは、AccountManager上のアカウントとContentProviderを紐付けることにより、クラウド上のデータと端末のデータを同期させる仕組みです。 一般的な動作としては「ContentResolverAccountManagerのアカウントとSyncAdapterを登録し、指定した周期でAccountManagerから取得した認証トークンを利用して通信処理を行い、ContentProviderを更新する」という挙動となります。 このとき、同期処理を管理・実行するのはContentResolverであり、AccountManagerはアカウントとトークンの取得にのみ利用します。

よくある誤解

SyncAdapterAccountManager上のaccountTypeContentProviderauthorityを利用して紐付けを行う仕組みですが、SyncAdapterを実装したアプリがアカウントの管理機能(Authenticator)を持つ必要はありません。 例えば、SyncAdapterが対象とするaccountTypeを"com.google"とすることでGoogleアカウントに自アプリのSyncAdapterを登録することもできます。 この記事中のサンプルでも実際にGoogleアカウントにSyncAdapterを紐付け、同期処理を実行させています。

SyncAdapterの実装

SyncAdapterの実装はAuthenticatorの実装と少し似ています。 Authenticator同様、システムから実行されるServiceと、その実態(IBinder)であるSyncAdapter、そしてSyncAdapterの宣言に用いるXMLファイルなどが必要なほか、同期するデータの置き場所としてContentProviderの宣言も必要です。 なお、ContentProviderの実装部分は通常のアプリと全く変わりがないため、今回のサンプルでは、ContentProviderの実装については詳しく説明しません。

SyncAdapterの宣言

SyncAdapterによる同期機能を提供するためには、SyncAdapterを実装したアプリであることをシステムに宣言する必要があります。 この宣言はAndroidManifest.xmlにおいて、特定の種類の<intent-filter>を持ったサービスが存在するかどうかでチェックされます。 同期設定を編集するための権限として、WRITE_SYNC_SETTINGSが必要な他、アカウント取得とトークン取得のためにGET_ACCOUNTSUSE_CREDENTIALSも必要になります。 また、注意点として<provider>タグに同期対象であることを示すandroid:syncableの設定が必要です。

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.nein37.syncadaptersample" >

    <uses-permission android:name="android.permission.GET_ACCOUNTS" />
    <uses-permission android:name="android.permission.USE_CREDENTIALS" />
    <uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS" />

    <application
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name">
        <activity
            android:name=".MyActivity"
            android:label="@string/app_name" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <provider
            android:exported="false"
            android:syncable="true"
            android:authorities="com.example.nein37.syncadaptersample"
            android:name=".MyContentProvider" />

        <service
            android:name=".MySyncService"
            android:exported="true"
            android:process=":sync">
            <intent-filter>
                <action android:name="android.content.SyncAdapter" />
            </intent-filter>
            <meta-data
                android:name="android.content.SyncAdapter"
                android:resource="@xml/syncadapter" />
        </service>

    </application>

</manifest>

タグの<intent-filter>と<meta-data>の記述はAuthenticatorアプリの実装に似ています。 SyncAdapterではそれに加えてandroid:process=":sync"という属性の宣言が必要なので、注意してください。 <meta-data>から参照されるXMLファイルにはSyncAdapterに関する宣言を記述します。

<sync-adapter xmlns:android="http://schemas.android.com/apk/res/android"
    android:accountType="com.google"
    android:allowParallelSyncs="false"
    android:contentAuthority="com.example.nein37.syncadaptersample"
    android:isAlwaysSyncable="true"
    android:supportsUploading="false"
    android:userVisible="true" />

ここで設定している属性は、それぞれ以下の様な意味です。

属性名 意味
android:accountType 対象とするアカウントのaccountType
android:allowParallelSyncs 複数のSyncAdapterが同時に実行されることを許すかどうか。
複数のアカウントと同期したい場合に設定する。
android:contentAuthority 同期対象のContentProviderauthorityと同じものにする
android:isAlwaysSyncable 常に同期が実行可能な常態かどうかを示すフラグ。手動同期のみの場合はfalse
android:supportsUploading 同期作業でデータのアップロード行うかどうかのフラグ。
trueの場合、ContentResolver#notifyChange()されていると同期が必要と見なすようだ
android:userVisible 設定アプリのアカウントと同期にこのアプリの同期設定が表示されるかどうかのフラグ。
trueにするとユーザが同期設定を解除できるので注意。

SyncAdapterの実装

SyncAdapterContentResolverによって実行される同期処理そのものを記述するクラスです。 必要なメソッドは抽象クラスであるAbstractThreadedSyncAdapterで宣言されているため、このクラスを継承して実装します。 基本的にはAbstractThreadedSyncAdapter#onPerformSync()メソッドのみ実装すれば良く、このメソッドの中でトークンの取得から通信処理、ContentProviderの更新処理を記述します。 また、このメソッドからトークンを取得するためにはAccountManager#blockingGetAuthToken()というメソッドを利用します。 このメソッドはトークンが取得できるまでの間処理をブロックするためメインスレッドでは使えませんが、AbstractThreadedSyncAdapter#onPerformSync()はバックグラウンドで処理するメソッドなので大丈夫です。

public class MySyncAdapter extends AbstractThreadedSyncAdapter {

    ContentResolver mContentResolver;

    public MySyncAdapter(Context context, boolean autoInitialize) {
        super(context, autoInitialize);
        mContentResolver = context.getContentResolver();
    }

    @Override
    public void onPerformSync(Account account, Bundle extras, String authority,
                              ContentProviderClient provider, SyncResult syncResult) {
        try {
            AccountManager manager =AccountManager.get(getContext());
            // トークンの取得
            String token = manager.blockingGetAuthToken(account, "cl", true);

            // TODO ここで通信処理を行う

            // TODO DBへの反映処理を行う
            ContentValues values = new ContentValues();
            values.put(COLUMN, VALUE);
            getContext().getContentResolver().insert(CONTENT_URI, values);

        } catch (OperationCanceledException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (AuthenticatorException e) {
            e.printStackTrace();
        }
    }
}

同期サービスの実装

認証サービスはContentResolverSyncAdapterとの橋渡しを行います。 実装的にはService#onBind()でAbstractThreadedSyncAdapter#getSyncAdapterBinder()を結果を返すだけです。

public class MySyncService extends Service {

    private MySyncAdapter mSyncAdapter;

    @Override
    public void onCreate() {
        super.onCreate();
        mSyncAdapter =new MySyncAdapter(this,true);
    }

    @Override
    public IBinder onBind(Intent intent) {
        return mSyncAdapter.getSyncAdapterBinder();
    }
}

SyncAdapterの登録

SyncAdapterによる同期を行うためには、ContentResolverへのSyncAdapterの登録が必要です。 通常、SyncAdapterの登録はアプリ内でのログイン/アカウント選択時や設定画面などで行われますが、サンプルでは単純にアプリ起動時に一番最初に登録されたGoogleアカウントに対して登録を行います。 SyncAdapterの登録メソッドは同期処理のタイミングによっていくつか種類があるので、以下それぞれ説明します。

定期的に同期を行いたい場合

何時間おき、何日おきというように間隔を指定して定期的に同期処理を実行したい場合、ContentResolver.addPeriodicSync()を使用し、間隔を秒単位で指定します。

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_my);
        // 今回は最初に登録されたGoogleアカウントが対象
        Account account = AccountManager.get(this).getAccountsByType("com.google")[0];

        Bundle args = new Bundle();
        // 必要なパラメータがあればBundleに詰める

        // 10分ごとに同期
        ContentResolver.addPeriodicSync(account, AUTHORITY, args, 600);
    }

ネットワークメッセージのタイミングで同期を行いたい場合

Androidシステムはネットワークに接続の確立後、TCP/IPコネクションを維持するために短い間隔でメッセージ送出しています。 このメッセージが送出されたタイミングで同期を行う場合、ContentResolver.setSyncAutomatically()を使用します。 実際にはこの設定に関する挙動はもう少し複雑で、android:supportsUploading設定やContentResolver#notifyChange()などで更新が必要かどうかを判断しているようです。 そのため、ContentProviderまわりの実装にミスがあるとうまく同期されなくなるかもしれません。 また、ContentResolver.setSyncAutomatically()を使用した場合でもContentResolver.addPeriodicSync()で登録した内容は削除されないことに注意してください。 ContentResolver.addPeriodicSync()した後でネットワークメッセージタイミングのみの同期に切り替えたい場合、ContentResolver.removePeriodicSync()を利用して既存設定を削除する必要があります。

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_my);
        // 今回は最初に登録されたGoogleアカウントが対象
        Account account = AccountManager.get(this).getAccountsByType("com.google")[0];

        // ネットワークメッセージタイミングで同期
        ContentResolver.setSyncAutomatically(account, AUTHORITY, true);
    }

任意のタイミングで同期を行いたい場合

ボタン押下時など、任意のタイミングで同期を行う場合、ContentResolver.requestSync()を使用できます。 通常、SyncAdapterの動作は同期間隔の指定やネットワークの効率的利用によってバッテリーの消費が抑えられていますが、 このメソッドを使用すると同期処理を強制的に行ってしまうため、バッテリーを過剰に消費する場合があります。

    @Override
    public void onClick(View v) {
        Account account = AccountManager.get(this).getAccountsByType("com.google")[0];
        ContentResolver.requestSync(account,SampleColumn.AUTHORITY,new Bundle());        
    }

注意点

SyncAdapterは非常に優れた機能ですが、実装する際に注意すべきことが多い上に資料が少ないのが難点です。 ここでは、私が特に注意すべきと感じたことをまとめます。

AndroidManifest.xmlの設定

SyncAdapterを使う場合、<service>だけでなく<provider>にも設定が必要なことに注意が必要です。 また、android:process=":sync"android:process="sync"と書いてしまったり、<service>android:exported=falseにしてしまったりすると動作しません。

android:userVisible="false"

SyncAdapterの設定をandroid:userVisible="false"にすると、設定アプリの同期一覧で表示されなくなります。 この設定はユーザにSyncAdapterを無効にさせないために有用ですが、SyncAdapterの設定を判別することができなくなります。 特に定期同期とネットワークメッセージ同期は片方を設定してももう片方が解除されないため、意図せず頻繁に同期されてしまう可能性があります。 SyncAdapterを設定する際はContentResolver.getPeriodicSyncs()ContentResolver.getSyncAutomatically()SyncAdapterの設定状況を取得し、不要な設定情報が存在しないか確認したほうが良いかもしれません。

APIレベル

SyncAdapterの大半の機能はAPI level 5から実装されていますが、ContentResolver.addPeriodicSync()などの定期同期のための機能はAPI level 8以降となります。 そのため、API level 5-7 の端末で定期同期を行いたい場合、AlarmManagerを使用する必要があります。

アプリの承認

ActivityAccountManager#getAuthToken()せず、SyncAdapterではじめてAccountManager#blockingGetAuthToken()した場合、アプリの接続承認のための通知が表示されます。 この通知をタップすると承認画面へ遷移し、そこで承認してはじめて同期処理でトークンを利用できるようになりますが、若干わかりづらいのでActivity側でアカウント選択時にAccountManager#getAuthToken()したほうが良いかもしれません。 当然ですが、その際取得したトークンを保存してはいけません。

AccountManagerでアカウントを管理する

はじめに

AccountManagerにおいて、アカウントの管理やトークンの取得を直接行うクラスのことをAuthenticatorと呼びます。 それにあわせて、この記事ではAuthenticatorを実装したアプリのことをAuthenticatorアプリと呼ぶことにします。

Authenticatorアプリの実装

Authenticatorアプリでは、以下の実装が必要です。

  1. アカウント種別の宣言 - AccountManagerに追加するアカウント種別の宣言
  2. 認証画面 - ユーザに提供するログイン画面のActivity
  3. Authenticator - AccountManagerへ提供する機能の実装
  4. 認証サービス - AccountManagerAuthenticatorを繋ぐためのService

以下、それぞれの実装について詳しく説明します。

アカウント種別の宣言

アカウントの管理機能を提供するためには、Authenticatorアプリであることをシステムに宣言する必要があります。 Authenticatorアプリの宣言はAndroidManifest.xmlにおいて、特定の種類の<intent-filter>を持ったサービスが存在するかどうかでチェックされます。 また、アカウントを編集するための権限として、AUTHENTICATE_ACCOUNTSが必要です。

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.nein37.authenticatorsample">

    <uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS" />

    <application
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name">

        <service
            android:name=".AuthenticatoinService"
            android:exported="false">
            <intent-filter>
                <action android:name="android.accounts.AccountAuthenticator" />
            </intent-filter>
            <meta-data
                android:name="android.accounts.AccountAuthenticator"
                android:resource="@xml/authenticator" />
        </service>

        <activity
            android:name=".LoginActivity"
            android:exported="true"
            android:label="@string/app_name" />
    </application>
</manifest>

<Service>タグの<intent-filter><meta-data>の記述はAuthenticatorアプリを作る場合の決まり文句です。 前者でAuthenticator対応のServiceがあることを宣言し、後者で設定可能なアカウントの種類を宣言しています。

<meta-data>から参照されるXMLファイルでは、アカウント種別の宣言となるaccount-authenticator情報を記述します。 このうち、android:accountTypeは必ず他と重複しないようなユニークな値にする必要があります。 android:accountTypeAccountManager#getAccountsByType()などで指定するaccountTypeと同じものです。 android:iconandroid:smallIconはどちらも設定アプリのアカウント管理画面で表示されるアイコンですが、このとき指定する画像サイズに関する情報は見つけられませんでした…。 android:labelは設定アプリでの表示に利用されます。直接記述すると表示されないので、必ず文字列リソースに定義してください。

 <account-authenticator
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:accountType="com.example.test"
    android:icon="@drawable/ic_launcher"
    android:smallIcon="@drawable/ic_launcher"
    android:label="@string/account_label" />

認証画面の実装

認証画面はユーザIDやパスワードなど、必要な情報を入力し、サーバと通信して認証できる実装になっている必要があります。 認証処理に成功後、AccountManager#addAccountExplicitly()によってAccountManagerへアカウントの登録を行います。 このとき、accountTypeが必ずauthenticator.xmlで定義したものと同じになるようにしてください。 実装例では、サーバへの通信処理や例外処理などは省略しています。

public class LoginActivity extends ActionBarActivity  {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_login);

        final EditText nameEdit = (EditText) findViewById(R.id.name);
        final EditText passwordEdit = (EditText) findViewById(R.id.password);
        Button button = (Button) findViewById(R.id.login);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                String name = nameEdit.getText().toString();
                String password = passwordEdit.getText().toString();
                login(name, password);
            }
        });

    }

    // ログイン処理    
    public void login(final String name, final String password) {
        // TODO:このメソッドは非同期の通信処理でログインを試みます。
        // ログインに成功した場合、loginSuccess()を呼び出します。
        loginSuccess(name, password);
    }

    // ログイン処理のコールバック
    public void loginSuccess(final String name, final String password) {

        Account account = new Account(name, "com.example.test");
        AccountManager am = AccountManager.get(this);
        // アカウント情報を保存
        // TODO:本来はパスワードを暗号化する必要があります
        am.addAccountExplicitly(account, password, null);

        // 認証画面終了
        setResult(RESULT_OK);
        finish();
    }
}

Authenticatorの実装

AuthenticatorAccountManagerからの要求に対して応答を行うAuthenticatorアプリで最も重要な機能です。 必要なメソッドなどはAbstractAccountAuthenticatorという抽象クラスで宣言されているので、このクラスを継承して実装します。 最低限実装が必要なメソッドaddAccount()getAuthToken()の2つです。 addAccount()AccountManager#getAccount()が呼び出されたときに実行されるメソッドで、認証画面を起動するためのIntentを生成して返します。 getAuthToken()も同じようにAccountManager#getAuthToken()が呼び出されたときに実行され、メソッドの中でサーバとの通信処理を行ってトークンを取得後、返却します。 どちらのメソッドBundleに特定のキーで返却する必要が有るため、AccountManagerが持つ定数について調べておく必要があります。

public class MyAuthenticator extends AbstractAccountAuthenticator {

    public static final String ACCOUNT_TYPE = "com.example.test";

    final Context mContext;

    public MyAuthenticator(Context context) {
        super(context);
        mContext = context;
    }

    @Override
    public Bundle addAccount(AccountAuthenticatorResponse response, String accountType,
            String authTokenType, String[] requiredFeatures, Bundle options)
            throws NetworkErrorException {

        // アカウントの追加を行う画面を呼び出すIntentを生成
        final Intent intent = new Intent(mContext, LoginActivity.class);
        // アカウント追加後、戻り先の画面を設定
        intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response);

        // Intentを返却
        final Bundle bundle = new Bundle();
        bundle.putParcelable(AccountManager.KEY_INTENT, intent);
        return bundle;
    }

    @Override
    public Bundle getAuthToken(AccountAuthenticatorResponse response, Account account,
            String authTokenType, Bundle options) throws NetworkErrorException {

        AccountManager manager = AccountManager.get(mContext);
        String name = account.name;
        // TODO:本来はパスワードを復号化する必要があります
        String password = manager.getPassword(account);

        // TODO:本来はここで通信を行い、ユーザ名とパスワードからトークンの取得を行う
        String authToken = "AUTH_TOKEN";
        // トークンをキャッシュ
        manager.setAuthToken(account,authTokenType,authToken);

        // トークンを返却する
        Bundle result = new Bundle();
        result.putString(AccountManager.KEY_ACCOUNT_NAME, name);
        result.putString(AccountManager.KEY_ACCOUNT_TYPE, ACCOUNT_TYPE);
        result.putString(AccountManager.KEY_AUTHTOKEN, authToken);
        return result;
    }

    @Override
    public Bundle editProperties(AccountAuthenticatorResponse response, String accountType) {
        return null;
    }

    @Override
    public Bundle confirmCredentials(AccountAuthenticatorResponse response, Account account,
            Bundle options) throws NetworkErrorException {
        return null;
    }

    @Override
    public String getAuthTokenLabel(String authTokenType) {
        return null;
    }

    @Override
    public Bundle updateCredentials(AccountAuthenticatorResponse response, Account account,
            String authTokenType, Bundle options) throws NetworkErrorException {
        return null;
    }

    @Override
    public Bundle hasFeatures(AccountAuthenticatorResponse response, Account account,
            String[] features) throws NetworkErrorException {
        return null;
    }
}

認証サービスの実装

認証サービスはAccountManagerAuthenticatorとの橋渡しを行います。 …といっても難しいことはなく、Service#onBind()AbstractAccountAuthenticator#getIBinder()を結果を返すだけです。 あとはAccountManagerIBinderを経由してAuthenticatorメソッドを呼び出し、認証画面の呼び出しやトークンの返却を行ってくれます。

public class AuthenticatoinService extends Service {

    private MyAuthenticator mAuthenticator;

    @Override
    public void onCreate() {
        super.onCreate();
        mAuthenticator =new MyAuthenticator(this);
    }

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

おまけ

以上でAuthenticatorアプリに必要な最低限の実装は終わりです。 しかし、Authenticatorアプリが扱う内容は非常にデリケートなため、セキュリティに細心の注意を払う必要があります。 以下ではAuthenticatorアプリを実装する上での注意点や補足などをまとめます。

Service、Activityの公開範囲を設定する

認証サービスを非公開サービス、認証画面を公開アクティビティとして設定する必要があります。 設定方法については上記AndroidManifest.xmlexported設定を参照してください。

パスワードを暗号化する

AccountManagerはアカウント情報をDBに保存しています。 このDBは通常、アプリからアクセスすることができませんが、まったく暗号化されていません。 root化された端末などではAccountManagerのDBを簡単に見られてしまうため、パスワードを平文で保存してはいけません。 暗号化する場合はAccountManager#addAccountExplicitly()する際に暗号化し、AccountManager#getPassword()した際に復号化します。

パスワードを保存しない

サービスへのアクセスによりトークンの期限を延長できるようなサービスの場合、パスワードを保存しないという選択肢があります。 その場合、Authenticator#getAuthToken()では新たにトークンを取得することが不可能なため、認証画面での認証成功事にAccountManager#setAuthToken()を呼び出してトークンをキャッシュする必要があります。 また、Authenticator#getAuthToken()が呼び出されたときはトークンの再取得が不可能なため、こちらも認証画面を再度表示させるなどの工夫が必要です。

トークンがキャッシュされる働きを理解する

認証トークンはAccountManager#setAuthToken()されたときにキャッシュされ、AccountManager#invalidateAuthToken()されたときに無効となります。 キャッシュが有効な間、AccountManager#getAuthToken()Authenticator#getAuthToken()を呼び出しません。 以下のようなAuthenticator#getAuthToken()の実装は不要です。

    @Override
    public Bundle getAuthToken(AccountAuthenticatorResponse response, Account account,
            String authTokenType, Bundle options) throws NetworkErrorException {

        AccountManager manager = AccountManager.get(mContext);
        // キャッシュからトークンを取得
        String authToken = manager.peekAuthToken(account,authTokenType);
        if(TextUtils.isEmpty(authToken)){
            // キャッシュが無効なのでトークンを取得
            authToken = "AUTH_TOKEN";
            manager.setAuthToken(account,authTokenType,authToken);
        }
        // ...

authTokenTypeによるトークン取得制限

アプリから認証トークンを取得する際、AccountManager#getAuthToken()authTokenTypeを指定する必要があります。 このauthTokenTypeについてサンプルではチェックしていませんが、本来はAuthenticator#getAuthToken()で有効な値かどうかを判定し、それに応じたトークンの取得を行うことになっています。 AccountManagerからは設定可能なauthTokenTypeの一覧を知ることができないため、トークンを取得したいアプリは最初からauthTokenTypeを知っている必要があります。 この制限のため、原理的にはauthTokenTypeを非公開とすることでトークンの取得制限をかけることができますが、実際にはauthTokenTypeも平文でDBに保存されるためroot化された端末では簡単に解析されてしまいます。 従って、非公開authTokenTypeによってセキュリティが確保されたと考えるのは危険です。

Authenticatorアプリの競合

※Authenticatorの実装と直接関係ありません。 まったく同一のaccountTypeを持つ複数Authenticatorアプリが端末にインストールされると、先にインストールされたAuthenticatorアプリだけが有効になります。 悪意のあるAuthenticatorアプリが先にインストールされた端末ではユーザIDやパスワードが漏洩する恐れがあるため、AccountManagerを利用するアプリは接続先のAuthenticatorが本物かどうか気をつける必要があります。 …多分AccountManager#getAuthenticatorTypes()AuthenticatorDescriptionを取得してパッケージ探して証明書を検証するんじゃないかと思うんですが、具体的にどう実装すればチェックできるのか把握できていません。

証明書の異なるアプリからアクセスする際の注意

Authenticatorアプリと異なる証明書を持ったアプリでAccountManager#getAuthToken()した場合、Android OS 4.0.x端末ではクラッシュします。 具体的には、システムがGrantCredentialsPermissionActivityという画面を呼びだそうとして、その途中でNullPointerExceptionが発生するようです。 この問題はAOSPのissuesにも登録されていて、4.1.xでは修正されているようですが、回避方法がわかりません。 どなたか回避方法をご存知の方は教えてください…。 https://code.google.com/p/android/issues/detail?id=23421

参考資料

この記事を書くにあたって以下の資料を参考にしました。 JSSEC 『Android アプリのセキュア設計・セキュアコーディングガイド』2014年7月1日版 http://www.jssec.org/report/securecoding.html

タオソフトウェア株式会社 Android Security 安全なアプリケーションを作成するために http://www.amazon.co.jp/dp/4844331345

AccountManagerを利用する

AccountManagerとは

AccountManagerとは、Androidにおいて様々なWebサービスのアカウントを管理するための仕組みのことです。 アプリはAccountManagerを利用することで以下のような様々なことができるようになります。

  • アカウント情報の取得
  • アカウントの編集
  • アカウントのトークン取得

それぞれの操作は<uses-permission>による制限がかけられています。 呼び出す機能に応じてに応じて必要な<uses-permission>が変わってくるため、必要な機能を十分に検討し、適切な<uses-permission>を設定する必要があります。

また、どの機能を利用する場合でもそのサービスに対応したアカウント管理アプリがインストールされていなければAccountManagerを経由してアカウント情報を管理することはできません。

アカウント情報の取得

AccountManagerからアカウント情報を取得する方法はいくつかあります。 ほとんどの方法でGET_ACCOUNTSパーミッションが必要です。

    <uses-permission android:name="android.permission.GET_ACCOUNTS"/>

アカウント情報は基本的にAccountクラスで管理されます。 このクラスはnametypeの2つの値を持っており、nameはアカウント名、typeはそのアカウントが属するサービスの種類となります。 例として、Googleのアカウントの場合、nameはuser@gmail.com、typecom.googleが格納されています。

すべてのアカウント情報を取得

すべてのアカウント情報を取得するにはAccountManager#getAccounts()を使用します。 通常のアプリでは利用するサービス種別が決まっているため、この方法を使うことはあまりないでしょう。 取得できるアカウント情報はAccountクラスの配列となっています。

    AccountManager manager = AccountManager.get(context);
    Account[] accountArray = manager.getAccounts();

サービス種別を指定してアカウント情報を取得

サービス種別を指定してアカウント情報を指定するにはAccountManager#getAccountsByType()を使用します。 この時指定するtypeAccount#typeと同じもので、サービスごとに決められた文字列です。 例として、Googleのアカウントであれば以下のように取得します。

    AccountManager manager = AccountManager.get(context);
    Account[] accountArray = manager.getAccountsByType("com.google");

特定の機能を持ったアカウント情報を取得

あるサービス種別のアカウントのうち、さらに特定の機能を持ったアカウントのみを取得したい場合、AccountManager#getAccountsByTypeAndFeatures()を使用します。 このメソッドは、「Gmailを利用することができるGoogleユーザ」といったような指定に使います。 https://developers.google.com/gmail/android/

また、このメソッドのみAccountクラスの配列ではなくAccountManagerFutureを返しますが、このAccountManagerFutureはメインスレッドで扱ってはいけないため、コールバックを設定できるようになっています。

// Get the account list, and pick the first one
final String ACCOUNT_TYPE_GOOGLE = "com.google";
final String[] FEATURES_MAIL = {
        "service_mail"
};
AccountManager.get(this).getAccountsByTypeAndFeatures(ACCOUNT_TYPE_GOOGLE, FEATURES_MAIL,
        new AccountManagerCallback() {
            @Override
            public void run(AccountManagerFuture future) {
                Account[] accounts = null;
                try {
                    accounts = future.getResult();
                    if (accounts != null && accounts.length > 0) {
                        String selectedAccount = accounts[0].name;
                        queryLabels(selectedAccount);
                    }
ru

                } catch (OperationCanceledException oce) {
                    // TODO: handle exception
                } catch (IOException ioe) {
                    // TODO: handle exception
                } catch (AuthenticatorException ae) {
                    // TODO: handle exception
                }
            }
        }, null /* handler */);

アカウント選択ダイアログの表示

API level 14以降では登録済みアカウントから選択、または新しいアカウントを追加させるダイアログを表示させることができます。 このメソッドの実行には権限が必要ありませんが、アカウントの選択ダイアログはOS標準のUIとなり、選択結果もonActivityResultで受ける形になります。

 @Override
    public void onClick(View v) {

        Intent intent = AccountManager.get(this).newChooseAccountIntent(null, null, new String[] {
                "com.google"
        }, false, null,
                null, null, null);
        startActivityForResult(intent, REQUEST_CODE);

    }

    @Override
    protected void onActivityResult(final int requestCode, final int resultCode, final Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        {
            if (requestCode == REQUEST_CODE && resultCode == Activity.RESULT_OK)
                Toast.makeText(this, data.getStringExtra(AccountManager.KEY_ACCOUNT_NAME),
                        Toast.LENGTH_SHORT).show();
        }
    }

アカウントの編集

AccountManagerではアカウントの追加、削除などを行うことができます。 そのいずれの方法でもMANAGE_ACCOUNTSパーミッションが必要です。 以下では、アカウント編集に関する代表的なメソッドを紹介します。

    <uses-permission android:name="android.permission.MANAGE_ACCOUNTS"/>

アカウントの追加

サービス種別を指定してアカウントの追加を行うには、AccountManager#addAccount()を利用します。 通常、このメソッドを実行するとサービス種別に対応したアカウントの登録画面が表示され、そこでアカウント情報を入力してログインし、アカウントを追加する形になります。 従って、このメソッドでもコールバックを利用して結果を受け取る方法を使います。 ログインに成功した場合、KEY_ACCOUNT_NAMEKEY_ACCOUNT_TYPEを受け取ることができます。 ここで第二引数に指定した"mail"はauthTokenTypeという引数で、後にトークンを取得する際に重要になります。

        AccountManager manager = AccountManager.get(this);
        manager.addAccount("com.google", "mail", null, null, activity,
                new AccountManagerCallback<Bundle>() {
                    @Override
                    public void run(AccountManagerFuture<Bundle> future) {
                        try {
                            Bundle bundle = future.getResult();
                            String accountName = bundle.getString(AccountManager.KEY_ACCOUNT_NAME);
                        } catch (OperationCanceledException e) {
                            e.printStackTrace();
                        } catch (IOException e) {
                            e.printStackTrace();
                        } catch (AuthenticatorException e) {
                            e.printStackTrace();
                        }
                    }
                }, null);

アカウントの削除

アカウント情報を指定して削除を行うには、AccountManager#removeAccount()を利用します。 このメソッドでは削除の成功可否booleanをコールバックで受け取ります。 一般的にアカウント情報を指定するためにはgetAccountsByTypeを利用するため、GET_ACCOUNTSパーミッションが必要ですが、nametypeが最初からわかっている場合、Accountを直接指定することができます。 実際に下記の例ではAccountを直接生成していますが、このような場合はMANAGE_ACCOUNTSパーミッションのみで動作します。

    AccountManager manager = AccountManager.get(this);
    manager.removeAccount(new Account("user@gmail.com","com.google"),
        new AccountManagerCallback<Boolean>() {
            @Override
            public void run(AccountManagerFuture<Boolean> future) {
                try {
                  boolean result = future.getResult(); 
                } catch (OperationCanceledException e) {
                    e.printStackTrace();
                } catch (IOException e) {
                    e.printStackTrace();
                } catch (AuthenticatorException e) {
                    e.printStackTrace();
                }
            }
        }, null);

アカウントの無効化

AccountManagerではアカウントのユーザIDとパスワードを保存している場合があり、トークン期限が切れた場合でもこれらの情報でトークンの再取得を行っています。 何らかの理由でこの再取得を無効化したい場合、AccountManager#clearPassword()を利用して保存されているパスワードをリセットすることができます。 このメソッドもアカウントを直接指定する場合にはMANAGE_ACCOUNTSパーミッションのみで動作します。

        AccountManager manager = AccountManager.get(this);
        manager.clearPassword(new Account("user@gmail.com","com.google"));

トークンの取得

AccountManagerでは、指定したアカウントからサービスに接続するためのトークンを取得することができます。 この仕組により、AccountManagerで管理されているアカウントでWebサービスを利用する場合、同一端末でのログインは初回のみで良いことになります。 トークンの取得にはUSE_CREDENTIALSパーミッションが必要です。 また、アカウントを直接指定した場合でもGET_ACCOUNTSが必要な場合があるようなのでこちらもあったほうが良いでしょう。

    <uses-permission android:name="android.permission.GET_ACCOUNTS"/>
    <uses-permission android:name="android.permission.USE_CREDENTIALS"/>

トークンを取得するメソッドはいくつかありますが、下記では最も単純なgetAuthToken()を例にあげます。 このメソッドを最初に実行したとき、アプリがアカウントにアクセスすることを承認するかどうかをユーザに確認する画面が表示されます。 この承認画面により、AccountManagerではユーザが許可しない限りはトークンを取得できないという仕組みになっているのです。 また、第二引数のauthTokenTypeは取得するトークンの種別を定義するもので、必ず指定する必要があります。 通常、このauthTokenTypeAccountManager#addAccount()で指定するauthTokenTypeと同じものになります。

        AccountManager manager = AccountManager.get(this);
        manager.getAuthToken(new Account("user@gmail.com", "com.google"), "mail", null,
            this, new AccountManagerCallback<Bundle>() {
                @Override
                public void run(AccountManagerFuture<Bundle> future) {
                    Bundle bundle = null;
                    try {
                        bundle = future.getResult();
                        String accountName = bundle.getString(AccountManager.KEY_ACCOUNT_NAME);
                        String accountType = bundle.getString(AccountManager.KEY_ACCOUNT_TYPE);
                        String authToken = bundle.getString(AccountManager.KEY_AUTHTOKEN);
                        Toast.makeText(MyActivity.this,authToken,Toast.LENGTH_SHORT).show();
                    } catch (OperationCanceledException e) {
                        e.printStackTrace();
                    } catch (IOException e) {
                        e.printStackTrace();
                    } catch (AuthenticatorException e) {
                        e.printStackTrace();
                    }
                }
            }, null);

注意点

AccountManagerを安全に使う上で、気をつけなければいけないことがあります。 以下はセキュリティの確保のために最低限注意しなければならない事柄です。

取得したトークン情報を永続化しない

AccountManager#getAuthToken()で取得したトークンを保存してはいけません。 必要にった時にAccountManager#getAuthToken()で取得しなおして下さい。 AccountManagerでは有効期限内のトークンをキャッシュしているため、通常はトークン再取得に時間はかかりません。

取得したトークン情報を他のアプリに共有しない

AccountManager#getAuthToken()で取得したトークンを他のアプリと共有してはいけません。 承認画面でユーザが確認できるのは AccountManager#getAuthToken()を実行したアプリのみです。 他のアプリでトークンを使わせることはユーザを裏切ることになります。

取得したトークンが使えない場合、トークンを無効化する

取得したトークンでAPIアクセスに失敗する場合、トークンが無効になっている場合があります。 このような場合でも有効期限が切れるまではAccountManagerでキャッシュされてしまうため、アプリで検知した場合はAccountManagerに無効なトークンであることを通知する必要があります。 AccountManager#invalidateAuthToken()を実行することで、指定したトークンのキャッシュを無効にできます。 AccountManager#invalidateAuthToken()実行後にAccountManager#getAuthToken()を実行した場合、AccountManagerは新しいトークンを取得して返します。

AccountManager manager = AccountManager.get(this);
manager.invalidateAuthToken("com.google",TOKEN);