iBeacon Tips: Estimoteビーコンモジュールのモニタリング

Estimote社からプレオーダーで発売されているビーコンモジュールですが、パッケージ内に情報が非常に少ないです。

またビーコンのUUIDなどを変更することができませんし、UUID情報もパッケージに示されていません。

Estimoteでは、iOSのAPIへのラッパーのSDKを用意していますが、UUIDのためだけにそれをわざわざ使うというのも面倒ですし、将来的にビーコンモジュールが潤沢に市場に出回ったら、コストや利便性などを勘案して機種選定を行うことになりますので、ロックインされると困ります。 ((ラッパーはMITライセンスで公開されていますが、バイナリ提供で情報が埋め込まれているようです))

ということで、UUID情報を探して、それを直接使って通常のiBeaconアプリの作り方で作っていくことをおすすめします。

では、UUID情報はどうすれば見つかるでしょうか?BLEをモニタリングするなどでも発見できるだろうと思いますが、世の中にはすでに情報を公開されている方がいますので、ありがたくそれを使わせていただくことにします。

上記ブログによると、EstimoteのビーコンのUUIDは”B9407F30-F5F8-466E-AFF9-25556B57FE6D”であるとのことです。

実際にその値を使用することで、弊社ではEstimoteのビーコンの検出に成功しております。

iBeacon Tips: 正しいビーコン監視方法

iBeacon 関連の情報をGoogleなどで検索すると、特に日本語ブログなどで示されているiBeaconのCentral(監視側)プログラムでの開始手順が不十分になっています。

そのせいで、ビーコンを試そうとしてうまくいかずに失敗されている方もいらっしゃるかもしれません。

たとえば、検索上位に出てくる、以下のクラスメソッドさんの以下のブログには不十分な手順が記載されています(2013年11月26日現在)。

上記ブログの例では、リージョンに入ったことをトリガにしてビーコンからの通知の受け取りを開始していますが、それではアプリの起動時にすでにリージョン内にいる場合に正常動作しません。

以下に、正しいビーコン監視の開始手順を示します。

例で参照しているプロパティは以下のように定義しています。

@interface ViewController () <CLLocationManagerDelegate>

@property (nonatomic) CLLocationManager *locationManager;
@property (nonatomic) NSUUID *proximityUUID;
@property (nonatomic) CLBeaconRegion *beaconRegion;

@end

viewDidLoad ではCLLocationManagerにビーコン監視の開始を要求しています。定数定義はご自身で適切なものにしてください(UUIDなどは自分で作る必要があります)。

- (void)viewDidLoad
{
    [super viewDidLoad];

    if ([CLLocationManager isMonitoringAvailableForClass:[CLBeaconRegion class]]) {
        self.locationManager = [CLLocationManager new];
        self.locationManager.delegate = self;

        self.proximityUUID = [[NSUUID alloc] initWithUUIDString:SERVICE_UUID];

        self.beaconRegion = [[CLBeaconRegion alloc] initWithProximityUUID:self.proximityUUID
                                                               identifier:SERVICE_IDENTIFER];
        [self.locationManager startMonitoringForRegion:self.beaconRegion];
    }
}

ここまでは特に変わったことはありません。

次の、モニタリング開始が正常に始まった時に呼ばれるdelegateメソッドが重要です。

- (void)locationManager:(CLLocationManager *)manager didStartMonitoringForRegion:(CLRegion *)region
{
    [self.locationManager requestStateForRegion:self.beaconRegion];
}

ここでiOS7から追加された”CLLocationManager requestStateForRegion:”を呼び出し、現在自分が、iBeacon監視でどういう状態にいるかを知らせてくれるように要求します。

これによって、次のdelegateメソッドが呼ばれます。

- (void)locationManager:(CLLocationManager *)manager didDetermineState:(CLRegionState)state forRegion:(CLRegion *)region
{
    switch (state) {
    case CLRegionStateInside: // リージョン内にいる
        if ([region isMemberOfClass:[CLBeaconRegion class]] && [CLLocationManager isRangingAvailable]) {
            [self.locationManager startRangingBeaconsInRegion:self.beaconRegion];
        }
        break;
    case CLRegionStateOutside:
    case CLRegionStateUnknown:
    default:
        break;
    }
}

CLRegionStateInside が渡ってきていれば、すでになんらかのiBeaconのリージョン内にいるので、iOS7から追加された”CLLocationManager startRangingBeaconsInRegion:”を呼び、通知の受け取りを開始します。

あとは、リージョンの境界を越えて入った時にも同じく通知を開始するようにします。

- (void)locationManager:(CLLocationManager *)manager didEnterRegion:(CLRegion *)region
{
    if ([region isMemberOfClass:[CLBeaconRegion class]] && [CLLocationManager isRangingAvailable]) {
        [self.locationManager startRangingBeaconsInRegion:(CLBeaconRegion *)region];
    }
}

以上です。

How to invoke AppOps in Android 4.4 KITKAT

In Android 4.3, we were able to invoke AppOps activity, a hidden brand-new security functionality. However the activity was completely hidden in Android 4.3.1 and later.

Can we never invoke AppOps in Android KITKAT?

Yes, there is a way to invoke AppOps not only in Android 4.3 but also 4.4.

UPDATE(Dec 10, 2013): Unfortunately, we cannot invoke AppOps setting fragment in Android 4.4.2.

If you activated developer mode in your KITKAT device, connect your device to PC and then try the following command.

% adb shell am start -n com.android.settings/com.android.settings.Settings \
-e :android:show_fragment com.android.settings.applications.AppOpsSummary \
--activity-clear-task --activity-exclude-from-recents

The new way invokes AppOps fragment via the Setting app!!

Of course, you can invoke AppOps from your application.

Intent intent = new Intent();
intent.setClassName("com.android.settings",
        "com.android.settings.Settings");
intent.setAction(Intent.ACTION_MAIN);
intent.addCategory(Intent.CATEGORY_DEFAULT);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK |
        Intent.FLAG_ACTIVITY_CLEAR_TASK |
        Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
intent.putExtra(":android:show_fragment",
        "com.android.settings.applications.AppOpsSummary");
startActivity(intent);

I confirmed that works fine both in Android 4.3 and 4.4.

The original method was discovered by @adakoda. Great job!!

I only fix some issues. For instance, @adakoda’s method didn’t work when the Setting app is on recent apps.

Reference:

Android 4.3以降でAppOpsを呼び出す方法 (KITKAT対応)

Android 4.3.1以降で呼び出せなくなっていると思われたAppOpsですが、以下の方法で呼び出すことが可能であることがわかりました。

追記(2013/12/10): Android 4.4.2では以下の方法でも呼び出せなくなってしまいました。

開発者オプションを有効にしてUSBデバッグ接続している場合、PCから以下のようにコマンドを入れるとAppOpsを呼び出すことができます。

% adb shell am start -n com.android.settings/com.android.settings.Settings \
-e :android:show_fragment com.android.settings.applications.AppOpsSummary \
--activity-clear-task --activity-exclude-from-recents

改行なしだと読みにくくなるため、バックスラッシュで行を区切っていますが、1行で入力しても構いません。

それをコードで書くと以下のようになります。

Intent intent = new Intent();
intent.setClassName("com.android.settings",
        "com.android.settings.Settings");
intent.setAction(Intent.ACTION_MAIN);
intent.addCategory(Intent.CATEGORY_DEFAULT);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK |
        Intent.FLAG_ACTIVITY_CLEAR_TASK |
        Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
intent.putExtra(":android:show_fragment",
        "com.android.settings.applications.AppOpsSummary");
context.startActivity(intent);

いずれもAndroid 4.3でも動作することを確認しております。

追記(2013/12/6): Android 4.4.1にアップデートしたNexus5でも動作することを確認しました。

なお上記の方法は、@adakoda さんが発見された方法を基にし、別の設定画面が最近表示したアプリ一覧に残っていても確実に呼び出せるようにするために若干手を加えさせていただいたものです。

また、呼び出した結果を履歴や最近呼び出したアプリ一覧には残さないようにもしています。

元手順を発見した@adakodaさんに感謝します!

AppOpsがどういうものか、設定状態の確認をどのようにすれば良いかなどについては、当ブログの過去記事「Android 4.3 の隠し機能 AppOpsについていろいろ」や「Effective Android」(達人出版会)をご参照ください。

参考サイト:

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などの他の通信を利用したシステム構築も選択肢とすべきです。

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

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