Android 4.4 NFCホストカードエミュレーションへの道 (導入編)

本記事は、Android 4.4 KitKat 冬コミ原稿リレーというちょっと気の早いアドベントカレンダー的なものの11/8担当分となります。

Android 4.4にて導入された Host card emulation (以下、HCE)を試したいけど、まず何をすれば良いのかわからないという方向けの導入編となります。

本記事よりも少し踏み込んだ話については、TechBooster様から冬コミに発行されるAndroid 4.4本に寄稿する予定です。

そもそもホストカードエミュレーションとはなにものか?

これまでも、(交通系カードや電子マネーカードなどのように)Android端末が非接触ICカードとして振る舞うことができていましたが、Androidアプリとして独自のカード機能を追加するようなことができませんでした。

Android 4.4 (API Level 19)からは、Androidアプリとして非接触ICカードのふりをするアプリを作成することが可能になりました。NFC規格に含まれる通信方式のうちType A/Bに対応し、通信コマンドはISO 7816-4に規定されたAPDUコマンドを用います。

カードをエミュレートするAndroidアプリがあると何がうれしいのか?

たとえば、以下のような利用方法が考えられます。

  • ICカードアプリ開発のプロトタイピング
  • ICカードを利用した既存システムと、Andorid端末の連携システムの開発

高機能なデバッガを利用できる環境でカードアプリのプロトタイピングができるというのは、システム構築の一部としてカスタマイズしたICカードを開発されている現場では役立つのではないでしょうか。

また、HCEはISO7816-4という標準仕様ベースですので、その標準を用いた既存システムとAndroid端末の連携に利用するということも考えられます。

出来るとしても気軽に手を出してはいけないものはなにか?

この機能を用いて電子マネーシステムを新たに作ろうとするのは高いリスクを伴います。

既存の電子マネーは、Secure Element (以下、SE)と呼ばれる耐タンパ性の非常に高いチップによって、決済に必要となる情報の保護や暗号化の処理を行っています。これと同等レベルのことをソフトウェアで実現することは不可能です。ソフトウェア耐タンパ技術といったものもありますが、ハードウェアによる保護よりも破られるリスクは非常に高いものです。 ((そのリスクを負ってでもチャレンジしようとされる会社さんは出てくると思いますが。))

HCE (API Level 19)でやりやすいこと

いろいろありますが、「Type 4のNFC Forum Tag のエミュレーション」は比較的簡単にできることの一つです。

NFC Forum TagのType 4というのは、ISO7816-4をベースにしていますし、タッチすれば誰でも読めるタグということでセキュリティがかかっていないためです。

HCE (API Level 19)でできないこと

「Felicaを利用したカードのエミュレーション」はできません。FelicaはISO7816-4とは異なるコマンド体系を持っているためです。特に日本ではFelicaベースのシステムが普及しているため、残念な仕様かもしれませんが諦めてください。 ((最近の非接触ICカードR/WはTypeA/B/Felica全対応のものも多いのでハードウェアは使いまわせる場合が多いでしょう。))

NFC HCEアプリ開発のハマリポイント

実は、GoogleのHCEドキュメントですが、間違いがあります(11/8現在)。この通りに実装してもHCEは動作しません。

HCEはサービスとして実装しインストールします。AndroidMannifest.xmlに適切に宣言することで、AndroidシステムがそのHCEサービスを起動してくれます。 ((正しく実装されていれば、インストール後すぐにサービスが起動します。))

しかし、前述のドキュメントには以下のようなマニフェスト断片が提示されていますが、抜けがあるため、システムによるサービスの起動が行われません。自分でサービスを起動しても権限が適切ではないため動作しません。

<service android:name=".MyHostApduService" android:exported="true"
        android:permission="android.permission.BIND_NFC_SERVICE">
    <intent-filter>
        <action android:name="android.nfc.cardemulation.action.HOST_APDU_SERVICE"/>
    </intent-filter>
    <meta-data android:name="android.nfc.cardemulation.host_apdu_service"
        android:resource="@xml/apduservice"/>
</service>

何が間違っているかというと、intent-filter にてカテゴリが宣言されていないためです。以下のようにカテゴリを追加してください。

<service android:name=".MyHostApduService" android:exported="true"
        android:permission="android.permission.BIND_NFC_SERVICE">
    <intent-filter>
        <action android:name="android.nfc.cardemulation.action.HOST_APDU_SERVICE"/>
        <category android:name="android.intent.category.DEFAULT" />
    </intent-filter>
    <meta-data android:name="android.nfc.cardemulation.host_apdu_service"
        android:resource="@xml/apduservice"/>
</service>

また、NFCを利用するため、以下のpermission宣言も必要です。忘れないようにしましょう。

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

カードアプリを書くために必要な情報

さて、これでカードのふりをするサービスを起動するところまでは出来ました。ではここからどうやってカードアプリを書けばいいでしょうか?

カードアプリには、必ず対向するシステム(たとえばNFCタグリーダ、クーポン読み取り機など)があります。そのシステムが求める機能を実現してください。

え? それでは不親切すぎる? ではとりあえず何か試してみたいということであれば、Type 4 の NFC Forum Tag を試しに実装してみてはいかがでしょうか。仕様書はNFC Forumから入手可能です。 ((弊社でも現在お試し実装にチャレンジ中です。))

Type 4のNFC Forum Tagの仕様書だけあれば、ISO7816-4のAPDUの詳しい知識が無くても実装できます。

暗号化や認証の機能もカードアプリとそれを用いるシステムにて行いたいのであれば、ISOから7816の仕様書(もしくはそれに対応したJIS規格書)を購入する必要が出てくるでしょう。 ICカードに関するISOとJISの対応関係は、こちらのサイトが詳しいです。

実際にお試しいただく際にあると良いもの

  1. NFC HCEに対応したAndroid 4.4端末
  2. 非接触ICカードR/W側となるAndroid 4.4端末

1は11/8時点ではNexus5しか存在しないかもしれません。2はGalaxy NexusにAOSPのAndroid 4.4をビルドして焼いたGalaxy NexusでOKでした。

自分でビルドしても当然良いのですが、@androidsola さんがビルドしたイメージを以下のページにて公開していらっしゃいますので、そちらを利用させていただくのも良いでしょう。

なお、Galaxy Nexus + 前述のAOSP版4.4 ROMイメージでは、Host card emulation非対応のようです。 ((対応端末では、NFCをONにしていれば、設定画面の「アプリ」の下に「タップ&ペイ」というメニュー項目が表示されるのですが、それが表示されませんでした。))

もう一つのハマリポイント、カードリーダによる動作確認

Android端末は古いものでも、NFC対応であればNFC Forum Tagを読み取れるはずと思った方もいるでしょう。NFC Forumタグを実現したHCEアプリを確認するのに、なぜ4.4端末をもう一台用意しないといけないのかと。

実は、NFC Forum Tag を実現したHCEアプリを、API Level 18以下のNFC対応端末に近づけた場合、せっかく作ったHCEアプリが呼び出されません。

なぜそんなことになるかというと、NFCをオンにしたAndroid端末同士を近づけると、まずNFCで規定されたLLCPというプロトコルで接続をしてしまうのです。LLCPが先に解決されてしまい、TypeA/BによるNFC Forum TagのHCEアプリにまで辿り着かないのです。

これを回避するためには、API Level 19にて NfcAdapterに追加されたAPIを用いて、R/W側の端末アプリを作らなければなりません。このことはNfcAdapterのenableReaderMode()メソッドのリファレンスにも以下のように記載されています。

For interacting with tags that are emulated on another Android device using Android’s host-based card-emulation, the recommended flags are FLAG_READER_NFC_A and FLAG_READER_SKIP_NDEF_CHECK.

enableReaderMode()はAPI Level 19からしか使えませんので、どうしてももう一台Android 4.4端末が必要となるのです。

以下に、NFC Forum Tagを確認するR/Wアプリを作るために役立つコード断片を記しておきます。参考にしてください。

public class MainActivity extends Activity {
    .... 
    private NfcAdapter mNfcAdapter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ......

        mNfcAdapter = NfcAdapter.getDefaultAdapter(getApplicationContext());
    }

    @Override
    protected void onResume() {
        super.onResume();

        mNfcAdapter.setNdefPushMessage(null, this);

        mNfcAdapter.enableReaderMode(this, new NfcAdapter.ReaderCallback() {
            @Override
            public void onTagDiscovered(Tag tag) {
                // TODO: ここに受け取ったタグに対しての処理を記載する
            }
        }, NfcAdapter.FLAG_READER_NFC_A, null);
        // NDEFタグ (NFC Forum Tag)を対象にしているので、ここでは enableReaderMode()に
        // 記載されたフラグ FLAG_READER_SKIP_NDEF_CHECK は使わないで、システムに
        // NDEFタグのスキャンを任せる。
    }

    @Override
    protected void onPause() {
        super.onPause();

        mNfcAdapter.disableReaderMode(this);
    }

最後に

本記事の情報を用いることで、ホストカードエミュレーションを試す入り口までたどり着くことはできるのではないかと思いますが、実際にそれを用いてアプリを実装するにはICカード(特にISO7816系コマンド)の知識を必要とします。

また、NFC Forum Tagカードのような静的なものを除けば、カードアプリだけを作ることには意味がありません。システムと連携することが非常に重要です。非接触ICカードの特徴の一つは、近接距離、すなわちタッチ動作をユーザの意思表示として利用するというところにあります。この性質によってユーザ体験(UX)を改善できるシステムでなければ、Bluetoothなどの他の通信を利用したシステム構築も選択肢とすべきです。

と、少しハードルを上げてしまいましたが、何はともあれどんな技術についても言えることですが、まずは触れてみましょう。その上で、自分が作りたいものに向いているのかいないのかを考えましょう。

触れてみて理解してから導入可否を検討するのは大変だ? その場合、弊社にて相談に乗れるかもしれませんのでお気軽にご連絡ださい(とさりげなく宣伝)。

2D/2.5Dゲームエンジン Playgroundのセットアップ

KLab株式会社さんが、9/25付で2Dゲーム(および2.5Dゲーム)用のゲームエンジンであるPlaygroundをオープンソースで公開されました(KLabさんによるプレスリリース )。

さっそく動作を確認してみようとされた方も多いと思いますが、Android版はハードルの高さに挫折された方も多いのではないでしょうか。弊社でも苦労しました。
まずドキュメントが英語です。この時点ですでに振り落とされる方も多いでしょう。そして、9/26時点でのドキュメントにはいくつか不備があります。

弊社では試行錯誤の結果、MacOS Xとその上で動作させているWindows7 VMを組み合わせて、Playgroundチュートリアルとして公開されているサンプルのビルドおよびAndroid上での動作確認まで行うことができました。その備忘録として、セットアップ手順をまとめておこうと思います。おそらく、数日中にPlayground側のドキュメントも改善され、この備忘録も不要になっていくだろうと思いますが。

(9/28追記: すでにかなりの修正がPlaygroundOSSで行われているので、ハマりポイントはこの記事執筆時点よりも減っていってます。)

Playgroundの取得

まずはgithubで公開されているPlaygroundのリポジトリからソースを取得します。ついでに作業用ブランチを作成します。

% git clone https://github.com/KLab/PlaygroundOSS.git
% cd PlaygroundOSS
% git checkout -b working

masterブランチ上で作業していて衝突(conflict)が発生すると面倒だったりする場合がありますので、ここでは念のためローカルブランチを作って作業しています。

Playgroudに含まれるもの

取得してきたものがそれぞれどういう役割を持っているか知っていた方がこれからどういう作業をしているのか理解しやすくなるので、少し説明します。

CSharpVersion
C#での開発に関連したファイルが入っています(調査中)。
Doc
ドキュメント類が入ってます。基本的に英語ドキュメントなので読むのに苦労します。また、9/26現在ではまだAndrodでのビルド方法などのドキュメントに不備がありますので、気をつけてください。
Engine
Playgroundのゲームエンジンそのものです。この下にある各プロジェクト用のひな形ファイルと、後述するassetsを組み合わせることで実行できるアプリをビルドすることができます。
SampleProject
紛らわしいですが、これはサンプルアプリが入っているディレクトリではありません。後述するエンジンのビルド時に用いるファイルが格納されています。エンジン本体をビルドする際には、このディレクトリ名を指定しなければなりません。
Tools
Windows用のassets作成ツールTobogganが入ってます。Tutorial以下に入っているサンプルをビルドするためには、このツールを使ってassetsを作成しないといけません。どうやらWindowsを用いなくても、monoでassetsの発行はできるらしいです(調査中)。
Tutorial
Luaを用いたサンプルプログラムがまとめられたzipファイルが入っています。本記事ではこちらのサンプルのビルドと実行までを確認します。

AndroidでPlayground を試すのための事前準備

開発環境インストール

Android SDKのインストール、Elicpseのインストール、EclipseへのADTのインストール、Android NDKのインストールを行ってください。
それぞれのインストールと設定の手順については本記事では省略します。各種のAndroid開発入門サイトや書籍を参考にしてください。

環境変数の設定

  • 環境変数 ANDROID_NDK_ROOT にNDKの場所を設定してください。.profileや.bash_profileなどに以下のような行を追加すると良いでしょう。

    export ANDROID_NDK_ROOT=$HOME/android-ndk-r9

  • 同様に、環境変数 ANDROID_SDK_ROOT にSDKの場所を設定してください。

    export ANDROID_SDK_ROOT=$HOME/adt-bundle-mac-x86_64-20130917/

  • 環境変数PATHにNDKとSDKを追加してください。

    PATH=$PATH:$ANDROID_NDK_ROOT:$ANDROID_SDK_ROOT/platform-tools

Playgroundエンジンのビルド

githubから取得した Playgroundは $HOME/PlaygroundOSS にあるという前提で説明します。

以下のコマンドを実行します(chmod は初回のみ必要です)。

% cd $HOME/PlaygroundOSS/Engine/porting/Android/GameEngine-android
% chmod +x ./build.py
% ./build.py --rebuild --project SampleProject

引数に指定するプロジェクト名は、SampleProjectでなければ成功しません。他のプロジェクト名を使いたい場合は、前述したSampleProjectディレクトリを複製して別の名前に変更し、さらには中のファイルもいろいろいじらないといけないと思われます。

ここではとりあえずサンプルのビルドと実行を目指していますので、上記のカスタマイズ方法については追求しないこととします。

環境設定が正しく行われていれば、$HOME/PlaygroundOSS/Engine/porting/Android/GameEngine-android/libs/ 以下の x86ディレクトリとarmeabiディレクトリの下に libGame.so ができるはずです。

Tobogganを用いて assets を発行する

ゲームエンジンはできましたが、それだけではゲームは動きません。ゲームの動作を記述したプログラムとデータを組み込む必要があります。
動作確認できるサンプルがTutorialディレクトリに用意されていますので、それで試しましょう。

先に述べたTobogganアプリを用いて、ゲームエンジンが利用できる形にassetsを発行する必要がありますので、ここからの作業はWindows上で行います。

Toboggan のインストールと実行

  • Tools/Toboggan-Tools.zip を展開してください。その中にできる Toboggan ディレクトリを適当な場所に置いてください。
  • Toboggan/KLBToolHost.exe を実行してください。

assetsの発行(publish)

  • Tutorial/Samples.zip をどこかに展開してください。たくさんのディレクトリができてしまいますので、どこか展開用のディレクトリを作成して、そこに展開するようにした方が良いかと思います。 9/28追記: 最新のリポジトリではすでに展開済みになっていますので、この手順は不要です。
  • 実行しているTobogganから、01.SimpleItemを開きます。 (メニューの File -> Open)
  • メニューの Tools -> Setup で開かれるダイアログに “Publish Targets” という項目がありますので、androidにチェックを入れてください。
  • メニューの File -> Publish All で発行処理を行ってください。
  • 01.SimpleItem/.publish/android ディレクトリに発行されたデータがありますので、このディレクトリをまるごとMacOS X環境にコピーしてください。

発行されたassetsをまとめる

ここから再び、作業環境はMacOS Xに戻ります。

前項で発行されたassetsは、AppAssets.zip というファイルにまとめなければなりません。Windowsでもこの作業は可能かとは思いますが、念のため、Playgroundの手順に従って、zip コマンドを用いて行いましょう。以下のコマンドを実行して下さい。

% cd .publish/android
% zip -0 -r <どこか適当なディレクトリ>/AppAssets.zip ./*

zip の最初のオプションは-O (オー)ではなく -0 (ゼロ)です気をつけてください。これは非圧縮でzipファイルを作成するためのオプションです。

また、再帰的にサブディレクトリもアーカイブしておかないと、参照するデータがAppAssets.zipに含まれないという事故も起こってしまうので、-r オプションもつけましょう。 

Eclipseへのプロジェクトのインポート

Eclipse に Playground 用の Androidプロジェクトを作成しなければなりませんが、これはすでに用意されているコードをEclipseにインポートすればOKです。英語版Eclipseのメニューで説明しますので、日本語化されている場合は適宜読み替えてください。

  • File -> Import を選択し、開いたダイアログで Existing Android Code into Workspace を選びNextボタンで次に進みます。
  • Browsボタンを押して、$HOME/PlaygroundOSS/Engine/Android/GameEngine-android を選択します。
  • Copy projects into workspace にチェックを入れ、Finishボタンを押します。

AppAssets.zip のEclipseプロジェクトへの組み込み

インポートしたElicpseプロジェクトは、Playgroundのゲームエンジンが入っているだけで、ゲームプログラム部分はまだ入っていません。それは AppAssets.zip としてまとめたものになりますので、このファイルをプロジェクトの assets フォルダに入れてください。

また、プロジェクトのassetsフォルダに version というファイルを新規作成し、その中に 1 とだけ記載してください。Playgroundのドキュメントに記載されている通り、 echo コマンドを用いて version ファイルを作成しても構いません。

AppAssets.zip を別のファイルに入れ替える際には、versionファイルの中身も書き換えましょう(たとえば数字をインクリメントしましょう)。AppAssets.zip 入れ替え前と違う数になっていればOKです。

プロジェクトのビルド

ここまででPlaygroundドキュメントに記載されているビルドできるプロジェクトの準備ができました。ではビルドしてみましょう。たいていの方はここで失敗します。

さて、何が悪いのでしょうか?実はインポートしたEclipseプロジェクトがあなたの環境にあっていないのです。

ということで、プロジェクトを選択し、右クリックしてコンテキストメニューからPropertiesを選んで設定画面を開いてください。以下の設定項目をご自身の環境に合わせて修正してください。

  • C/C++ の下のEnvironmentを選び、右側に表示された環境変数の一覧のうち、ANDROID_NDK_ROOT, NDKROOT, NDK_ROOTをご自身の環境に合わせて修正します。
  • C/C++ を選び、右側に表示された Build Command の項目を、${ANDROID_NDK_ROOT}/ndk-build -j に修正します。

プロジェクトの実行

ビルドが成功したら実行してみましょう! PlaygroundゲームエンジンはOpenGLを用いているので、Androidではエミュレータではなく実機を使った方が良いでしょう。

最後に

ビルド手順についてまとめたドキュメントの不備については、すでにPlaygroundのオープンソースプロジェクトの方に伝達していますので、Github上のドキュメントも今後改善されていくでしょう(9/28追記:改善作業が着々と進んでいます)。

オープンソースの2Dゲームエンジンとしては、cocos2d-x が有名です。また製品では、Unity も今秋の4.3から2Dゲーム開発を支援する機能を提供していくことが発表されています。Playground はそれらと競合するという位置づけです。

プレスリリースによると、Playgroundゲームエンジンは、音の遅延が小さく、リズムアクションゲームなどに適しているなどの強みがあるとのことです。

Playgroundについては、すでにハッカソンの企画も動いています。弊社もハッカソンの参加や、開発情報の共有などを通じて貢献できればと考えております。

Android: NotificationListenerServiceとAccessibilityServiceの違い

Android 4.3から導入された NotificationListenerService で出来る機能は、AccessibilityServiceでも実現できる場合があります。

新しい NotificationListenerService を使わなくてもできるのであれば、昔ながらのやり方をすれば良いと考えられるかもしれませんが、新しいOSバージョンでは NotificationListenerService を利用し、どうしても古いバージョンでも対応する必要がある場合は AccessibilityService を併用することをおすすめします。

NotificationListenerService の利用を推奨する大きな理由は以下の通りです。

  • 通知管理のほとんどをシステム側に任せられ、またできることが増える
  • セキュリティメニューによって有効・無効が管理されるため、ユーザに通知管理をダウンロードしたアプリに任せることのセキュリティリスクを意識させやすい

業務用アプリであれば、対象端末をAndorid 4.3以上に限定して開発することで、工数を大幅に削減することも可能となるでしょう。システムを設計する際に、ソフトウェアの開発工数とAndroid 4.3端末の導入にかかる費用を合算して検討することで、お客様にとって最適なシステムを提案できるでしょう。 ((新型Nexus7など、Android 4.3を指定した端末の導入は現実的な選択肢になってきていると、弊社では考えております。))

Google Playで広いユーザに対して提供するアプリを開発しなければならない場合に、通知管理機能を開発しなければならない場合は、引き続きAccessibilityService も利用しての開発を頑張りましょう。

通知管理について

AccessibilityService を利用して、裏技的に通知を管理しようとする場合、通知が届く度にその情報をそのアプリ内で管理する情報として保持し、アプリが起動される度にそのアプリの通知情報を削除するといったことをアプリ内で行う必要があります。それをちゃんと設計・実装するには大きな工数が必要となります。

また残念ながら、管理のためにたくさんのコードを書いたとしても、通知バーに表示されている情報とアプリが管理している情報を正確に一致させることはできません。

NotificationListenerService を利用する場合、以下のことができるので、アプリ側で行う処理を非常にシンプルにすることが可能となりますし、アプリ内の情報を通知バーの情報と一致させることは容易です。

  • getActiveNotifications() で有効なすべての通知を取得する
  • cancelAllNotification() や cancelNotification() で通知を削除する
  • onNotificationPosted() で新しい通知の到着をトリガとして処理を実行する
  • onNotificationRemoved() で通知の削除をトリガとして処理を実行する

通知の即時実行と、実行後の自動削除を行うサンプルコード

以下のコードサンプルは、通知が届いたらすぐに通知をタップしたのと等価な処理を実行するものです。その時、通知が削除可能かどうか、タップした時に自動削除することを要求しているか確認して、どちらもtrueなら通知を削除する処理を呼び出しています。

@Override
public void onNotificationPosted(StatusBarNotification sbn) {
    Notification notification = sbn.getNotification();
    Intent intent = new Intent();
    PendingIntent pendingIntent = notification.contentIntent;
    try {
        pendingIntent.send(this, 0, intent);

        int id = sbn.getId();
        String tag = sbn.getTag();
        String packageName = sbn.getPackageName();
        int flags = sbn.getNotification().flags;

        // 通知がキャンセル可能、かつユーザによる通知タップ時に
        // 消える設定の場合に通知を消す
        if (sbn.isClearable() &&
            flags & Notification.FLAG_AUTO_CANCEL) {
            cancelNotification(packageName, id, tag);
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
}

Android 4.3: NotificationListenerServiceマニアックスとAndroidセキュリティ観測

Android 4.3では、NotificationListnerService という、アプリが、他のアプリも含めて通知バーに追加された通知の情報を取得することが出来るという機能が追加されています。

このNotificationListnerServiceとリフレクションを用いたサンプルアプリをgithub上に公開しております

本記事では、NotificationListenerServiceとはどういうものか、基本的な使い方と踏み込んだ使い方、そしてNotificationListenerServiceを含めたAndroidのセキュリティについての考え方の方針転換の可能性についてを書きたいと思います。

NotificationListenerServiceとは?(基本的な使い方)

4.2以前のAndroidでは、自分のアプリの通知を通知バーに登録することは出来ました。4.3では、他のアプリが通知を行ったことを知り、それをトリガにして処理を実行することが出来るようになりました。 ((裏ワザ的なAccessibilityService使用によって、同様のことは出来たりはしますが))

ユーザは以下のように NotificationListnerService の派生クラスを作り、

public class MyListenerService extends NotificationListenerService {

    @Override
    public void onNotificationPosted(StatusBarNotification sbn) {
        /* 通知が追加された時に呼ばれる */
    }

    @Override
    public void onNotificationRemoved(StatusBarNotification sbn) {
        /* 通知が通知バーから削除された時に呼ばれる */
    }
}

AndroidManifest.xml にその派生クラスがNotificationListnerServiceの権限でのアクセスのみを受け付けることを宣言します。

    <service
        android:name=".MyListenerService"
        android:label="@string/app_name"
        android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE">
        <intent-filter>
            <action android:name="android.service.notification.NotificationListenerService" />
        </intent-filter>
    </service>

これによって、MyListenerSeriviceはBIND_NOTIFICATION_LISTENER_SERVICE のpermissionを持つものからの要求のみを受け付けるようになります。
BIND_NOTIFICATION_LISTNER_SERVICE は一般ユーザが使用することが出来ないprotectionLevelを持っているため、このサービスの機能を呼び出すことが出来るのはAndroid 4.3のシステムに組み込まれて適切な権限を持っているものからのみとなります。

前述のpermission宣言により、アプリ自身からもこのサービスを起動することは出来ませんが、[設定]->[セキュリティ]->[通知へのアクセス]の設定に従って自動的にシステムにより起動されます。以下はセキュリティ設定画面のキャプチャです。

security-setting

「通知へのアクセス」という項目は、実際にNotificationListnerServiceを用いたアプリを手順に従って端末にインストールするまではあらわれません。
最初、アプリをインストールしただけでは、そのアプリの使用許可は与えられていない状態です(下図参照)。

setting-notificationlistener01

チェックボックスをオンにすることでインストールされたアプリを有効化することが出来ますが、その時に非常に刺激的な警告文が表示されます。

setting-notificationservice02

非常にいろんなことが出来るので、信頼出来るアプリにしか許可を与えてはいけないという警告文です。この警告を受けた上でOKボタンを押すと、チェックボックスが有効になり、インストールしたアプリのNotificationListnerServiceを派生したサービスが起動します。逆にチェックボックスを外すと、サービスは停止します。

このようにして、システムから起動されたサービスは新しい通知が通知バーに登録される度にonNotificationPosted()が呼び出され、通知が削除される度にonNotificationRemoved()が呼び出されますので、それらをトリガとして処理を行うことが可能となります。

NotificationListnerServiceの踏み込んだ使い方

NotificationListenerService#onNotificationPosted()の引数として渡されるStatusBarNotificationからは、通知元のパッケージ名や通知が作成された時間、Linux UID、Notificationクラスのインスタンスなどを取得することが出来ます。しかしそれらの情報を用いてできることはそんなにたいしたことではありません。先ほど、機能を有効にする際にダイアログで警告されたようなことはほとんど出来ません。

しかしあれは単なる脅しではありません。Androidでは非公開のAPIもリフレクションを用いて呼び出すことができます。リフレクションによって先のダイアログにて警告されたような機能を実現することは可能です(本記事では述べません)。

以下のサンプルコードは、StatusBarNotificationのインスタンスからメールが到着した時にそのGmailを表示するためにPendingIntentを取得し、そのPendingIntentに紐付けられたIntentを送る方法を示します。

public class MyListenerService extends NotificationListenerService {
    private static final String TARGET_APP_PACKAGE = "com.google.android.gm"; // Gmail Application

    @Override
    public void onNotificationPosted(StatusBarNotification sbn) {
    // return if the notification isn't from the target
    if (!TARGET_APP_PACKAGE.equals(sbn.getPackageName())) {
        return;
    }

    Notification notification = sbn.getNotification();

    Intent intent = new Intent();
    PendingIntent pendingIntent = notification.contentIntent;
    try {
        pendingIntent.send(this, 0, intent);
    } catch (PendingIntent.CanceledException e) {
        e.printStackTrace();
    }
}

実際に動作するサンプルコード全体は github で公開しておりますので、そちらをご参照ください。

NotificationListenerService のセキュリティ

従来のpermissionモデルであれば、ユーザがアプリをインストールした時点で、そのアプリが宣言していた機能をすべて使用させることになりますが、NotificationListenerServiceはその方法で通知バーの情報を保護しません。

通知バーにアクセスをする機能を持ったアプリをユーザがインストールし、なおかつ、ユーザが明示的に通知バーへのアクセスを許可するように設定を変更することを求めます。そして、その設定変更時には非常に強い警告を表示することで、ユーザが安易に設定を変更しないことを求めています。

このようにして、アプリのインストール後にユーザがアプリに対して機能の利用をON/OFFするというのは、弊社ブログでも紹介したAppOpsでも行われています。これは、Androidのセキュリティについての考え方が変化してきている兆候だろうと弊社では考えています。

騙してインストールさせて、電話帳のデータや端末固有情報(機体番号や電話番号)などを吸い出し、サーバに勝手に送信してしまうようなアプリが社会的な問題となりました。そのため、アプリ作成時に利用する権限を宣言し、インストール時に確認をさせるだけでなく、インストール後にも与えた権限を剥奪出来るようにすることや、重大な機能はユーザが明示的に許可するまでは使用できなくするようにGoogleはAndroidのセキュリティについての考え方を変化させつつあると判断しています。

また、通知バー広告を使ったアプリの排除など、Google Playを通じたアプリの配信でもセキュリティ強化は進んでいます。

特にGoogle Playを通じて配信する場合は、Androidでのセキュリティについての考え方の変化についてもしっかりと考慮し、誠実なアプリ開発を行っていくことが必要です。

Android 4.3 の隠し機能 AppOpsについていろいろ

AppOpsとは?

Android 4.3 には、まだ公開されていない隠し機能として AppOps というものが追加されています。これまでとは別のアプローチで、 ユーザが自分自身の情報(電話帳や位置情報など)を守ることが出来るようにするというものです。

追記(2013年11月5日):Android 4.4 KITKATでは、AppOps機能の入口となるActivityが設定アプリのAndroidManifest.xmlから削除(コメントアウト)されています。よって、素のAndroid 4.4 KITKAT では AppOps機能を使用することはできません。 ((当該Activityをコメントアウトをすることで機能を使えなくしているため、将来バージョンにて正式版として復活させる可能性が高いのではないかと思われます。))

追記(2013年11月5日その2):Android 4.3.1 の時点ですでに塞がれていたという情報をいただきました。
Android 4.3端末で設定を行った後に、4.3.1または4.4にアップデートするとその設定をもとに戻せなくなる可能性がありますので、アップデートを予定されている方(アップデートが提供される端末をお持ちの方)は、その点にご注意の上でお試しください。

追記(2013年11月21日):Android 4.3.1と4.4でもAppOpsを呼び出す方法が発見されました

AppOpsの紹介記事はすでにいろいろあるようですので、そちらを参照されると大まかな機能についてはわかるかと思います。たとえば、engadget日本語版の記事など。

従来のAndroid 4.2まででは、アプリ作成時にそのアプリがどういう権限を使いますよ(どういう機能を使いますよ)ということを宣言しておき、ユーザがインストール時(アップデート時)に、それを確認して問題が無いと判断すればインストールしていました。一旦インストールされてしまえば、アプリは宣言した権限の範囲内の機能を自由に使用することが出来ます。

4.3では、アプリのインストール後に、インストール時に許可した機能であってもアプリに使わせなくすることが出来るようにユーザが設定できる機能が、隠し機能として搭載されています。 ((Android 4.1で隠し機能としてマルチユーザ機能が搭載され、4.2にて正式機能として格上げされた事例がありますので、この機能も近く正式なものになるものと思われます。))

AppOps時代に、アプリ開発者が気をつけないといけないことはあるのかどうかは知りたいところです。ということで、Android 4.3の公開されているソースコードから読み取れる情報などからいろいろと調べてみます。 ((正式機能となった時には変わっているかもしれませんので、あくまでもAndroid 4.3向けの情報です。))

アプリからAppOps画面を呼び出す方法

Google Playで”Permission Manager”という名称で、隠されているAppOpsの設定画面を呼び出す機能を持ったアプリがいくつか出ていますが、単にその画面を呼び出すだけなら自分で作るのも簡単です。

    Intent intent = new Intent();
    intent.setAction(Intent.ACTION_MAIN);
    intent.setClassName("com.android.settings",
        "com.android.settings.Settings$AppOpsSummaryActivity");
    startActivity(intent);

単にこれだけでOKです。setAction()で指定するアクションはアプリでの呼び出し目的に合わせて調整する方がいいかもしれませんが。

AppOpsで制限された機能を使った場合、アプリはどうなる?

アプリが 、マニフェストファイルで uses-permission 宣言していない、権限が必要な機能を使用した場合、その機能を利用した瞬間に例外(SecurityException)が発生します。では、AppOpsでユーザが制限した機能を使用した場合はどうなるでしょうか。同様に例外が発生するでしょうか?

Android 4.3のソースコードを確認したところ、これまでのAPI仕様に記載されていないような例外を発生させないようにしつつ、その機能を使わせないようにしようという意図が見て取れます。たとえば、電話帳データへのアクセスなどの様々な機能のベースとして用いられているContentProviderを見てみます。そのインナークラスのTransportの query() メソッドは次のようになっています。

    @Override
    public Cursor query(String callingPkg, Uri uri, String[] projection,
        String selection, String[] selectionArgs, String sortOrder,
        ICancellationSignal cancellationSignal) {
        if (enforceReadPermission(callingPkg, uri) != AppOpsManager.MODE_ALLOWED) {
            return rejectQuery(uri, projection, selection, selectionArgs, sortOrder,
                CancellationSignal.fromTransport(cancellationSignal));
        }
        return ContentProvider.this.query(uri, projection, selection, selectionArgs, sortOrder,
            CancellationSignal.fromTransport(cancellationSignal));
    }

ここから呼び出されている enforceReadPermission() メソッドでは、呼び出し元アプリのパッケージとアクセスするContentProviderのURIから、アクセス可否を判定する処理を行って結果を返します。アクセスが許可されている(ユーザがAppOpsでアクセス許可をOFFにしていない)場合、AppOpsManager.MODE_ALLOWED が返ってくるので、それと比較して処理を分けています。

MODE_ALLOWEDではない値が返ってきた場合は rejectQuery() を呼び出してその結果を返し、そうでない場合は通常のクエリを行って結果を返します。rejectQuery() では何をしているかというと、以下のようなコードで、絶対に検索結果が空になるクエリを行った結果を返します。 ((‘A’ = ‘B’ という必ずfalseになる条件を追加したクエリにすることで実現しています。))

    public Cursor rejectQuery(Uri uri, String[] projection,
        String selection, String[] selectionArgs, String sortOrder,
        CancellationSignal cancellationSignal) {
        // The read is not allowed...  to fake it out, we replace the given
        // selection statement with a dummy one that will always be false.
        // This way we will get a cursor back that has the correct structure
        // but contains no rows.
        if (selection == null || selection.isEmpty()) {
            selection = "'A' = 'B'";
        } else {
            selection = "'A' = 'B' AND (" + selection + ")";
        }
        return query(uri, projection, selection, selectionArgs, sortOrder, cancellationSignal);
    }

アプリからAppOpsによる制限状態を知る方法

自分の作ったアプリが使っている機能がAppOpsで制限されていたとしても、アプリはクラッシュしないから問題ないですね、ってそんなわけはありません。空っぽのデータが返ってきた時、データが無くて取れなかったのか、ユーザが制限していて取れなかったのかを区別しないといけない場合は多いのではないでしょうか。たとえば、アプリが利用している機能へのアクセスが制限されて必要な情報が得られない場合、アプリを活用するためには制限をかけないでくださいということをユーザに通知する必要があります。 ((ユーザにこっそり電話帳データを抜き取ってサーバに送るようなアプリは制限されてしかるべきだと思いますが、位置に基いた情報を提供する有益なアプリで、位置情報取得を制限されると何の役にも立たなくなってしまいます。))

ということで、アプリ開発者は、自分が使おうとしている機能がAppOpsで制限されているかどうか確認出来ないといけなくなるでしょう。しかし残念ながら、まだこの機能は非公開の隠し機能であるため制限状態を知る方法も公開されていません。

ただし、内部的にその確認のために利用しているメソッドが存在しているので、リフレクションでメソッドを呼び出すことで確認することが可能です。以下にそのコード例を示します(電話帳を読む機能が制限されているかどうかを確認する例です)。

    /* acquiring AppOpsManager class and its checkOpNoThrow method */
    Class appOpsClass = null;
    Method checkOpNoThrowMethod = null;
    try {
        appOpsClass = Class.forName("android.app.AppOpsManager");
        checkOpNoThrowMethod = appOpsClass.getMethod("checkOpNoThrow",
            Integer.TYPE, Integer.TYPE, String.class);
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    } catch (NoSuchMethodException e) {
        e.printStackTrace();
    }

    /* invoking checkOpNoThrow method to get AppOps access mode */
    Object appOps = getSystemService("appops");
    Object ret = null;
    int appOpsMode = 1; /* AppOpsManager.MODE_IGNORED */
    if (checkOpNoThrowMethod != null && applicationInfo != null) {
        try {
            ret = checkOpNoThrowMethod.invoke(appOps,
                    4, /* AppOpsManager.OP_READ_CONTACTS */
                    applicationInfo.uid, /* Linux UID */
                    this.getPackageName() /* package name of this application */
            );
            appOpsMode = ( (Integer)ret ).intValue();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
    }

    if (appOpsMode == 0) { /* AppOpsManager.MODE_ALLOWED */
        /* execute reading contacts because allowed */
        Log.d(TAG, "allowed");
    } else if (appOpsMode == 1) { /* AppOpsManager.MODE_IGNORED */
        /* don't read contacts because not allowed */
        Log.d(TAG, "not allowed");
    } else { /* AppOpsManager.MODE_ERRORED */
        /* dont' read contacts because of error */
        Log.d(TAG, "error");
    }

少しサボって、いくつかの値を即値で書いているのはご勘弁を。このコードで何をやっているかというと以下の通りです。

  • AppOpsManagerクラスの中で制限状態取得処理を司る checkOpNoThrow() メソッドを取得する
  • getSystemService() で AppOps サービスのインタフェースオブジェクトを取得する
  • そのオブジェクトの checkOpThrow() に、チェックしたい機能、このアプリのLinux UID、このアプリのパッケージ名を渡して、利用可能かどうかの判定結果を取得する ((パッケージ名だけでなくLinux UIDも判定に用いているのは、Android 4.2から導入されたマルチユーザ機能でユーザごとに異なる制限をかけるためと思われます。))
  • 判定結果に応じてログを表示する

チェックしたい機能を示す数値は、AppOpsManagerの中で定義されていますので、自分がチェックしたいものに合わせて変更してみてください。

おまけ

通知機能へのアクセスや、クリップボードの読み書きなど、従来ではパーミッションによる制限が無かった機能に対しても、AppOpsは機能制限を行えるようにしています。パーミッションの機能を拡張するのではなく、AppOpsをパーミッション機構とは別のものとして導入しているからこそ出来ることです。

もしかすると、正式版の時点では、パーミッションとしては従来通りの粒度で大きく与えておき(例:READ_PHONE_STATE)、AppOpsによって細かい粒度で機能を制限するということをやってくるのかもしれません(例:AppOpsでIMEIなどの端末固有ID取得を制限する)。

おまけ2

AppOpsManager では、機能が利用可能かどうかを判定するメソッドとして、checkOpNoThrow() だけでなく checkOp() も提供しています。名称からわかるように、前者は判定に失敗した場合に例外を投げず、後者は例外(SecurityException)を投げるものとなっています。

クリップボードとカメラ機能の制限チェックでは checkOp() が使われていますので、それらの機能にアクセスしているアプリでは例外処理の追加についても考慮しなければならないかもしれません。

Android Studio (IntelliJ IDEA) の Language Injection

Android Studio (およびそのベースとなっているIntelliJ IDEA)にはLanguage Injection というおもしろ機能があります。I/Oのセッションでも簡単に説明していた正規表現チェックの例を使ってみます。

まず、以下は(Javaのリファレンスにも載っているような)非常に簡単な正規表現チェックコードの例です。aが0回以上とbという文字列が含まれるかどうかチェックしています。

正規表現コード例
正規表現コード例

ここで正規表現(“a*b”)にカーソルを合わせて、Alt+Returnでポップアップメニューを表示し、Inject Language を選択……

Inject Language
Inject Language

さらにどんな言語の機能を差し込むのか選択が出るので、Regular Expression(正規表現)を選択します。

Inject RegExp
Inject RegExp

これで準備完了です。再度 Alt+Returnでポップアップメニューを呼び出すと、正規表現チェックのメニューが増えているかと思いますので、それを選択肢ます。

Check RegExp
Check RegExp

すると、正規表現をチェックするダイアログのようなものが表示されます。

正規表現チェッカ登場
正規表現チェッカ登場

ではここに正規表現とマッチしない文字列を入れてみます。

正規表現がマッチしなければ赤
正規表現がマッチしなければ赤

マッチしないので赤表示となります。次にマッチする文字列を入れます。

正規表現がマッチすれば緑
正規表現がマッチすれば緑

マッチするので緑に変わりました。

ということで、プログラム開発中に、テストを動かすまでも無く簡易に正規表現が意図通りに動作するものとなっているかを確認することができます。 (( ちゃんとテストコードも書くべきなのは当然です。 ))

もうここではこのLanguage Injectionを使わないぞと思ったら、取り除くこともできます。

不要になったらun-inject
不要になったらun-inject

ちょっとした機能ではありますが、これも開発効率アップに貢献してくれる機能の一つです。

Android Studio Tips

Google I/O 2013において、Androidの新しい開発環境である Android Studio が発表されました。これまでのEclipseを用いた環境からガラリと一新し、JetBrainsIntellIJ IDEA Community Editionをベースにしたものとなりました。

弊社ではすでにアプリ開発においてIntelliJ IDEA(の有償版であるUltimate Edition)を活用しており、非常に強力なコード開発補助機能に助けられています。Androidアプリ開発用に無償で公開されたAndroid Studioにおいても、その強力な支援機能は健在です。

Android Studio は現在バージョン0.1のEarly Access Preview版として公開されています。まだ正式版では無いためバグなども潜在しているものと思われます。しかし、正式版が公開された後のどこかでおそらくEclipse用のプラグインの提供も終了し、最新の開発環境はAndroid Studioに絞られるのではないかと予想されますので、出来るだけ今のうちからどういったものか知っておくことが望ましいでしょう。 ((しばらくはAndroid StudioとADTは並行して提供されるという噂あり))

インストール手順などについては、TechBoosterHow to Install “Android Studio” for Mac OSなど多数のサイトで紹介されていますので、本記事ではインストール手順などについては参照することにとどめ、Android Studioを使う上でのいくつかのTipsを、IntelliJ IDEA利用の経験から示したいと思います。

コード補完 (Java編)

IDEA系のIDEの強さの一つはなんといっても強力なコード補完です。たとえば、Androidアプリを書く際によく使用する例として getSystemService() を入力する場合の補完例を以下に示します。

completion8

“gSySer” という入力で、getSystemService() が補完候補として提示されています。メソッドに含まれる単語の頭一文字を入力して補完候補を出すCamelCase補完という機能を持っているIDEは多いですが、Android Studioの補完はそれよりも柔軟に対応してくれます。この例では、”System”を飛ばして “gSer” と入力した場合でも getSystemService()が補完候補として提示されます。

ただし、補完候補の提示には若干のクセがありますので、出てほしいものが出てきてくれない場合があります。たとえば、アプリケーションを新規作成した状態で、SharedPreferences の変数を宣言しようとした場合、以下のような状態なります。

completion3

“SharedPreferences” のすべてを入力しているにも関わらず、補完候補として現れてくれません。Android Studioの補完はSmart Completionという少し頭のいい補完をしてくれるのですが、たまに間違った推論が行われてしまう場合があるようです。

completion4

CamelCase入力でも同様です。

このような推論ズレが発生している場合、Ctrl+スペースを入力することで、Basic Completionによる補完候補の提示が行われますので、補完候補が出てくれないなという時には Ctrl-スペースで補完方法を切り替えてみましょう。ちなみに、キーバインドをEmacsライクに変更している場合は、Ctrl-スペースではなく Alt+/ となります。

completion7

一度補完が働き、コードの中でその型が使用されていれば、次回以降はSmart Completionでも適切に候補として提示されます。

completion6

completion5

コード入力支援 (Java編)

Android Studio はコード補完以外の入力支援も強力です。たとえば、以下はメソッド呼び出しで戻り値がある場合に、その戻り値を格納するローカル変数の作成の支援の例です。支援が可能な箇所(や、エラーがある箇所)では、Alt+Return を入力すると、適切な支援作業がポップアップで提示してくれます。

introduce_local_var1

変数の宣言と代入を”Introduce local variable” のポップアップから選択すると……

introduce_local_var2

このように変数宣言が追加されます。変数名の候補も、メソッド名などからの類推で自動的に提示されたりもします。

また、メソッドの引数が抜けている場合、その位置にカーソルを移動して Ctrl-P で何を引数として設定すれば良いかをポップアップで教えてくれます。

method_parameters1

上記の例では第二引数に@MagicConstant というアノテーションがついています。このアノテーションにより、第二引数を入力する時にCtrl-スペースで補完候補を表示すると、このメソッドの第二引数として利用してよい定数に絞りこまれた候補が一覧されます。

method_parameters2

Java8っぽいコード表示

Androidアプリ開発において、現時点では Java6相当(+α)の機能しか使用することができません。たとえば、ボタンクリック時のハンドラはリスナオブジェクトを生成して、そのオブジェクトを引数で渡すという方法で登録します。

callback_like_lambda1

おや?左側に (-) が表示されていて、そこをクリックすると折りたたみ表示が出来るようですね。押してみましょう。

callback_like_lambda

折りたたまれて表示されたコードは、Java8のlambda風に表示されます!

実際にlambdaのコードになっているわけではありませんが、直感的に理解しやすくなってコードの可読性は向上しますし、同ページ内に表示される情報量が増します。

レイアウトエディタ

Android Studioはレイアウトエディタも強力です。かなりの部分をGUIで記述するのも簡単でしょう。

layout_editor1

慣れている方はXMLを直接編集した方が早いかもしれません。その場合は、エディタの下部にあるタブでTextを選択します。

layout_editor2

ここでも補完機能は強力です。レイアウトXMLで非常によく使う属性の一つに android:id がありますが、これを入力してみましょう。

layout_editor3

上図は andid と入力しただけで、android:idが候補として提示されている例です。

以下はViewの必須属性である android:layout_width と andorid:layout_height が未設定の場合です。警告が出ています。

layout_editor4

右側に表示されているプレビュー画面上に、何か支援候補が表示されています。

layout_editor5

その中から”Automatically add all missing attributes” を選択してみます。

layout_editor6

自動的に必須項目が追加されます。楽ですね!

当然、レイアウトXMLに埋め込んだテキストを strings.xml に追い出すのもお手のものです。

layout_editor7

ハードコードしているものがあるよという警告が出ているので、Alt+Returnで修正方法の候補を表示し……

layout_editor8

その中から、”Extracting String Resource” を選択するとダイアログが表示されますので……

layout_editor9

ダイアログからその文字列を定義するstrings.xmlの置き場を選択すると、レイアウトXML内にそのまま埋め込まれた文字列を追い出せます。

ちなみに、カスタムビューを作成している場合、それもレイアウト作成時に入力の候補として表示されます。

layout_editor10

署名されたAPKの出力

アプリをGoogle Playを通じて公開する場合、必ず署名されたAPKを作成しなければなりません。Android Studioはこの部分の支援も優秀です。

generate_signed_apk1

メニューの Build -> Generate Signed APK… を選択すると以下のダイアログが表示されますので、出力したいアプリ(Module)を選んでNextを押して進みます。すると、Eclipseでもおなじみのように、鍵ストアおよび署名鍵の選択画面が表示されます。Android Studioでは一画面に収まっています。

generate_signed_apk2

このダイアログに “Remember Password”というチェックボックスがありますので、いちいち毎回パスワードを打つのが面倒だという方はチェックしておくと良いでしょう。パスワードを覚えせるのは不安だ?ご安心ください。それに対してはちゃんと対策が用意されています。

先ほど入力したパスワードを記憶させる場合、マスターパスワードを設定して、そのマスターパスワードを入力しなければ保存されたパスワードを参照されないようになっています。下記は、すでに設定済みのマスターパスワードを入力するダイアログです。

input_master_password

すでにマスターパスワードによる解除が行われているものとして、次の画面に Next で進むと、出力する署名したAPKファイルの出力先選択ダイアログが表示されます。

generate_signed_apk3

ProGuardによる難読化を行うかどうかもここで設定出来ます。

出力に成功するとその旨を示したダイアログが表示されます。

generate_signed_apk4

Macの場合は、出力されたフォルダをFinderで表示することもワンボタンです。

おまけ

残念ながら新規ファイル作成で作成できるAndroidコンポーネントに、Fragmentは含まれないようです。これはSupport Libraryを使うアプリとそうでないアプリとがあるということに起因しているんでしょうか。

new android componet1

何かの機能を呼び出すキーを忘れたら、Help -> Find Action… を使うと便利です。

find_action_help_menu1

たとえば「補完」を意味する completion で検索してみましょう。

find_action_help_menu2

私が Emacs キーバインドで使用しているため、Alt+/ がBasic Completionの呼び出し方だよと表示されています。Defaultのキーバインドを使用されている場合、ここには Ctrl+スペースが示されることになります。

以上、簡単なものだけですが、Android Studio Tipsをお送りしました。

Android Studio (およびそのベースとなっている IntelliJ IDEA)が便利なものであることを少しでもご理解いただけていると幸いです。

rbenv を用いて Ruby をシステム全体にインストールする方法

注:本記事はMacOS X や Linux などのUnix系OS向けとなります。WindowsでもCygwin環境を使えば出来るかもしれませんが。

MacOS Xを使っていると、標準でインストールされている ruby が1.8系なので、最新のRuby on Railsを用いたウェブアプリ開発が出来なかったりして困ります。そこで最新のRubyをインストールすることになりますが、バージョンアップ時の手間を考えると、ソースコードをもってきてビルドするという手順を出来るだけ簡単にしておきたいところです。

そのような要望に応えるツールとして rvmrbenv といったものがあります。本記事では rbenv を用いてシステム全体に新しいバージョンのRubyを適用する方法を記します。

なお、Rubyのビルドに必要なツール類(コンパイラやライブラリなど)のインストールについては省略します。

rbenv のインストール

本記事では、/usr/local 以下に rbenv および Ruby をインストールするものとします。システム全体でのインストールとするため、作業は特権ユーザ(root)にて行います。

まず、/usr/local 以下に rbenv を github からもってきます。

# mkdir -p /usr/local
# git clone https://github.com/sstephenson/rbenv.git

rbenv 以下にpluginsディレクトリを作成し、その下に ruby-build を持ってくる。

# cd rbenv
# mkdir plugins
# git clone https://github.com/sstephenson/ruby-build.git

rbenv と ruby-build は gitで取ってくるのではなく、パッケージを取ってきて展開するという方法でも構いませんが、git pullするだけでアップデートと後々楽になるので、ここでは git clone しています。

rbenv の環境設定を行います。 システム全体でrbenvを使用したい場合、MacOS Xの場合、/etc/bashrc に以下を追加するなどが良いでしょう。 ((ユーザごとで設定する場合は、$HOME/.bash_profile などに記載してください。))

export RBENV_ROOT=/usr/local/rbenv
export PATH="/usr/local/rbenv/shims:/usr/local/rbenv/bin:$PATH"
eval "$(rbenv init -)"

Linuxの場合、/etc/profile.d/rbenv.sh などのファイルを作成して、その中に上記を記述するというのでも良いでしょう。

その後、rbenv の環境設定を有効化します(上記の追加内容の一連のコマンドを実行したり、source コマンドを用いて上記の設定を読みこませたり、ログインしなおしたりしてください)。設定が成功しrbenv にパスが通っているかどうかは、以下のコマンドで確認ができます。

# which rbenv
/usr/local/rbenv/bin/rbenv

/usr/local/rbenv/bin/rbenv が返ってきていればOKです。

Rubyのビルド

次にRubyをビルドします。rbenv を使えば、バージョンを指定してRubyのビルドを行うことができます。((rbenv内の整合性がとれていない場合にrbenvコマンドは失敗するかもしれません。その場合は rbenv rehashコマンドで整えるなどして再度実行してみてください。))

たとえば、ruby 2.0.0-p0 をインストールしてみます。

# rbenv install 2.0.0-p0

Rubyのビルドに必要なツールが揃っていれば、ビルドに成功するはずです。成功しない場合、何かツールが欠けているので、エラーメッセージに従って足りないツールをインストールしてください。 ((opensslやreadlineなどをあらかじめシステムにインストールしておき、ビルド時にそれを参照するようにすることも可能ですが、ここではrbenvに依存するライブラリの準備も任せています。))

インストールされているRubyを確認します。これには rbenv versions コマンドを使用します。

# rbenv versions
  system
* 2.0.0-p0 (set by /usr/local/rbenv/version)

ここで、system に * が付いている場合は、標準のRuby(1.8系)が選択されている状態ですので、ruby global コマンドを用いて、先ほどインストールしたrubyを利用するように変更してください。

# rbenv global 2.0.0-p0

バージョンを指定したRubyのインストールは完了です。実行されるrubyコマンドのパスとバージョンを確認してください。

# which rbenv
/usr/local/rbenv/shims/ruby
# ruby -version
ruby 2.0.0p0 (2013-02-24 revision 39474) [x86_64-darwin12.3.0]

以上です。Enjoy your Ruby life!

RubyMine (またはIntelliJ IDEA)で、deviseのヘルパーが認識されない問題を回避する方法

Ruby on Railsでのユーザ認証機能には、定番として devise がよく用いられます。

また、Ruby on Railsでのアプリは、JetBrainsRubyMine (もしくは同機能を包含した IntelliJ IDEA)を用いると非常に効率よく開発を進めることができます。

しかし、この便利なdeviseを用いたRailsプロジェクトを、RubyMineIntelliJ IDEAで編集する場合に、deviseが提供するcurrent_userやuser_signed_in? ((Userクラスをdeviseを用いて作成した場合)) などの便利なヘルパーメソッドを認識してくれないという問題が発生します。

IDEが認識をしていないだけですので、実際にそれらのヘルパーメソッドを用いて作成した機能は正常動作するのですが、それらのヘルパーメソッドを用いる箇所で、IDEによる補完機能の恩恵を受けることが出来なくなります。また、文法違反の可能性があるとして警告が出てしまうため、他の本当に対処すべき問題を埋もれさせてしまう(潜在的なバグを見つけにくくなる)という問題もはらみます。

これは、RubyMineが動的に生成されたメソッドを認識して文法チェックすることが出来ないために発生していると思われます。

そこで、その問題を回避するため、以下のようなヘルパーを app/helpers/application_helper.rb に追記します(この例は、ログイン用にdeviseで作成したモデルのクラスがUserの場合)。

# IDEの補完機能を有効にするため、Deviseのヘルパを宣言
def authenticate_user!(opts={})
  opts[:scope] = :user
  warden.authenticate!(opts) if !devise_controller? || opts.delete(:force)
end

def user_signed_in?
  !!current_user
end

def current_user
  @current_user ||= warden.authenticate(:scope => :user)
end

def user_session
  current_user && warden.session(:user)
end

上記に追加したヘルパーメソッド群、元々のdeviseの中ではより柔軟に様々なモデルに対して適用できるように以下のようなコードがevalで動的に処理されます。(Github上の該当ファイル)

def authenticate_#{mapping}!(opts={})
    opts[:scope] = :#{mapping}
      warden.authenticate!(opts) if !devise_controller? || opts.delete(:force)
end

def #{mapping}_signed_in?
  !!current_#{mapping}
end

def current_#{mapping}
  @current_#{mapping} ||= warden.authenticate(:scope => :#{mapping})
end

def #{mapping}_session
  current_#{mapping} && warden.session(:#{mapping})
end

この #{mapping} の部分は devise 管理下で作成されたモデル名を元にして引数として渡されたものと置き換えられます。たとえば、Userモデルクラスの場合は #{mapping} の部分は user となります。

上記のような、任意のモデル名に対応することが可能なヘルパーメソッド定義となっているものを、特定モデルに特化したヘルパメソッド定義のせいで発生している問題を、特定のモデルに特化したヘルパーメソッドを定義することで回避するという対策です。

同様の方法で動的にメソッドを追加している箇所は他にもあるため、deviseを用いてコーディングをしている時に補完が働かないという現象が発生した場合は、必要に応じて同様の処理を行うと解決させられるかもしれません。

ところで余談ですが、RubyMineやIntelliJ IDEAは、サムライズムさんのような日本の代理店から日本的な商慣行での購入も可能です。開発効率を劇的に向上してくれる非常にパワフルなIDEですので、まずは30日間の無料体験をしてみてはいかがでしょうか。((とはいえ、これを確認に来た方はすでにRubyMineかIntelliJ IDEAユーザさんなのではないかと思いますが。))

なお、Railsでの devise の導入方法や使用方法については日本語のブログでの解説記事も多数ありますので、本エントリでは割愛いたします。