Hatena::ブログ(Diary)

明日の鍵

2012-12-07

0次発行FeliCa LiteにNDEFを書き込む

まえがき

Android Advent Calendar 2012 (表)の8日目担当の@tomorrowkey です!

裏は @rukiadia さんです。

がんばります!

いきさつ

0次発行状態のFeliCa LiteにNDEFを書き込めるソフトウェアがなかったので、自分で作りました。

WindowsではPaSoRi+NDEFWriterで、1次発行状態にすると同時にNDEFを書き込むことができます。

NDEFを書き込むために1次発行は必須ではないので、0次発行の状態でNDEFを書き込めるようにしました。

某イベントで2,000枚のFeliCa LiteにSmartPosterを書き込む必要がありました。

当初はPaSoRi+NDEFWriterでやろうかと思ったのですが

1枚でも書き込むためにURLを入力したり、ダイアログのOKボタンを押したりと

オペレーションが煩雑だったのでアプリを作り、連続で書き込めるようにしたのです。

その時はただ同じ値をかければよかったので、マジックナンバーの嵐だったのですが

今回、値を動的に変えられるように、あとわりと綺麗なソースになるように書き直しました。

ライブラリは使っておらず、スクラッチしています。

まだまだ手を抜いてるところがたくさんあるので、なんとかしたいです。

てきとーですが、ここからは各所の解説など書きます。

アプリがFeliCa Liteに反応するようにする

use-permission

NFCを使うためにpermissionの設定が必要です。

/AndroidManifest.xml

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

NFCのIntentはNEW_TASKがついた状態で飛んできます。

いちいち新しいActivityが起動されてはうざいので

launchModeにsingleTaskもしくはsingleInstanceを指定して防ぎます。

android:launchMode="singleTask"
enableForegroundDispatch

NFCが反応したら優先して自分のアプリが起動されるようにforegroundDispatchという機能を使います。

アプリがフォアグラウンドに表示されている状態でNFCをフックしたいので、onResumeに処理を書きます。

あとでonPauseに解除するコードを書けばOKです。

/src/jp/tomorrowkey/android/felicalitewriter/WriteActivity.java

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

  mNfcAdapter = NfcAdapter.getDefaultAdapter(this);
  if (mNfcAdapter == null) {
    Toast.makeText(getApplicationContext(), "not found NFC feature", Toast.LENGTH_SHORT)
        .show();
    finish();
    return;
  }

  if (!mNfcAdapter.isEnabled()) {
    Toast.makeText(getApplicationContext(), "NFC feature is not available",
        Toast.LENGTH_SHORT).show();
    finish();
    return;
  }

  PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, new Intent(this,
      getClass()), 0);
  IntentFilter[] intentFilter = new IntentFilter[] {
    new IntentFilter(NfcAdapter.ACTION_TECH_DISCOVERED),
  };
  String[][] techList = new String[][] {
    {
      android.nfc.tech.NfcF.class.getName()
    }
  };
  mNfcAdapter.enableForegroundDispatch(this, pendingIntent, intentFilter, techList);

}

pendingIntentにはNFCタグを検出した時に投げて欲しいIntentを入れてあげます。

IntentFilterとtechListには検出したいNFCタグの種類を指定します。

IntentFilterのActionには

  • ACTION_NDEF_DISCOVERED
    • NDEFフォーマットされたNFCタグ
  • ACTION_TECH_DISCOVERED
    • NDEFフォーマットされていないタグの中で、タグの種類指定ができる
  • ACTION_TAG_DISCOVERED
    • どんなタグでもいいから検出する

techListにはタグの種類を指定します。

今回はFeliCa Liteだけに対応したいので

  • ActionにACTION_TECH_DISCOVERED
  • techListにNfcF

を指定します。

disableForegroundDispatch

アプリがバックグラウンドに入ったときには、NFCフックしなくていいので

有効にしたforegroundDispatchを無効にします。

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

  mNfcAdapter.disableForegroundDispatch(this);
}

FeliCa の仕様について

ここまで作るとActivity#onNewIntent(:intent)にNFCタグの情報が飛んでくるようになります。

ExtraからTagを引っ張ってNDEFを書いたりするわけですが

中には書けないFeliCa Liteタグがあります。

それはNDEFフラグがたっていないものです。*1

通販等で購入したFeliCa Liteタグはたいていたっていない状態です(あたりまえですね)

つまり、NDEFフラグを立てればいいわけですが

Android SDKでは、FeliCa LiteのNFCフラグをたてる機能がついていません。

自分でポチポチたてる必要があります。

そのためにFeliCa Liteの仕様について知る必要があります。

がんばろう。

FeliCa仕様書

FeliCaの仕様書はSonyのサイトにあります。

FeliCaカード ユーザーズマニュアル 抜粋版
http://www.sony.co.jp/Products/felica/business/tech-support/index.html?j-short=tech-support#Standard01
FeliCa Liteユーザーズマニュアル
http://www.sony.co.jp/Products/felica/business/tech-support/index.html?j-short=tech-support#Lite01

一般的なコマンドの体型

コマンド長 : 1 Byte

先頭に全体のコマンド長が入ります。

それはコマンド長を格納する1Byteも含みます。

よく忘れます。

コマンド : 1 Byte

コマンドを指定します。

FeliCa Liteの仕様書に載っているコマンドは以下のもののみです。

  • Polling
    • 0x00
  • Read Without Encryption
    • 0x06
  • Write Without Encryption
    • 0x08

FeliCa Standardにはもうすこしあります。

IDm : 8 Byte

コマンドによりますが、そのあとにはだいたいIDmが入ります。

IDmとはFeliCaタグひとつひとつで一意に識別できるIDのことです。

製造番号みたいなもので、同じ通信領域内に複数のFeliCaタグがあった際に

通信相手を選ぶために使われます。

書き込みコマンドについて

FeliCa Liteには以下のような制限があります。

  • 一度に書き込めるブロック数は1つまで
  • サービスコードは0x0009に固定
  • アクセスモードは000に固定
  • サービスコード順番は0000に固定

FeliCa Standardと比べれば考えることが少なくて楽です。

以上のことを踏まえて書き込みコマンドを実装すると以下のようになります。

/**
 * Write Without Encryptionコマンドを発行します<br>
 * FeliCa Liteなので、1度のコマンド発行で1ブロックだけ書き込めます
 * 
 * @param idm IDm
 * @param blockNumber ブロック番号
 * @param data 書き込みデータ
 * @return レスポンス
 * @throws TagLostException
 * @throws IOException
 */
public byte[] writeWithoutEncryption(byte[] idm, int blockNumber, byte[] data)
    throws TagLostException, IOException {
  if (idm == null || idm.length == 0)
    throw new IllegalArgumentException();

  ByteBuffer byteBuffer = ByteBuffer.allocate(31);

  // Write Without Encryption
  byteBuffer.put((byte)0x08);

  // IDm
  byteBuffer.put(idm);

  // サービス数
  // FeliCa Liteなので1に固定
  byteBuffer.put((byte)0x01);

  // サービスコード(リトルエンディアン)
  // 0x00 0x09
  byteBuffer.put((byte)0x09);
  byteBuffer.put((byte)0x00);

  // ブロックリスト
  // 長さ 2Byteなので1b
  // アクセスモード FeliCa Liteなので000bに固定
  // サービスコード順番 FeliCa Liteなので0000bに固定
  // ブロック番号 引数から指定
  byteBuffer.put((byte)0x80);
  byteBuffer.put((byte)blockNumber);

  // 書き込みデータ
  byteBuffer.put(data);

  byte[] command = byteBuffer.array();
  byte[] response = executeCommand(command);

  return response;
}

コマンド長はコマンド発行直前に付加するためはぶいてあります。

FeliCa Liteのメモリマップについて

これで書き込みコマンド組立は完成しました。

次にどこになにを書けばいいか調べます。

FeliCa Liteのメモリマップは以下のようになっています。

(これもFeliCa Liteの仕様書に書かれています)

f:id:tomorrowkey:20121205224921p:image

S_PADと書かれた部分が、ユーザーブロックと呼ばれる場所で

ここにさまざまなデータを書き込みます。

そのほかの領域はいろいろな役割や機能があるので詳しくは仕様書を参考にしてください。

ユーザブロック以外にこんかい必要になるのが

MCブロック(メモリーコンフィグレーションブロック)です。

MCブロックのSYS_OP(System Option)に0x01を指定することで

FeliCa LiteがNDEF化されます。

通常は0x00です。

FeliCa LiteのNDEFフラグをたてる

さっそく書き込みコマンドを使ってNDEF化します。

// NDEF化します
byte[] data = new byte[] {
    (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0x01, (byte) 0x07, (byte) 0x00, (byte) 0x00, (byte) 0x00, 
    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, 
};
felicaLiteTag.writeWithoutEncryption(idm, 0x88, data);

MCブロックに書き込みたいので、ブロック番号に0x88を指定し

データに1ブロック分のデータを指定します。

一度の書き込みで1ブロックすべてを指定しなければなりません。

このブロックの中には一度特定の値を書き込むと、二度とその値を変更できない箇所があります。

ですので、気をつけて書き込みましょう。

本来であれば現在のブロックデータを読み込んでSYS_OPの部分だけ変更したほうがいいんだけど

手を抜きました。

NDEFデータを作る

タグのメタデータっぽいところはNDEFに対応できたから

今度はデータ本体を作ります。

AndroidはNFC対応!だなんて謳ってるくらいだから、NDEFの各フォーマットビルダーくらい用意されてるよなーって

思ったんですが、ありません。

仕様書を読んで自分で作るか、どこかからひろってきましょう。

NDEF仕様書はNFC Forumにあります。

NDEFについては'NFC Data Exchange Format (NDEF) Technical Specification' という仕様書

RTDについては'Record Type Definition Technical Specifications' の各仕様書

を参考にしてください。

NFC Forum : Technical Specifications 
http://www.nfc-forum.org/specs/spec_list/

以前NDEFについて発表したときの資料はここから

避けては通れないバイナリ地獄 - NDEFってなんだろう - 
http://www.slideshare.net/tomorrowkey/ndef-13784268
避けては通れないバイナリ地獄 - NDEFってなんだろう - 
http://www.slideshare.net/tomorrowkey/ndef-13784268

ちなみに身近なところでNDEFデータを作るサンプルとしては

ApiDemosにRTD-Textを作るコードが入っています。

NDEFデータができたら書き込みます。

NDEFフラグを変更した時と同じ要領です。

NDEFヘッダを作成する

データができたところで、S_PAD(スクラッチパッド)に書き込みたいところですが

そのまま書いてもNDEFだと認識してくれません。

実はS_PAD0をAttribute Information Blockにしないといけないためです。

そのへんの仕様はNFC Forumに定義されています。

FeliCa系のタグはNFC ForumではType 3となっているので

NFC Forum Type 3 Tag Operation Specification

という仕様書を見てみると書いてあります。

NFC Forum : Technical Specifications 
http://www.nfc-forum.org/specs/spec_list/

f:id:tomorrowkey:20121206205014p:image

  • Ver

バージョンを指定します。

1.0を表す0x10を指定します。

  • Nbr

タグ読み取り時に、一度に読めるブロック数を指定します。

FeliCa Liteなので0x04を指定します。

  • Nbw

タグ書き込み時に、一度に書き込めるブロック数を指定します。

FeliCa Liteなので0x01を指定します。

  • Nmaxb

NDEFデータとして使えるブロック数を指定します。

13ブロックなので0x00 0x0Dを指定します。

  • WriteF

データは一つのタグに収まるので、0x00を指定します。

  • RW Flag

今後も書き込みできる状態なので、0x01を指定します。

  • Ln

NDEFデータの長さを指定します。

実際のデータ長から設定されます。

  • Checksum

チェックサムです。

ブロック0のすべてを加算した値を設定します。

private byte[] createNdefHeader(int ndefLength) {
  ByteBuffer buffer = ByteBuffer.allocate(16);

  // Ver
  buffer.put((byte)0x10);

  // Nbr
  // Read Without Encrypitonで一度に読めるブロック数を指定します
  // FeliCa Liteなので、一度に4ブロック読み込める
  buffer.put((byte)0x04);

  // Nbw
  // Write Without Encryptionで一度に書き込めるブロック数を指定します
  // FeliCa Liteなので、一度に1ブロック書き込める
  buffer.put((byte)0x01);

  // Nmaxb
  // NDEFとして使用できるブロック数
  // FeliCa Liteなので、データ領域は13ブロックまで
  buffer.put((byte)0x00);
  buffer.put((byte)0x0d);

  // unused
  buffer.put((byte)0x00);
  buffer.put((byte)0x00);
  buffer.put((byte)0x00);
  buffer.put((byte)0x00);

  // WriteF
  // 一枚で完結しているので、0x00
  buffer.put((byte)0x00);

  // RW Flag
  // Read Writeなので0x01
  buffer.put((byte)0x01);

  // Ln
  // NDEFデータの長さを指定します
  buffer.put((byte)((ndefLength >>> 16) & 0xff));
  buffer.put((byte)((ndefLength >>> 8) & 0xff));
  buffer.put((byte)(ndefLength & 0xff));

  // Checksum
  // チェックサムを指定します
  buffer.put(checksum(buffer.array()));

  return buffer.array();
}

/**
 * チェックサムを作成します<br>
 * すべてのバイト配列の合計を計算します
 * 
 * @param byteArray
 * @return
 */
private byte[] checksum(byte[] byteArray) {
  int sum = 0;
  for (byte b : byteArray) {
    sum += b & 0xff;
  }
  return new byte[] {
    (byte)((sum >>> 8) & 0xff), (byte)(sum & 0xff)
  };
}
NDEFデータを書き込む

これで、すべての材料は揃いました。

ユーザーブロックの0から順にデータを書き込みます。

/**
 * NdefMessageを書き込みます
 * 
 * @param idm IDm
 * @param ndefMessage NDEF
 * @throws SizeOverflowException NdefMessageのサイズが大きすぎる場合に発生します
 * @throws TagLostException
 * @throws IOException
 */
public void writeNdefMessage(byte[] idm, NdefMessage ndefMessage) throws SizeOverflowException,
    TagLostException, IOException {
  if (idm == null || idm.length == 0)
    throw new IllegalArgumentException();
  if (ndefMessage == null)
    throw new IllegalArgumentException();

  byte[][] datas = mappingBlock(ndefMessage);

  for (int blockNumber = 0; blockNumber <= 13; blockNumber++) {
    // FIXME レスポンスを握りつぶしているので、どうにかする
    writeWithoutEncryption(idm, blockNumber, datas[blockNumber]);
  }
}

/**
 * NdefMessageからFeliCa Liteの各ブロックにマッピングします
 * 
 * @param ndefMessage
 * @return
 * @throws SizeOverflowException
 * @throws IOException
 */
private byte[][] mappingBlock(NdefMessage ndefMessage) throws SizeOverflowException,
    IOException {
  byte[] ndefMessageBytes = ndefMessage.toByteArray();
  int ndefMessageBytesLength = ndefMessageBytes.length;
  int blockCount = (int)Math.ceil(ndefMessageBytesLength / 16.0);
  if (blockCount > 13)
    throw new SizeOverflowException(ndefMessageBytesLength, 16 * 13);

  byte[][] datas = new byte[14][16];
  datas[0] = createNdefHeader(ndefMessageBytesLength);

  ByteArrayInputStream inputStream = new ByteArrayInputStream(ndefMessageBytes);
  try {
    inputStream = new ByteArrayInputStream(ndefMessageBytes);
    int readLength;
    for (int i = 1; i < datas.length; i++) {
      readLength = inputStream.read(datas[i]);
      if (readLength == -1)
        break;
    }
  } finally {
    try {
      if (inputStream != null)
        inputStream.close();
    } catch (IOException e) {
      // ignore
    }
  }

  return datas;
}

完成

これでNDEFを書きこめるようになりました。

たのしいですね。

ソースコードはここに晒しておきます。

https://github.com/tomorrowkey/FeliCaLiteWriter

最後に

明日はNexus 7がなかなか届かないことで有名な@sekitoba さんと

@sugimotoak さんです。

みんながんばれー

*1:ユーザーエリアがReadOnlyに変更されている場合もあります

2012-02-26

フッタにボタンを表示する4.0

4.0以前はこちら

フッタにボタンを表示する - 明日の鍵 
http://d.hatena.ne.jp/tomorrowkey/20110809/1312869978

ボタン2つ

f:id:tomorrowkey:20120226173416p:image:w250

この画面のレイアウトファイルはどうなっているかソースを探す

uninstall_confirm.xml


<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical"
    android:divider="?android:attr/dividerHorizontal"
    android:showDividers="beginning"
    android:paddingTop="16dip">

  <LinearLayout
      style="?android:attr/buttonBarStyle"
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:orientation="horizontal"
      android:measureWithLargestChild="true">

    <LinearLayout android:id="@+id/leftSpacer"
      android:layout_weight="0.25"
      android:layout_width="0dip"
      android:layout_height="wrap_content"
      android:orientation="horizontal"
      android:visibility="gone" />

    <Button android:id="@+id/cancel_button"
      android:layout_width="0dip"
      android:layout_height="wrap_content"
      android:layout_gravity="left"
      android:layout_weight="1"
      android:text="@string/cancel"
      android:maxLines="2"
      style="?android:attr/buttonBarButtonStyle" />

    <Button android:id="@+id/ok_button"
      android:layout_width="0dip"
      android:layout_height="wrap_content"
      android:layout_gravity="right"
      android:layout_weight="1"
      android:text="@string/ok"
      android:maxLines="2"
      style="?android:attr/buttonBarButtonStyle" />

    <LinearLayout android:id="@+id/rightSpacer"
      android:layout_width="0dip"
      android:layout_weight="0.25"
      android:layout_height="wrap_content"
      android:orientation="horizontal"
      android:visibility="gone" />

  </LinearLayout>
</LinearLayout>

ボタン1つ

軽く探したけど、だいたいActionBarで実現してるみたい

レイアウトで実装したいなら以下でそれっぽくできる

<LinearLayout
  style="?android:attr/buttonBarStyle"
  android:layout_width="match_parent"
  android:layout_height="wrap_content"
  android:measureWithLargestChild="true"
  android:orientation="horizontal" >

  <Button
    android:id="@+id/done_button"
    style="?android:attr/buttonBarButtonStyle"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:drawableLeft="@drawable/ok"
    android:drawableRight="@drawable/space"
    android:maxLines="2"
    android:text="@string/done" />
</LinearLayout>

2012-02-10

乗換案内を検索するアプリを作ったよ

f:id:tomorrowkey:20120210123528p:image:h400

簡単に使える乗換案内アプリを作りました。


毎回駅名を入力して、検索をするなんてめんどうです。

いつも使うルートを予め登録しておけば、あとは選択するだけで

今の時間で検索してくれます


検索結果をわざわざ保存なんてめんどうです。

検索したら自動的に保存してくれます

ダウンロード

https://market.android.com/details?id=jp.tomorrowkey.android.transitguide

サイト

Androidアプリ 乗換案内

https://sites.google.com/site/androidtrainguide/

その他スクリーンショット

f:id:tomorrowkey:20120210123529p:image:h400

f:id:tomorrowkey:20120210123531p:image:h400

f:id:tomorrowkey:20120210123530p:image:h400

2011-12-03

RealViewSwitcherを作りました

Android Advent Calendar

このエントリはAndroid Advent Calendarのエントリの1つです。

クリスマスまでに1日1記事更新されるので、ぜひご覧になってください。

Android Advent Calendar http://androidadvent.blogspot.com/

RealViewSwitcherを作りました

Launcherアプリのように、1ページごとに切り替わるViewGroupを作りました。

すでにcompatibility packageでViewPagerが公開されていますが

これを作った当時は、まだcompatibility packageがありませんでした。

RealViewSwitcherというViewGroupです。

"RealViewSwitcher"で検索すると同じようなクラスがでてきますが、私に命名センスがないので名前だけ拝借しました。

すでにデコ美シンプルQRコードにインプリされています。

動画

D

3つページがあり、ぐるぐる循環します。


D

もちろんフリックする以外にコントロールを追加することが可能です。


D

ばいーんの挙動を変更することも可能です。

簡単な使い方

コードから動的にViewの追加も可能ですが、xmlにも記述できます。

通常の独自Viewと同じやり方です。

<?xml version="1.0" encoding="utf-8"?>
<view xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:app="http://schemas.android.com/apk/res/jp.tomorrowkey.android.realviewswitchersample"
  android:id="@+id/viewSwitcher"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  class="jp.tomorrowkey.android.realviewswitcher.widget.RealViewSwitcher"
  app:interpolator="overshoot_interpolator" >

  <TextView
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/color1"
    android:gravity="center"
    android:text="1"
    android:textSize="64sp" />

  <TextView
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/color2"
    android:gravity="center"
    android:text="2"
    android:textSize="64sp" />

  <TextView
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/color3"
    android:gravity="center"
    android:text="3"
    android:textSize="64sp" />
</view>

ばいーんの挙動を変更したい場合に限り、

xmlns:app="http://schemas.android.com/apk/res/使用するアプリのパッケージ名"

app:interpolator="overshoot_interpolator"

が必要です。

interpolatorに設定可能なリストはここに定義されています。

RealViewSwitcher/res/values/attrs.xml at master from tomorrowkey/RealViewSwitcher - GitHub 
https://github.com/tomorrowkey/RealViewSwitcher/blob/master/RealViewSwitcher/res/values/attrs.xml

注意点

人によってフリックするときの感覚が違うと思うので、閾値の調整が必要です。

ぜひ、コードを読んで調整してみてください。

サンプルアプリ

https://github.com/tomorrowkey/RealViewSwitcher/downloads

ソースコード

tomorrowkey/RealViewSwitcher - GitHub

https://github.com/tomorrowkey/RealViewSwitcher

最後に

初めてViewGroupを書いてみて、Androidの描画の仕組みについて理解が深まりました。

Androidの独自Viewは大変だけど楽しいですね。



2011-10-05

Canvas#drawTextするときのメモ

どの値が何を表しているかよく忘れるのでメモ

原点

f:id:tomorrowkey:20111006002741p:image:w400

Canvas#drawText() に指定する座標はここになる。

f:id:tomorrowkey:20111006002740p:image:w400

文字の幅を取得するにはPaint#measureText(:String):floatを使う。

高さ

f:id:tomorrowkey:20111006002739p:image:w400

Top一番上
Ascent文字の上限
Leading原点のyと同じ位置
Descent文字の下限
Bottom一番下

文字のそれぞれのy位置が取得できる。

Paint#getFontMetrics()で取得できるFontMetrixから、フィールドの値を取り出せる。

bottom - topをすれば高さが取得できる。

値(追記)

Paint.setTextSize(:int)に96を設定した場合のそれぞれの値

leadingは必ず0になる

他はleadingからの相対的な位置の差

top-100.59375
ascent-89.109375
leading0.0
descent22.640625
bottom26.015625

調査に使用したソースコード

http://code.google.com/p/tomorrowkey/source/browse/#svn%2Ftrunk%2FFontMetrics