afjkの技術メモ

主に技術系備忘録

Bluetooth機器にタッチで接続出来るNFCタグを作る(その4)

前回紹介したBluetooth NFCタグライタで、いろいろなBluetooth機器用のNFC接続タグを作って試してみました。

結果、

スピーカー(Creative D100):OK

ヘッドセット(LBT-HS05):OK

キーボード(mac用):NG

MagicTrackpad(mac用):NG

Androidスマートフォン:NG

iPhone:NG

どうやら、A2DPとHeadsetプロファイルにしか対応していなさそうです。

NFCタグにはUUID情報も書き込んでいるのに何故だ?と思い調べて見ました。

NFCでの接続時のLogcatを取る。

関係ありそうな箇所を抜き出してみました。

D/NfcDispatcher(685): dispatch tag: TAG: Tech [android.nfc.tech.MifareUltralight, android.nfc.tech.NfcA, android.nfc.tech.Ndef] message: NdefMessage [NdefRecord tnf=2 type=6170706C69636174696F6E2F766E642E626C7565746F6F74682E65702E6F6F62 
payload=22008A7E3A3C02000E0943726561746976652044313030040D04042405030B110E11]
D/NfcHandover(685): tryHandover(): NdefMessage [NdefRecord tnf=2 type=6170706C69636174696F6E2F766E642E626C7565746F6F74682E65702E6F6F62 payload=22008A7E3A3C02000E0943726561746976652044313030040D04042405030B110E11]
D/BluetoothAdapterProperties(1672): CONNECTION_STATE_CHANGE: 00:02:3C:3A:7E:8A: 0 -> 1
D/CachedBluetoothDevice(1599): onProfileStateChanged: profile HEADSET newProfileState 1
:
D/CachedBluetoothDevice(1599): onProfileStateChanged: profile A2DP newProfileState 1
:
D/CachedBluetoothDevice(1599): onProfileStateChanged: profile HEADSET newProfileState 0
:
D/CachedBluetoothDevice(1599): onProfileStateChanged: profile A2DP newProfileState 2
:
D/BluetoothAdapterProperties(1672): CONNECTION_STATE_CHANGE: 00:02:3C:3A:7E:8A: 1 -> 2
:
D/CachedBluetoothDevice(1599): onProfileStateChanged: profile HEADSET newProfileState 1
:
D/CachedBluetoothDevice(1599): onProfileStateChanged: profile HEADSET newProfileState 0

何故かA2DPとともにHEADSETプロファイルの接続シーケンスも走っているように見えます。

ソースコードを確認してみましょう。
その気になればソースコードを調べられるのがオープンソースの魅力です。
Androidソースコード検索サービスで、ログに出力されているキーワードを元に検索します。
ここでは、http://tools.oesf.biz/android-4.2.0_r1.0/を参照します。

結果、BluetoothとのNFCでの接続シーケンスは以下に実装されていました。
/src/com/android/nfc/handover/BluetoothHeadsetHandover.java
start()->nextStep()->nextStepConnect()と呼び出しながら接続シーケンスが実行され、

    245     void nextStepConnect() {
    246         switch (mState) {
    260             :
    261             case STATE_BONDING:
    262                 // Bluetooth Profile service will correctly serialize
    263                 // HFP then A2DP connect
    264                 mState = STATE_CONNECTING;
    265                 synchronized (mLock) {
    266                     if (mHeadset.getConnectionState(mDevice) != BluetoothProfile.STATE_CONNECTED) {
    267                         mHfpResult = RESULT_PENDING;
    268                         mHeadset.connect(mDevice); //★HeadSetの接続
    269                     } else {
    270                         mHfpResult = RESULT_CONNECTED;
    271                     }
    272                     if (mA2dp.getConnectionState(mDevice) != BluetoothProfile.STATE_CONNECTED) {
    273                         mA2dpResult = RESULT_PENDING;
    274                         mA2dp.connect(mDevice);  // ★A2DPの接続
    275                     } else {
    276                         mA2dpResult = RESULT_CONNECTED;
    277                     }
    278                     if (mA2dpResult == RESULT_PENDING || mHfpResult == RESULT_PENDING) {
    279                         toast(mContext.getString(R.string.connecting_headset) + " " + mName + "...");
    280                         break;
    281                     }
    282                 }
    283                 // fall-through

やはり、HEADSETとA2DPの接続しかしていない。それも、とにかく両方とも接続要求を出し、接続された方を利用するという実装の様です。

では、NFCに設定したUUIDは?NFCからのデータ読み込み部分を見てみましょう。

    777     public boolean tryHandover(NdefMessage m) {
    778         if (m == null) return false;
    779         if (mBluetoothAdapter == null) return false;
    780 
    781         if (DBG) Log.d(TAG, "tryHandover(): " + m.toString());
    782 
    783         BluetoothHandoverData handover = parse(m);//①
    784         if (handover == null) return false;
    785         if (!handover.valid) return true;
    786 
    787         synchronized (HandoverManager.this) {
    788             if (mBluetoothAdapter == null) {
    789                 if (DBG) Log.d(TAG, "BT handover, but BT not available");
    790                 return true;
    791             }
    792             if (mBluetoothHeadsetHandover != null) {
    793                 if (DBG) Log.d(TAG, "BT handover already in progress, ignoring");
    794                 return true;
    795             }
    796             mBluetoothHeadsetHandover = new BluetoothHeadsetHandover(mContext, handover.device,
    797                     handover.name, mHandoverPowerManager, this);//②
    798             mBluetoothHeadsetHandover.start();
    799         }
    800         return true;
    801     }

①の箇所でNFCから取得したバイナリデータをパースし、BluetoothHandoverDataを生成しています。
BluetoothHeadsetHandoverが先ほどのBluetooth接続シーケンスを実装しているクラスです。
これに②で、①でのパース情報を渡しています。
では、①でのパース処理はどうなっているか。
/packages/apps/Nfc/src/com/android/nfc/handover/HandoverManager.java

    916     BluetoothHandoverData parseBtOob(ByteBuffer payload) {
    917         BluetoothHandoverData result = new BluetoothHandoverData();
    918         result.valid = false;
    919 
    920         try {
//①アドレス取得処理
    921             payload.position(2);
    922             byte[] address = new byte[6];
    923             payload.get(address);
    924             // ByteBuffer.order(LITTLE_ENDIAN) doesn't work for
    925             // ByteBuffer.get(byte[]), so manually swap order
    926             for (int i = 0; i < 3; i++) {
    927                 byte temp = address[i];
    928                 address[i] = address[5 - i];
    929                 address[5 - i] = temp;
    930             }
    931             result.device = mBluetoothAdapter.getRemoteDevice(address);
    932             result.valid = true;
    933 
//②デバイス名取得処理
    934             while (payload.remaining() > 0) {
    935                 byte[] nameBytes;
    936                 int len = payload.get();
    937                 int type = payload.get();
    938                 switch (type) {
    939                     case 0x08:  // short local name
    940                         nameBytes = new byte[len - 1];
    941                         payload.get(nameBytes);
    942                         result.name = new String(nameBytes, Charset.forName("UTF-8"));
    943                         break;
    944                     case 0x09:  // long local name
    945                         if (result.name != null) break;  // prefer short name
    946                         nameBytes = new byte[len - 1];
    947                         payload.get(nameBytes);
    948                         result.name = new String(nameBytes, Charset.forName("UTF-8"));
    949                         break;
    950                     default:
    951                         payload.position(payload.position() + len - 1);
    952                         break;
    953                 }
    954             }
    955         } catch (IllegalArgumentException e) {
    956             Log.i(TAG, "BT OOB: invalid BT address");
    957         } catch (BufferUnderflowException e) {
    958             Log.i(TAG, "BT OOB: payload shorter than expected");
    959         }
    960         if (result.valid && result.name == null) result.name = "";
    961         return result;
    962     }

①②のデバイスアドレス、デバイス名以外の情報は取得されていません。

折角NFCタグに書き込んだClass情報やUUID情報は無駄という訳です。
確かに、仕様書上はデバイスアドレス以外はオプション情報なのですが、設定されている場合は有効に利用してほしい物ですね。