XamarinのFelicaReaderプラグイン

Posted by 技術ブログ by Strawhat.net on Saturday, December 16, 2017

これはXamarin その1 Advent Calendar 2017の16日目の記事です。 明日は、@t-miyakeさんが担当されます。

この記事では、XamarinのAndroidアプリとUWPアプリでFeliCaカード読み取りを行えるFelicaReaderプラグインの概要を説明するとともに、Xamarin.Formsアプリでの利用例を紹介します。

なお、FelicaReaderプラグインはNuGetとGitHubで公開の予定ですが、NuGetのほうは公開が遅れています。 公開されたら改めて報告しますので、少々おまちください。

FelicaReaderプラグインとは

これまで、SuicaやEdyなどのFeliCaカードを読み取るKumalicaというアプリをUWPアプリとして開発・公開してきましたが、 アーキテクチャを見直してAndroidにも対応したKumalica vNextの開発を少しずつ進めています。

FelicaReaderプラグインは、そのKumalica vNextの開発で作成したXamarinプラグインで、UWPアプリとAndroidのXamarinアプリで FeliCaカード読み取りをできる限り同じインターフェースで扱えるように設計しました。

FelicaReaderプラグインでは、以下の2つのインターフェースを提供します。

IFelicaReaderインターフェース

FeliCaカードを検知したときに、アプリが通知を受け取るための機能を提供します。

  • フォアグラウンドでのカード検知の有効・無効設定
  • カード検知の開始
  • FeliCaカード読み取り機能の対応状況の取得
  • FeliCaカード読み取り機能の有効・無効状態の取得
  • FeliCaカード検知の通知を登録するIObservableの取得
    • このIObservableのサブスクライバーは、FeliCaカードを検知すると、IFelicaCardMediaインターフェースを実装したクラスのインスタンスを受け取ります

IFelicaCardMediaインターフェース

検知したFeliCaカードの情報を取得するための機能を提供します。 FeliCaカードへのコマンド送信は、FeliCaカードユーザーズマニュアル抜粋版に 記載されたコマンドの一部だけを実装しています。

  • 検知したFeliCaカードのIDm取得
  • 検知したFeliCaカードへのFelicaコマンドの送信
    • 任意のバイトデータの送信
    • Pollingコマンドの送信、応答の解析
    • ReadWithoutEncryptionコマンドの送信、応答の解析
    • RequestServiceコマンドの送信、応答の解析
    • RequestSystemCodeコマンドの送信、応答の解析
    • RequestResponseコマンドの送信、応答の解析
    • SearchServiceCodeコマンドの送信、応答の解析

FelicaReaderプラグインの使い方

FelicaReaderプラグインの使い方は、GitHubで公開しているサンプルコード"nobukuma/FelicaReaderSample“を 参照していただくとして、Xamarin.Formsアプリを例にとって、実装すべきポイントのみ紹介します。

単体のUWPアプリや、Xamarin.Androidアプリの場合はまだ検証できていないので、後日記事にしたいと思います。

プラグインのインストール

FelicaReaderプラグインは、NuGetで近日中に公開を予定しています。 なお、ソースコードは、GitHubの”nobukuma/FelicaReader“で 公開していますs。

Xamarin.Formsアプリ(アプリ名をSample.FelicaReaderとします)のソリューションにおいて、 共通部分のSample.FelicaReader、またSample.FelicaReader.DroidとSample.FelicaReader.UWPの それぞれでFelicaReaderプラグインをNuGetからインストールする必要があります。

Android固有部分での実装内容

Androidの固有部分Sample.FelicaReader.Droidで、いくつか実装する必要があります。

まず、MainActivity.csの修正は以下の通りです。

  • バックグラウンド時のNFCタグ検知の設定
    • IntentFilter属性の設定
    • MetaData属性の設定
namespace Sample.FelicaReader.Droid
{
    [Activity(Label = "Sample.FelicaReader", Icon = "@drawable/icon", MainLauncher = true,
        LaunchMode = Android.Content.PM.LaunchMode.SingleTop,
        ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation)]
    [IntentFilter(new[] { NfcAdapter.ActionTechDiscovered })]
    [MetaData(NfcAdapter.ActionTechDiscovered, Resource = "@xml/nfc_filter")]
    public class MainActivity : global::Xamarin.Forms.Platform.Android.FormsAppCompatActivity
    {
        private IFelicaReader felicaReader;
  • OnCreateでの設定
    • MainActivityインスタンスとその型を指定して、CrossFelicaReader.Initを呼び出す
    • インテントの処理(タグデータからNfcFインスタンスを生成して、接続、それを指定して作成したFelicaCardMediaImplementationインスタンスをサブスクライバーに通知)
protected override void OnCreate(Bundle bundle)
{
    ...(省略)...

    CrossFelicaReader.Init(this, GetType());
    this.felicaReader = CrossFelicaReader.Current;
    LoadApplication(new App(new AndroidInitializer()));

    this.ProcessActionTechDiscoveredIntent(this.Intent);
}

private void ProcessActionTechDiscoveredIntent(Intent intent)
{
    string action = intent.Action;
    if (action != NfcAdapter.ActionTechDiscovered)
    {
        return;
    }

    var tag = intent.GetParcelableExtra(NfcAdapter.ExtraTag) as Android.Nfc.Tag;
    if (tag != null)
    {
        var subject = this.felicaReader.WhenCardFound() as Subject<IFelicaCardMedia>;
        NfcF nfc = NfcF.Get(tag);
        nfc.Connect();
        subject.OnNext(new FelicaCardMediaImplementation(nfc));
    }
}
  • OnNewIntentでのインテントの処理
    • 処理内容は上記のインテントの処理と同じ
protected override void OnNewIntent(Intent intent)
{
    base.OnNewIntent(intent);
    ProcessActionTechDiscoveredIntent(intent);
    return;
}
  • Pause・Resume時のフォアグラウンドでのインテントのディスパッチの停止・開始
protected override void OnPause()
{
    base.OnPause();
    this.felicaReader.DisableForeground();
}

protected override void OnResume()
{
    base.OnResume();
    this.felicaReader.EnableForeground();
}

次に、Resrouces/xml/nfc_filter.xmlを作成して、以下を記述します。

<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2" >
  <tech-list>
    <tech>android.nfc.tech.NfcF</tech>
  </tech-list>
</resources>

最後に、プロジェクトのプロパティを開いて、AndroidマニフェストのRequired PermissionsでNFCを選択します。

UWP固有部分での実装内容

UWPの固有部分Sample.FelicaReader.UWPで実装すべきコードはありません。

ただし、共通部分でFeliCaカード検知の開始処理を書く必要があります。 サンプルでは、ボタンが押されたときにFeliCaカードの検知を開始しています。

public class MainPageViewModel : BindableBase, INavigationAware
{
    private IFelicaReader felicaReader;

    ...

    public DelegateCommand ReadButtonClickedCommand { get; private set; }

    public MainPageViewModel()
    {
        this.felicaReader = CrossFelicaReader.Current;
        this.ReadButtonClickedCommand = new DelegateCommand(() => ButtonClicked());
        ...
    }

    private void ButtonClicked()
    {
        this.felicaReader.FindCard();
    }

また、Sample.FelicaReader.UWPのパッケージマニフェストを開いて、機能シートにおいて近接通信を選択します。

共通部分での実装内容

共通部分Sample.FelicaReaderでは、FeliCaカード検知の通知を購読して、その処理を記述します。

サンプルでは、MainPageのViewModelで、FeliCaカード検知の通知を購読して、検知したときの処理を書いています。 ReadWithoutEncryptionで指定するサービスコードとブロック番号リストを変えることで、SuicaやEdyなど FeliCaカードが実装するサービスからデータを取得できます。

public class MainPageViewModel : BindableBase, INavigationAware
{
    private IFelicaReader felicaReader;
    private IDisposable subscription;
    
    ...

    public MainPageViewModel()
    {
        this.felicaReader = CrossFelicaReader.Current;
        ...
    }
    
    ...

    public void OnNavigatedFrom(NavigationParameters parameters)
    {
        this.subscription.Dispose();
    }
    
    ...

    public  void OnNavigatedTo(NavigationParameters parameters)
    {
        this.subscription = this.felicaReader.WhenCardFound().Subscribe(async x =>
        {
            try
            {
                var byteIdm = await x.GetIdm();
                this.IDmString = BitConverter.ToString(byteIdm);
                System.Diagnostics.Debug.WriteLine("Idm: {0}", this.IDmString, 0x00);

                var result = await x.ReadWithoutEncryption(byteIdm, 0x008b, 1, new byte[] { 0x80, 0x00 });
                string resStr = BitConverter.ToString(result.PacketData);
                System.Diagnostics.Debug.WriteLine("Res: {0}(len={1})", resStr, result.PacketData.Length);

                this.Message = String.Format("Res: {0}", resStr, 0x00);
            }
            catch (Exception e)
            {
                System.Diagnostics.Debug.WriteLine(e.Message);
            }
            finally
            {
                x.Dispose();
            }
        },
        onError: x =>
        {
            System.Diagnostics.Debug.WriteLine(x.Message);
        });
    }
}

実行例

Windows 10 Mobile(NuAns NEO) Android(HUAWEI Mate 9)
2017-12-16_18-00-00.png 2017-12-16_18-30-00.png

FelicaReaderプラグインの構成

FelicaReaderプラグインの構成、および実装内容について、ざっと紹介します。

まず、FelicaReaderプラグインの構成ですが、以下のライブラリが含まれます。

  • Plugin.FelicaReader(Pluginのインスタンス生成)
  • Plugin.FelicaReader.Abstractions(FeliCaカード読み取りのインタフェース定義)
  • Plugin.FelicaReader.Android (Xamarin.Droid固有の実装)
  • Plugin.FelicaReader.UWP(Xamarin.UWP固有の実装)

それぞれの実装内容は以下の通りです。

Plugin.FelicaReader

Plugin.FelicaReaderでは、IFelicaReaderインターフェースのインスタンスを取得できるCrossFelicaReader.Currentプロパティを実装しています。 同プロパティを参照すると、Bait and Switchと呼ばれる仕組みを利用して、実行時にはプラットフォームそれぞれで実装された IFelicaReaderインターフェースの実装クラスのインスタンスを生成して、返します。

Bait and Switchについては、本ブログでも近いうちにまとめておこうと思います。

なお、Android版ではActivityインスタンスが必要になるため、CrossFelicaReader.Initメソッドも実装しています。

Plugin.FelicaReader.Abstractions

Plugin.FelicaReader.Abstractionsでは、IFelicaReaderインターフェースとIFelicaCardMediaインターフェース、 およびそれらから参照されるクラスを定義します。

FeliCaカードを検知すると、IFelicaCardMediaオブジェクトを受け取りますが、それを使ったFelicaコマンドの送受信処理、およびその後のデータの解析処理は、Model層で行います。Model層はPCLや.NET Standardクラスライブラリとして実装されて、各プラットフォームから共通に利用されるため、本ライブラリは.NET Standard 1.4をターゲットとしたクラスライブラリとして実装しています。

Plugin.FelicaReader.Android

Plugin.FelicaReader.Androidはプラグインのインタフェースを実装するAndroidプラットフォーム固有のライブラリです。

Androidでは、FelicaなどのNFCタグを端末が検知するとIntentが発生して、それを受信したアクティビティのOnNewIntentにおいてインテントのExtraTagとしてNFCタグの情報が渡されます。それをAndroid.Nfc.Tech.NfcFクラス等を用いて、NFCタグにアクセスします。 Xmarinでは、MainActivityクラスのIntentFilter属性としてインテントフィルタを実装して、OnNewIntentメソッドを実装すること、また同クラスのMetaData属性としてNFCのフィルタを記述したnfc_filter.xmlを指定することで実装できます。

そこで、Plugin.FelicaReader.Androidでは、NfcManager.DefaultAdapter.EnableForegroundDispatchでのフォアグラウンドで動作するときのインテントのインテントのディスパッチの設定・解除と、NfcFインスタンスによるFeliCaカードへのアクセスだけを実装しています。アプリがフォアグラウンドにいないときのFeliCaカードの検知は、アプリの実装に任せています。

Plugin.FelicaReader.UWP

Plugin.FelicaReader.UWPはプラグインのインタフェースを実装するUWP固有のライブラリです。

UWPでは、以前に書いたブログ記事「Windows 10 MobileでFeliCaカードの読み取り~Edy・Suica編」で紹介したように、 Windows.Devices.SmartCards名前空間のSmartCardConnectionクラスを使って、APDUコマンドをスマートカードリーダに 送信することで、NFCタグにアクセスします。 このとき、GitHubのMicrosoft/Windows-universal-samplesリポジトリのサンプル “Near field communication (NFC) sample”に 含まれるPcscSdkプロジェクトの利用が便利です。

なお、UWPでは、FeliCaカードがスマートカードリーダに近づいたときにアプリに通知する仕組みがなく、 アプリがフォアグラウンドで実行されているときにNFCタグの読み取りを試みることになります。

このため、Plugin.FelicaReader.UWPでは、フォアグラウンドでのNFCタグの検知をその通知と、 PcscSdkのFelica.AccessHandlerを利用したFeliCaカードへのアクセスを実装しています。

まとめ

UWPとAndroid向けのXamarin.Formsアプリで、FeliCaカードの検出とそれへのコマンド送受信処理を統一する目的は達成できていると思います。 まだ、未実装の機能があったり、テストが不十分(NFC-F非対応の端末での動作確認とか)なので、今後さらに開発を進めていきます。

今後の予定

FelicaReaderプラグインとそれに関連した機能として、以下を計画しています。

  • 電子マネー系カード・公共交通機関系カードから取得したデータの解析処理の公開(別ライブラリとして)
  • Plugin.FelicaReader.UWPのPcscSdkへの依存の排除
  • NFC-AやNFC-Bへの対応(需要が低そうなので優先度は低。対応時はプラグイン名を変えます)
  • iOSのCoreNFCへの対応(ただしCoreNFCでのFeliCaカード読み取りが可能になって、かつ私のiOSアプリ開発環境が整ったら)

検討事項

FelicaReaderプラグインの開発で検討しているところは以下の通りです。

①Xamarinプラグインとしての実装方法

UWPとAndroidでFeliCaカードの読み取り方法が異なり、プラグインで統一したアクセス方法を提供できているかというと、Androidアプリ側で実装してもらう処理が多く、疑問に思うところもありますが、プラットフォームの制約で改善は難しいかなと思います。

また、AndroidではNFCの利用方法がわかりやすいので、UWP版だけ作成すればよいのではという思いもありましたが、検知したFeliCaカードへのコマンド送信とその応答の処理をModel層で統一して行えているので、両方のプラットフォームに対応している今の実装でよいと考えています。

②IObservableとTaskの混在

IFelicaReaderインターフェースでは、FeliCaカード検知の通知をアプリから監視できる機能の意味からIObservableを、またIFelicaCardMediaインターフェースでは、IDmの取得やFelicaコマンドの送信を非同期で行う意味からTaskを利用しています。

設計の意図を考えるとこれでよいと思っていますが、他の著名なライブラリでの実装事例や、ガイドラインがあればそれを踏まえて、検討しなおしたいと思います。

最後に、FelicaReaderプラグインを公開したことで、次はKumalica vNextのリリースに向けての作業を進めていきたいと思います。