GingerBreadでFeliCaのPush送信機能を使う

Push送信機能とは?

FeliCaのPush送信機能を使うとタッチした端末にコマンドを送り込める。
この機能は、IS03、Lynx3Dなどのおサイフケータイ対応Android端末で利用でき、 そのためのライブラリがフェリカネットワークスの配布ページからダウンロードできる。

Push送信機能は、三者間通信と呼ばれる方式を使って実現されている。
三者間通信とは、リーダーライター(またはリーダーライターモードで動作している端末)から、 カードエミュレーションモードで動作している端末にデータを送り込み、端末が送り込まれたコマンドを解釈して実行する方式である。

GingerBreadでカードエミュレーションを有効にする方法は今のところ確認されていないので、ここでは送信側として対応端末であるLynx3DにPush送信を試みた記録を記す。


結果はこんな感じ。

Push送信機能を使うには?

Arduino向けRC-S620/S制御ライブラリの提供PaSoRi/RC-S320 - osdev-j (MMA)あたりを見るとPushはコマンドコード0xb0であることがわかる。

幸いにも先達が@hideなメソッドをリフレクションして、生のコマンドを送る方法を見いだしている。
その方法でPush(0xb0)コマンドを発行すれば目的は果たせる。(はずである)

しかし、上述のライブラリはオープンソースではないし、コマンドの発行などの低レイヤの処理は隠蔽されていて、Push送信で実際に送るデータの内容を知ることはできない。
ここで、同ライブラリの「従来のおサイフケータイ」にもPush可能な点に着目すると次のページが見つかる。

このページの「外部R/Wからの携帯電話アプリケーション制御」説明書」というPDFに、ブラウザを起動するためのデータフォーマットが記載されている。

  • 個別部数(1byte) - 0x01
  • 個別部
    • 起動制御情報(1byte) - 0x02
    • 個別部パラメータサイズ(2bytes)
    • URLサイズ(2bytes)
    • URL(n bytes)
  • チェックサム(2bytes)

あとは単純にこれをコマンドとして発行するだけである。

Push送信機能を実装する

個別部

個別部は

  • 起動制御情報
  • 個別部パラメータサイズ
  • URLサイズ
  • URL

で構成される。

プログラム中の引数 browserStartupParam については上述の資料には記載がないが、端末によっては確認ダイアログなどに表示する文字列を指定する。

	private static byte[][] buildPushStartBrowserSegment(int type, String url,
			String browserStartupParam) {

		byte[] urlByte = url.getBytes();
		byte[] browserStartupParamByte = browserStartupParam == null ? new byte[0]
				: browserStartupParam.getBytes();

		int capacity = urlByte.length + browserStartupParamByte.length + 5;

		ByteBuffer buffer = ByteBuffer.allocate(capacity);

		// 個別部ヘッダ

		// 起動制御情報
		buffer.put((byte) type);
		// 個別部パラメータサイズ
		int paramSize = urlByte.length + browserStartupParamByte.length + 2; // urlLength(2bytes)
		putAsLittleEndian(paramSize, buffer);

		// 個別部パラメータ

		// URLサイズ
		putAsLittleEndian(urlByte.length, buffer);
		// URL
		buffer.put(urlByte);
		// (ブラウザスタートアップパラメータ)
		buffer.put(browserStartupParamByte);

		return new byte[][] { buffer.array() };
	}
Envelope

ブラウザ起動については一つのみ、とのことであるがPush送信は複数のコマンドを含められるようになっている。
このためEnvelope(というのだろうか?)は複数の個別部を受け付けるようにしている。

チェックサムは、資料では非常に文芸的な書き方をしているが、つまり以下のとおりである。

	private static byte[] packSegment(byte[]... segments) {

		int bytes = 3; // 個別部数(1byte) + チェックサム(2bytes)
		for (int i = 0; i < segments.length; ++i)
			bytes += segments[i].length;

		ByteBuffer buffer = ByteBuffer.allocate(bytes);

		// 個別部数
		buffer.put((byte) segments.length);

		// 個別部
		for (int i = 0; i < segments.length; ++i)
			buffer.put(segments[i]);

		// チェックサム
		int sum = segments.length;
		for (int i = 0; i < segments.length; ++i) {
			byte[] e = segments[i];
			for (int j = 0; j < e.length; ++j)
				sum += e[j];
		}
		int checksum = -sum & 0xffff;

		putAsBigEndian(checksum, buffer);

		return buffer.array();
	}
Pushコマンド

Pushコマンドの形式は

  • データのサイズ
  • データ

である。

このため、Envelopeのサイズを先頭に付加している。
このデータのサイズは、Felicaコマンドのpreambleなどに指定するデータのサイズとは別の、Pushコマンドで必要となるサイズである。

	private static byte[] packContent(byte[] segments) {
		byte[] buffer = new byte[segments.length + 1];
		buffer[0] = (byte) segments.length;
		System.arraycopy(segments, 0, buffer, 1, segments.length);
		return buffer;
	}
Felicaコマンド

最後にこれらを順に呼び出してFelicaコマンドを構成する。
(すでに先達のブログなどで既出なので省略する。)

	private byte[] buildPushData(String url, String browserStartupParam){
		
		byte[] content = packContent(packSegment(buildPushStartBrowserSegment(0x02, url, browserStartupParam)));
                ... (略)
		return data;
	}

Push送信機能を実行する

こんな感じで @hide なRawTagConnection をリフレクションしてコマンドを発行すれば、受け手側のLinx3Dに冒頭のスクリーンショットのように確認ダイアログが表示される。

プログラム中の DelegateFactory はリフレクションのためのユーティリティクラス。

Proxy.newProxyInstance やばい @hideがベイビー・サブミッション

INfcAdapter、IRawTagConnection は@hideなクラス、メソッドを扱うためのプロキシインターフェイスで、プレフィクスの"I"を除いた同名のNFC APIのクラスのメソッドを呼び出している。


		NfcAdapter nfcAdapter = NfcAdapter.getDefaultAdapter();
		INfcAdapter iNfcAdapter = DelegateFactory.create(INfcAdapter.class,
				nfcAdapter);

		Object rawTagConnection = iNfcAdapter.createRawTagConnection(tag);
		IRawTagConnection iRawTagConnection = DelegateFactory.create(
				IRawTagConnection.class, rawTagConnection);

		String url = editUrl.getEditableText().toString();
		String browserStartupParam = "Hello Android!!";

		byte[] pushData = buildPushData(url, browserStartupParam);
		try {
			iRawTagConnection.transceive(pushData);
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			iRawTagConnection.close();
		}

IntentをPush。。。したい

AndroidのPush送信機能では、Android同士の場合はIntentを飛ばすことができる。
しかし、これに関しては公開された情報がないため、まだ倒せていない。 


Push送信機能をnfc-felicaで使ってみた。

Push送信機能のライブラリは MFCUtility_x.x.x.jar(xは適当なバージョン番号)として配布されている。(以下MFC)
MFCではブラウザ起動のコマンドは、次のように表現される。

import com.felicanetworks.mfc.PushStartBrowserSegment;

...(略)
		PushStartBrowserSegment segment = new PushStartBrowserSegment(url,
				browserStartupParam);


これを @Kazzzさんが公開されている nfc-felicaと組み合わせてみた。 

まずはCommandPacketを継承して PushCommand を作る。
PushCommandはコンストラクションで MFCのPushStartBrowserSegmentを受け取り、nfc-felicaの内部表現に変換する。

package com.undrdevelopment.android.felica.push;

import java.nio.ByteBuffer;

import net.kazzz.felica.FeliCaException;
import net.kazzz.felica.lib.FeliCaLib;
import net.kazzz.felica.lib.FeliCaLib.CommandPacket;
import net.kazzz.felica.lib.FeliCaLib.IDm;

import com.felicanetworks.mfc.PushSegment;
import com.felicanetworks.mfc.PushStartBrowserSegment;

public class PushCommand extends CommandPacket {

	public static final byte PUSH = (byte) 0xb0;

	static {
		FeliCaLib.commandMap.put(PUSH, "Push");
	}

	public PushCommand(IDm idm, PushSegment segment) throws FeliCaException {
		super(PUSH, idm, packContent(packSegment(buildData(segment))));
	}

	private static byte[] packContent(byte[] segments) {
		byte[] buffer = new byte[segments.length + 1];
		buffer[0] = (byte) segments.length;
		System.arraycopy(segments, 0, buffer, 1, segments.length);
		return buffer;
	}

	private static byte[] packSegment(byte[]... segments) {

		int bytes = 3; // 個別部数(1byte) + チェックサム(2bytes)
		for (int i = 0; i < segments.length; ++i)
			bytes += segments[i].length;

		ByteBuffer buffer = ByteBuffer.allocate(bytes);

		// 個別部数
		buffer.put((byte) segments.length);

		// 個別部
		for (int i = 0; i < segments.length; ++i)
			buffer.put(segments[i]);

		// チェックサム
		int sum = segments.length;
		for (int i = 0; i < segments.length; ++i) {
			byte[] e = segments[i];
			for (int j = 0; j < e.length; ++j)
				sum += e[j];
		}
		int checksum = -sum & 0xffff;

		putAsBigEndian(checksum, buffer);

		return buffer.array();
	}

	private static byte[][] buildData(PushSegment segment)
			throws FeliCaException {
		if (segment instanceof PushStartBrowserSegment)
			return buildPushStartBrowserSegment((PushStartBrowserSegment) segment);
		throw new IllegalArgumentException("not supported " + segment);
	}

	private static byte[][] buildPushStartBrowserSegment(
			PushStartBrowserSegment segment) {

		final int type = segment.getType();
		final String url = segment.getURL();
		final String browserStartupParam = segment.getBrowserStartupParam();

		byte[] urlByte = url.getBytes();
		byte[] browserStartupParamByte = browserStartupParam == null ? new byte[0]
				: browserStartupParam.getBytes();

		int capacity = urlByte.length + browserStartupParamByte.length + 5;

		ByteBuffer buffer = ByteBuffer.allocate(capacity);

		// 個別部ヘッダ

		// 起動制御情報
		buffer.put((byte) type);
		// 個別部パラメータサイズ
		int paramSize = urlByte.length + browserStartupParamByte.length + 2; // urlLength(2bytes)
		putAsLittleEndian(paramSize, buffer);

		// 個別部パラメータ

		// URLサイズ
		putAsLittleEndian(urlByte.length, buffer);
		// URL
		buffer.put(urlByte);
		// (ブラウザスタートアップパラメータ)
		buffer.put(browserStartupParamByte);

		return new byte[][] { buffer.array() };
	}

	private static void putAsLittleEndian(int i, ByteBuffer buffer) {
		buffer.put((byte) ((i >> 0) & 0xff));
		buffer.put((byte) ((i >> 8) & 0xff));
	}

	private static void putAsBigEndian(int i, ByteBuffer buffer){
		buffer.put((byte) ((i >> 8) & 0xff));
		buffer.put((byte) ((i >> 0) & 0xff));
	}
}


呼び出し部分。

		String url = editUrl.getEditableText().toString();
		String browserStartupParam = "Hello Android!!";

		PushStartBrowserSegment segment = new PushStartBrowserSegment(url,
				browserStartupParam);

		PushCommand pushCommand;
		try {
			pushCommand = new PushCommand(new IDm(idm), segment);
			FeliCaLib.execute(tag, pushCommand);
		} catch (FeliCaException e) {
			e.printStackTrace();
		}


(オチがない)

  • 以上 -

FeliCa™ はソニー登録商標です。