2013/10/20

Nexus7でPASMO(Suica)の履歴を読んでみた

先日、財布の上にNexus7を置いたら「ポロリ〜ン」って変な音がしたので何かと思ったら PASMO に反応していた。

Androidは2.3からFelica等の非接触カードに対応しているらしくNexus7にも付いていたわけだ。
音を鳴らしたアプリは分からずじまいだが面白そうなので自分でアプリを書いてみた。

PASMO/Suicaの仕様

  • Felica仕様の詳細は Sonyの「Felicaカードユーザマニュアル抜粋版」と言うPDFをDLして読んでください。
    • Felica技術情報のページ
    • APIで使うのはこの中の「2.3 アプリケーション層」のコマンドパケットです。
    • Pollingまでは終わった状態からAndroidアプリは始まります。
    • 普通使うのは Read/Write Without Encryption くらいですが引数の指定の仕方が結構ややこしいです。
    • ざっくりとした解説はこちらのページにあります。
  • Felicaの用語
    • サービスコード:
      • 1枚のカードに複数のサービスを同居させる為にサービスに割り当てられた2byteのコード。
      • コードの管理者は良く分かりません。Sony?
    • ブロック:
      • サービスの扱うデータブロックの事で16byte固定です。
      • 何ブロック使えるかはサービス毎の割り当てにより決まるようです。
      • ブロックの中の仕様はサービス毎に仕様が定義されます。
  • PASMO/Suica の仕様は非公開ですが有志で解析された情報があるのでそれを参考にします。
    • felicalibのWiki
    • ここから履歴のサービスコードが 0x090f とわかりました。
    • 履歴の保存フォーマットもここを参考にしています。

AndroidManifext.xml

  • NFC の uses-feature と uses-permission を宣言します。
  • intent-filter でカードを認識したら Activity を起動するようにします。

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="org.kotemaru.android.sample.nfc"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-sdk android:minSdkVersion="14" android:targetSdkVersion="17" />

    <uses-feature android:name="android.hardware.nfc" android:required="true" />    ←追加
    <uses-permission android:name="android.permission.NFC" />                       ←追加

    <application
        android:allowBackup="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >
        <activity
            android:name="org.kotemaru.android.sample.nfc.MainActivity"
            android:label="@string/app_name" >
            <intent-filter>
                <action android:name="android.nfc.action.TAG_DISCOVERED" />         ←変更
                <category android:name="android.intent.category.DEFAULT" />         ←変更
            </intent-filter>

            <meta-data      ←追加
                android:name="android.nfc.action.TAG_DISCOVERED"
                android:resource="@xml/nfc_filter" />
        </activity>
    </application>
</manifest>

res/xml/nfc_filter.xml

  • 使いたいカードのパッケージを設定します。
  • 今回はFelicaだけなので NfcF です。

<?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>

MainActivity.java

  • IntentからカードIDを取得してbyte配列のFelicaコマンドを送信します。
  • byte配列のレスポンスが帰るので解析すれば終わりです。
  • 履歴の保存レコード数は不明ですが私のPASMOでは最新15件しかありませんでした。
    • 1回の要求で取得できるレコード数は「製品により異なります」とマニュアルに書いてありました。
  • 注意事項:byte配列はリトル/ビッグ エンディアンが混在しています。仕様を良く確認してください。
package org.kotemaru.android.sample.nfc;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import android.nfc.NfcAdapter;
import android.nfc.Tag;
import android.nfc.tech.NfcF;
import android.os.Bundle;
import android.app.Activity;
import android.content.Intent;
import android.util.Log;
import android.widget.TextView;

public class MainActivity extends Activity {
    private static final String TAG = "NFCSample";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        TextView textView1 = (TextView) this.findViewById(R.id.textView1);

        // カードID取得。Activityはカード認識時起動に設定しているのでここで取れる。
        byte[] felicaIDm = new byte[]{0};
        Intent intent = getIntent();
        Tag tag = (Tag) intent.getParcelableExtra(NfcAdapter.EXTRA_TAG);
        if (tag != null) {
            felicaIDm = tag.getId();
        }


        NfcF nfc = NfcF.get(tag);
        try {
            nfc.connect();
            byte[] req = readWithoutEncryption(felicaIDm, 10);
            Log.d(TAG, "req:"+toHex(req));
            // カードにリクエスト送信
            byte[] res = nfc.transceive(req);
            Log.d(TAG, "res:"+toHex(res));
            nfc.close();
            // 結果を文字列に変換して表示
            textView1.setText(parse(res));
        } catch (Exception e) {
            Log.e(TAG, e.getMessage() , e);
            textView1.setText(e.toString());
        }
    }

    /**
     * 履歴読み込みFelicaコマンドの取得。
     * - Sonyの「Felicaユーザマニュアル抜粋」の仕様から。
     * - サービスコードは http://sourceforge.jp/projects/felicalib/wiki/suica の情報から
     * - 取得できる履歴数の上限は「製品により異なります」。
     * @param idm カードのID
     * @param size 取得する履歴の数
     * @return Felicaコマンド
     * @throws IOException
     */
    private byte[] readWithoutEncryption(byte[] idm, int size)
            throws IOException {
        ByteArrayOutputStream bout = new ByteArrayOutputStream(100);

        bout.write(0);           // データ長バイトのダミー
        bout.write(0x06);        // Felicaコマンド「Read Without Encryption」
        bout.write(idm);         // カードID 8byte
        bout.write(1);           // サービスコードリストの長さ(以下2バイトがこの数分繰り返す)
        bout.write(0x0f);        // 履歴のサービスコード下位バイト
        bout.write(0x09);        // 履歴のサービスコード上位バイト
        bout.write(size);        // ブロック数
        for (int i = 0; i < size; i++) {
            bout.write(0x80);    // ブロックエレメント上位バイト 「Felicaユーザマニュアル抜粋」の4.3項参照
            bout.write(i);       // ブロック番号
        }

        byte[] msg = bout.toByteArray();
        msg[0] = (byte) msg.length; // 先頭1バイトはデータ長
        return msg;
    }

    /**
     * 履歴Felica応答の解析。
     * @param res Felica応答
     * @return 文字列表現
     * @throws Exception
     */
    private String parse(byte[] res) throws Exception {
        // res[0] = データ長
        // res[1] = 0x07
        // res[2〜9] = カードID
        // res[10,11] = エラーコード。0=正常。 
        if (res[10] != 0x00) throw new RuntimeException("Felica error.");

        // res[12] = 応答ブロック数
        // res[13+n*16] = 履歴データ。16byte/ブロックの繰り返し。
        int size = res[12];
        String str = "";
        for (int i = 0; i < size; i++) {
            // 個々の履歴の解析。
            Rireki rireki = Rireki.parse(res, 13 + i * 16);
            str += rireki.toString() +"\n";
        }
        return str;
    }

    private String toHex(byte[] id) {
        StringBuilder sbuf = new StringBuilder();
        for (int i = 0; i < id.length; i++) {
            String hex = "0" + Integer.toString((int) id[i] & 0x0ff, 16);
            if (hex.length() > 2)
                hex = hex.substring(1, 3);
            sbuf.append(" " + i + ":" + hex);
        }
        return sbuf.toString();
    }

}

Rireki.java

  • 履歴のパーザとBeanです。
  • 駅コードから駅名を引くのは大きなテーブルが必要なので今回はパスです。
package org.kotemaru.android.sample.nfc;

import android.util.SparseArray;

/**
 * Pasumo履歴レコード。
 * - 資料:http://sourceforge.jp/projects/felicalib/wiki/suica
 */
public class Rireki {
    public int termId;
    public int procId;
    public int year;
    public int month;
    public int day;
    public String kind;
    public int remain;
    public int seqNo;
    public int reasion;

    public Rireki(){
    }

    public static Rireki parse(byte[] res, int off) {
        Rireki self = new Rireki();
        self.init(res, off);
        return self;
    }

    private void init(byte[] res, int off) {
        this.termId = res[off+0]; //0: 端末種
        this.procId = res[off+1]; //1: 処理
        //2-3: ??
        int mixInt = toInt(res, off, 4,5);
        this.year  = (mixInt >> 9) & 0x07f;
        this.month = (mixInt >> 5) & 0x00f;
        this.day   = mixInt & 0x01f;

        if (isBuppan(this.procId)) {
            this.kind = "物販";
        } else if (isBus(this.procId)) {
            this.kind = "バス";
        } else {
            this.kind = res[off+6] < 0x80 ? "JR" : "公営/私鉄" ;
        }
        this.remain  = toInt(res, off, 11,10); //10-11: 残高 (little endian)
        this.seqNo   = toInt(res, off, 12,13,14); //12-14: 連番
        this.reasion = res[off+15]; //15: リージョン 
    }

    private int toInt(byte[] res, int off, int... idx) {
        int num = 0;
        for (int i=0; i<idx.length; i++) {
            num = num << 8;
            num += ((int)res[off+idx[i]]) & 0x0ff;
        }
        return num;
    }
    private boolean isBuppan(int procId) {
        return procId == 70 || procId == 73 || procId == 74 
                || procId == 75 || procId == 198 || procId == 203;
    }
    private boolean isBus(int procId) {
        return procId == 13|| procId == 15|| procId ==  31|| procId == 35;
    }

    public String toString() {
        String str = seqNo
                +","+TERM_MAP.get(termId)
                +","+ PROC_MAP.get(procId)
                +","+kind
                +","+year+"/"+month+"/"+day
                +",残:"+remain+"円";
        return str;
    }

    public static final SparseArray<String> TERM_MAP = new SparseArray<String>();
    public static final SparseArray<String> PROC_MAP = new SparseArray<String>();
    static {
        TERM_MAP.put(3 , "精算機");
        TERM_MAP.put(4 , "携帯型端末");
        TERM_MAP.put(5 , "車載端末");
        TERM_MAP.put(7 , "券売機");
        TERM_MAP.put(8 , "券売機");
        TERM_MAP.put(9 , "入金機");
        TERM_MAP.put(18 , "券売機");
        TERM_MAP.put(20 , "券売機等");
        TERM_MAP.put(21 , "券売機等");
        TERM_MAP.put(22 , "改札機");
        TERM_MAP.put(23 , "簡易改札機");
        TERM_MAP.put(24 , "窓口端末");
        TERM_MAP.put(25 , "窓口端末");
        TERM_MAP.put(26 , "改札端末");
        TERM_MAP.put(27 , "携帯電話");
        TERM_MAP.put(28 , "乗継精算機");
        TERM_MAP.put(29 , "連絡改札機");
        TERM_MAP.put(31 , "簡易入金機");
        TERM_MAP.put(70 , "VIEW ALTTE");
        TERM_MAP.put(72 , "VIEW ALTTE");
        TERM_MAP.put(199 , "物販端末");
        TERM_MAP.put(200 , "自販機");

        PROC_MAP.put(1 , "運賃支払(改札出場)");
        PROC_MAP.put(2 , "チャージ");
        PROC_MAP.put(3 , "券購(磁気券購入)");
        PROC_MAP.put(4 , "精算");
        PROC_MAP.put(5 , "精算 (入場精算)");
        PROC_MAP.put(6 , "窓出 (改札窓口処理)");
        PROC_MAP.put(7 , "新規 (新規発行)");
        PROC_MAP.put(8 , "控除 (窓口控除)");
        PROC_MAP.put(13 , "バス (PiTaPa系)");
        PROC_MAP.put(15 , "バス (IruCa系)");
        PROC_MAP.put(17 , "再発 (再発行処理)");
        PROC_MAP.put(19 , "支払 (新幹線利用)");
        PROC_MAP.put(20 , "入A (入場時オートチャージ)");
        PROC_MAP.put(21 , "出A (出場時オートチャージ)");
        PROC_MAP.put(31 , "入金 (バスチャージ)");
        PROC_MAP.put(35 , "券購 (バス路面電車企画券購入)");
        PROC_MAP.put(70 , "物販");
        PROC_MAP.put(72 , "特典 (特典チャージ)");
        PROC_MAP.put(73 , "入金 (レジ入金)");
        PROC_MAP.put(74 , "物販取消");
        PROC_MAP.put(75 , "入物 (入場物販)");
        PROC_MAP.put(198 , "物現 (現金併用物販)");
        PROC_MAP.put(203 , "入物 (入場現金併用物販)");
        PROC_MAP.put(132 , "精算 (他社精算)");
        PROC_MAP.put(133 , "精算 (他社入場精算)");
    }
}

実行結果

  • 実行前に「無線とネットワーク」の設定で NFC を有効にする必要があります。
  • とりあえず過去10件分の履歴の表示に成功しました。

雑感

データの解析が面倒でしたが思ったより感単にアクセスできました。
白カードを購入すれば書き込みも出来るようなので簡易のカード鍵とかは簡単に作れそうです。

このサンプルのSVN:


2013/10/16

Annotation ProcessorのAPI移行ではまった

JDK7で自作APT使ったらAPIがJDK8で無くなるから使うなと怒られた。

こんなメッセージが...

  警告: aptツールとツールに関連付けられたAPIは、次回のJDKメジャー・リリースで削除される予定です。
  これらの機能はjavacおよび標準化された注釈処理APIのjavax.annotation.processingおよび
  javax.lang.modelによって置き換えられました。
  ユーザーはjavacの注釈処理機能に移行することをお薦めします。
  詳細は、javac manページを参照してください。

使っていたのは com.sun.mirror のパッケージで移行が必要なのは知っていたんだけど どうせパッケージ名の変更ぐらいだろと思って放置していた。

ところがいざ蓋を開けてみると API 全然変わっちゃってるやん。
Factoryとか有ったのに無いし、データ構造もアクセスメソッド名も変わってるし...

何とか移行の為のヒントのページを発見。

これ見ながら大改造して移行した。

まあ、自作のAPTなんて使ってる人ほとんどいないと思うけど移行はお早めに。


2013/10/13

AndroidのPreference使ってみた

Androidで設定画面を作ろうと思い Google 先生に聞いたとおり PreferenceActivity を使ったら deprecated だって起こられた (;_;)

てか、Android は deprecated 多すぎ。 ググって出てきたコードの半分ぐらい引っかかる気がする。 しかも、代替手段とかすぐに分かるようになって無いし。

そんな訳でさらにググって PreferenceFragment を使わないといけない事が分かった。

以下、最小限のテンプレ。

public class SimplePrefActivity extends Activity {
    private SimplePrefFragment fragment;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        fragment = new SimplePrefFragment();
        getFragmentManager().beginTransaction()
                .replace(android.R.id.content, fragment).commit();
    }

    public static class SimplePrefFragment extends PreferenceFragment
            implements OnSharedPreferenceChangeListener
    {
        @Override
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            addPreferencesFromResource(R.xml.my_pref); // => res/xml/my_pref.xml
        }

        @Override
        public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
            // 変更通知処理
        }
    }
}
  • リソースは従来と同じでAndroid XMLファイルから Preference を選択して専用エディタで作る。
  • PreferenceFragment は static な内部クラスにしないといけないそうです。

リソース書いてみて分かったのだが記述できる内容がかなりしょぼく Checkbox,List選択,TextField しか使えない。

Radio とかは Preferenceクラスを拡張すればできるようだけど せっかく XML化して設定画面の UI を統一してるのにカスタマイズしてよいの?

さらに List選択,TextField の現在の設定値がタップしないと分からないようなっている。
流石にこれは有り得ないと思うので現在値を表示させる汎用のカスタマイズをして以下のコードに落ち着いた。

public class PrefActivity extends Activity {
    private PrefFragment fragment;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        fragment = new PrefFragment();
        getFragmentManager().beginTransaction()
                .replace(android.R.id.content, fragment).commit();
    }
    public void setChanged(boolean b) {
        Intent intent = new Intent();
        // 設定変更があったことを呼び出し元に返す。
        setResult(b ? RESULT_OK : RESULT_CANCELED, intent);
    }

    public static class PrefFragment extends PreferenceFragment
            implements OnSharedPreferenceChangeListener
    {
        @Override
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            addPreferencesFromResource(R.xml.adkterm_pref); // res/xml/adkterm_pref.xml
        }

        @Override
        public void onResume() {
            super.onResume();
            resetSummary();
            getPreferenceScreen().getSharedPreferences()
                    .registerOnSharedPreferenceChangeListener(this);
        }

        @Override
        public void onPause() {
            super.onPause();
            getPreferenceScreen().getSharedPreferences()
                    .unregisterOnSharedPreferenceChangeListener(this);
        }

        @Override
        public void onSharedPreferenceChanged(SharedPreferences paramSharedPreferences, String paramString) {
            resetSummary();
            ((PrefActivity)getActivity()).setChanged(true);
        }

        // CheckBoxを除く項目のSummaryに現在値を設定する。
        public void resetSummary() {
            SharedPreferences sharedPrefs = getPreferenceManager().getSharedPreferences();
            PreferenceScreen screen = this.getPreferenceScreen();
            for (int i = 0; i < screen.getPreferenceCount(); i++) {
                Preference pref = screen.getPreference(i);
                if (pref instanceof CheckBoxPreference) continue;
                String key = pref.getKey();
                String val = sharedPrefs.getString(key, "");
                pref.setSummary(val);
            }
        }
    }
}

リソースXMLの例はこんな感じ。

<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android" >
    <ListPreference
        android:defaultValue="Landscape"
        android:entries="@array/direction_list"
        android:entryValues="@array/direction_list"
        android:key="orientation"
        android:title="Orientation" />
    <CheckBoxPreference
        android:defaultValue="true"
        android:key="keybord"
        android:selectable="true"
        android:title="Keybord" />
    <EditTextPreference
        android:defaultValue="16"
        android:dialogTitle="Font size (px)"
        android:key="fontsize"
        android:inputType="number"
        android:maxLength="2"
        android:title="Font size" />
    <EditTextPreference
        android:defaultValue="300"
        android:key="logsize"
        android:inputType="number"
        android:maxLength="3"
        android:title="Log size" android:dialogTitle="Log size (lines)"/>
</PreferenceScreen>

実行するとこうなります。(下段に小さく表示されるのが現在値)

設定参照側コードの例はこんな感じです。

public class Config  {
    public final static String K_KEYBORD = "keybord";
    public final static String K_ORIENT = "orientation";
    public final static String K_FONTSIZE = "fontsize";
    public final static String K_LOGSIZE = "logsize";
    public final static String V_LANDSCAPE = "Landscape";
    public final static String V_PORTRAIT = "Portrait";

    private static SharedPreferences sharedPrefs;

    public static void init(Context context) {
        sharedPrefs = PreferenceManager.getDefaultSharedPreferences(context);
    }

    public static Boolean getKeybord() {
        return sharedPrefs.getBoolean(K_KEYBORD, true);
    }
    public static String getOrientation() {
        return sharedPrefs.getString(K_ORIENT, V_LANDSCAPE);
    }
    public static int getFontsize() {
        return Integer.parseInt(sharedPrefs.getString(K_FONTSIZE, "16"));
    }
    public static int getLogsize() {
        return Integer.parseInt(sharedPrefs.getString(K_LOGSIZE, "300"));
    }
}
  • getする型を間違えると ClassCastException が起こります。
    • CheckBox のキーに getString() とかすると。
  • どのキーがどの型を返すのかは Preference のクラスをチェックするしかありません。
    • 上記の PrefFragment.resetSummary()参照

所感

設定内容をストレージに自動保存してくれたりするのはよいけど全体的に詰めが甘い感じ。
せめてラジオボタンとスライダーくらい標準で欲しい。

キー文字列の定義や設定値の参照クラスとかもXMLから自動生成して欲しい気がする。


2013/10/10

WindowsUpdateしたらNexus7認識しなくなった

今日の Windows update の直後から Windows7 が Nexus7 を認識しなくなった。
ドライバは認識しているのだがエクスプローラとか Eclipse とかのアプリが反応しない。

俺の環境だけ?

とりあえず Linux で開発はできるから困らないけど、修正 update 来ないと面倒くさいなぁ。

P.S:対処方判明しました。

  • 設定から「ストレージ」で右上のメニューを開いて「USBでパソコンに接続」で「カメラ(PTP)」を選択

これで再接続すると認識されました。 元々Win8で発生していた問題のようですがupdateでWin7にもバグが更新されてしまったようです。

ファイル転送が画像しかできなくなりますが仕方ないですね。


2013/10/06

AndroidとPCのUSB通信のサンプル

Android で USB 通信を行う為の API が有ったので試してみた。

USB の API は ADK(Accessory Development Kit)と呼ばれている物。

Android がホストになる場合とUSB機器になる場合があるが今回試したのは後者の方で Android が PC の USBデバイスとして認識されるケース。

Android側

API の選択

ADK は android.hardware.usb と com.android.future.usb の2つがある。 この2つは基本的に同じなのだけど後者は Android/2.3.4 向けの互換用。

違いは基本インスタンスの取得方法だけ。

android.hardware.usb:

UsbManager manager = (UsbManager) getSystemService(Context.USB_SERVICE);
UsbAccessory accessory = (UsbAccessory) intent.getParcelableExtra(UsbManager.EXTRA_ACCESSORY);

com.android.future.usb:

UsbManager manager = UsbManager.getInstance(this);
UsbAccessory accessory = UsbManager.getAccessory(intent);

今回は android.hardware.usb を使用する。

AndroidManufest.xml の設定

USB 関連の設定を追加して置く。

AndroidManufest.xml:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="org.kotemaru.android.usbsample" android:versionCode="1"
    android:versionName="1.0">

    <uses-sdk android:minSdkVersion="12" android:targetSdkVersion="17" />

    <application android:allowBackup="true" android:icon="@drawable/ic_launcher"
        android:label="@string/app_name" android:theme="@style/AppTheme">

        <uses-feature android:name="android.hardware.usb.accessory" />

        <activity android:name="org.kotemaru.android.usbsample.MainActivity"
            android:label="@string/app_name">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />

                <intent-filter>
                    <action android:name="android.hardware.usb.action.USB_ACCESSORY_ATTACHED" />
                </intent-filter>
                <meta-data android:name="android.hardware.usb.action.USB_ACCESSORY_ATTACHED"
                    android:resource="@xml/accessory_filter" />
            </intent-filter>
        </activity>
    </application>

</manifest>

res/xml/accessory_filter.xml:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <usb-accessory
        manufacturer="kotemaru.org"
        model="AdkSample"
        version="1.0" />
</resources>

ソース

MainAcrivity.java:

  • UsbReceiverとUsbDriverを呼んでいる所以外は普通のActivity。
package org.kotemaru.android.usbsample;

import java.io.IOException;

import android.os.Bundle;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.Intent;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.EditText;

public class MainActivity extends Activity {

    private static final String ACTION_USB_PERMISSION = "org.kotemaru.android.usbsample.USB_PERMISSION";

    private UsbDriver usbDriver;
    private UsbReceiver usbReceiver;
    private EditText editText1;
    private EditText editText2;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
        usbDriver = new UsbDriver(this);
        usbReceiver = UsbReceiver.init(this, ACTION_USB_PERMISSION, usbDriver);
        
        editText1 = (EditText) findViewById(R.id.editText1);
        editText2 = (EditText) findViewById(R.id.editText2);

        Button sendBtn = (Button) findViewById(R.id.sendBtn);
        sendBtn.setOnClickListener(new OnClickListener(){
            @Override public void onClick(View v) {
                String msg = editText1.getText().toString();
                try {
                    usbDriver.send(msg);
                    String rmsg = usbDriver.receive();
                    editText2.setText(rmsg);
                } catch (IOException e) {
                    errorDialog(e.getMessage());
                }
            }
        });
        Button resetBtn = (Button) findViewById(R.id.resetBtn);
        resetBtn.setOnClickListener(new OnClickListener(){
            @Override public void onClick(View v) {
                restart();
            }
        });
    }

    @Override
    public void onResume() {
        super.onResume();
        usbReceiver.resume();
    }

    @Override
    public void onPause() {
        super.onPause();
        usbReceiver.close();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        usbReceiver.destroy();
    }

    protected void restart() {
        Intent intent = this.getIntent();
        this.finish();
        this.startActivity(intent);
    }
    public void errorDialog(String message) {
        AlertDialog.Builder dialog = new AlertDialog.Builder(this);
        dialog.setTitle("Error!");
        dialog.setMessage(message);
        dialog.show();
    }
}

UsbReceiver.java:

  • USB の ATTACH/DETTACH イベントを受け取る為の Receiver。
  • pause/resume の処理が有るため若干ややこしくなっている.
package org.kotemaru.android.usbsample;

import android.app.Activity;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.hardware.usb.UsbAccessory;
import android.hardware.usb.UsbManager;
import android.util.Log;

public class UsbReceiver extends BroadcastReceiver {
    private static final String TAG = "UsbReceiver";

    private Activity activity;
    private Driver driver;
    private final String action_usb_permission;

    private UsbManager usbManager;
    private UsbAccessory activeAccessory;

    private PendingIntent permissionIntent;
    private boolean permissionRequestPending = false;

    // I/O処理を分離する為のインターフェース。
    public interface Driver {
        public void openAccessory(UsbAccessory accessory);
        public void closeAccessory(UsbAccessory accessory);
    }

    public static UsbReceiver init(Activity activity, String permissionName, Driver driver) {
        UsbReceiver receiver = new UsbReceiver(activity, permissionName, driver);

        /* receiver */
        IntentFilter filter = new IntentFilter();
        filter.addAction(permissionName);
        filter.addAction(UsbManager.ACTION_USB_ACCESSORY_DETACHED);
        activity.registerReceiver(receiver, filter);

        return receiver;
    }

    public UsbReceiver(Activity activity, String permissionName, Driver driver) {
        super();
        this.activity = activity;
        this.action_usb_permission = permissionName;
        this.driver = driver;

        this.usbManager = (UsbManager) activity.getSystemService(Context.USB_SERVICE);
        this.permissionIntent =
                PendingIntent.getBroadcast(activity, 0, new Intent(permissionName), 0);
    }

    @Override
    public void onReceive(Context context, Intent intent) {
        String action = intent.getAction();
        if (action_usb_permission.equals(action)) {
            open(intent);
        } else if (UsbManager.ACTION_USB_ACCESSORY_DETACHED.equals(action)) {
            close(intent);
        }
    }

    private synchronized void open(Intent intent) {
        UsbAccessory accessory = (UsbAccessory) intent.getParcelableExtra(UsbManager.EXTRA_ACCESSORY);
        if (intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)) {
            driver.openAccessory(accessory);
            activeAccessory = accessory;
        } else {
            Log.d(TAG, "permission denied for accessory " + accessory);
        }
        permissionRequestPending = false;
    }

    private synchronized void close(Intent intent) {
        UsbAccessory accessory = (UsbAccessory) intent.getParcelableExtra(UsbManager.EXTRA_ACCESSORY);
        if (accessory != null && accessory.equals(activeAccessory)) {
            close();
        }
    }

    public void resume() {
        UsbAccessory[] accessories = usbManager.getAccessoryList();
        UsbAccessory accessory = (accessories == null ? null : accessories[0]);
        if (accessory != null) {
            if (usbManager.hasPermission(accessory)) {
                driver.openAccessory(accessory);
            } else {
                synchronized (this) {
                    if (!permissionRequestPending) {
                        usbManager.requestPermission(accessory, permissionIntent);
                        permissionRequestPending = true;
                    }
                }
            }
        } else {
            Log.d(TAG, "accessory is null");
        }
    }

    public synchronized void close() {
        if (activeAccessory != null) {
            driver.closeAccessory(activeAccessory);
            activeAccessory = null;
        }
    }
    public void destroy() {
        activity.unregisterReceiver(this);
    }
}

UsbDriver.java:

  • USBへのI/O処理。
  • USB接続はFileDescriptor扱いなのでほぼ普通の java.io と変わらない。
package org.kotemaru.android.usbsample;

import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

import android.content.Context;
import android.hardware.usb.UsbAccessory;
import android.hardware.usb.UsbManager;
import android.os.ParcelFileDescriptor;
import android.util.Log;

public class UsbDriver implements UsbReceiver.Driver {
    private static final String TAG = "UsbDriver";

    private UsbManager usbManager;

    private ParcelFileDescriptor fileDescriptor;
    private FileInputStream usbIn;
    private FileOutputStream usbOut;

    public UsbDriver(MainActivity activity) {
        this.usbManager = (UsbManager) activity.getSystemService(Context.USB_SERVICE);
    }

    @Override
    public void openAccessory(UsbAccessory accessory) {
        fileDescriptor = usbManager.openAccessory(accessory);

        if (fileDescriptor != null) {
            FileDescriptor fd = fileDescriptor.getFileDescriptor();
            usbIn = new FileInputStream(fd);
            usbOut = new FileOutputStream(fd);
        } else {
            Log.d(TAG, "accessory open fail");
        }
    }

    @Override
    public void closeAccessory(UsbAccessory accessory) {
        try {
            if (fileDescriptor != null) {
                fileDescriptor.close();
            }
        } catch (IOException e) {
            // ignore.
        } finally {
            fileDescriptor = null;
        }
    }

    public void send(String msg) throws IOException {
        usbOut.write(msg.getBytes("UTF-8"));
        usbOut.flush();
    }
    public String receive() throws IOException {
        byte[] buff = new byte[1024];
        int len = usbIn.read(buff);
        return new String(buff,0,len,"UTF-8");
    }
}

PC 側

PC 側は Ubuntu,FreeBSD,RaspberryPi で接続を確認している。 libusb を使用するので Windows でも繋がるはず。

libusb の準備

linux系の場合は libusb-1.0 をインストールする。FreeBSD はOS組込なので不要。

$ sudo apt-get install libusb-1.0

libusb で ADK のプロトコルを実装したライブラリ AOA をこちらからお借りしました。 (若干修正が入っています。)

ソース

受け取った文字列を大文字に変換して送り返すだけです。 言語は C++ です。

AdkEcho.cpp:

/**
Android USB connection sample.
@Author kotemru.org
@Licence apache/2.0
*/

#include <stdlib.h>
#include <fcntl.h>
#include <errno.h>
#include <unistd.h>
#include <stdio.h>
#include <signal.h>
#include <string.h>

#include "AOA/AOA.h"

// USB Connector opend.
AOA acc("kotemaru.org",
        "AdkSample",
        "Sample for ADK",
        "1.0",
        "http://blog.kotemaru.org/androidUSBSample",
        "000000000000001") ;

/**
 * Disconnect USB innterrupt aborted.
 */
void signal_callback_handler(int signum)
{
    fprintf(stderr, "\ninterrupt %d\n",signum);
    acc.disconnect();
    exit(0);
}

static void error(char *msg, int rc) {
    fprintf(stderr,"Error(%d,%s): %s\n",rc,strerror(errno),msg);
    acc.disconnect();
    exit(0);
}

int main(int argc, char *argv[])
{
    signal(SIGINT, signal_callback_handler);
    signal(SIGTERM, signal_callback_handler);

    unsigned char buff[1024];

    acc.connect(100);
    // Echo back.
    while (1) {
        int len = acc.read(buff, sizeof(buff), 1000000);
        if (len < 0) error("acc.read",len);
        buff[len+1] = '\0';
        printf("USB>%s\n", buff);
        for (int i=0; i<len; i++) buff[i] = buff[i] - 0x20;
        acc.write(buff, len, 1000);
    }
}

実行結果

先に Android 側アプリを起動して置きます。

USBをPCに接続してから PC 側のプログラムを起動します。 (※root権限が必要です。)

$ sudo ./AdkEcho
VID:18D1, PID:2D01 Class:00
already in accessory mode.
bNumInterfaces: 2
bNumEndpoints: 2
 bEndpointAddress: 81, bmAttributes:02
 bEndpointAddress: 02, bmAttributes:02
VID:18D1, PID:2D01

Android 側にダイアログが出るので応答します。

接続したら英小文字の文書を入力して「Send」をタップします

英大文字が帰ってきたら成功です。

所感

正直、ネットワークで無くUSBでPCと繋げたいケースと言うのが余り思い付かない。

今のところ、RaspberryPiのTTY端末くらい。

何か面白いアイデアが有ったら教えて下さい。

ソースのSVNは以下に有ります。

  • https://kotemaru.googlecode.com/svn/trunk/androidUSBSample

2013/09/23

AndroidのソフトキーボードはonKey()を呼ばない。

Android で端末アプリを作ろうとしてえらいはまった。

キーボード入力を普通に OnKeyListener.onKey() で拾うプログラムを書いてエミュレータで動作確認後に 実機で動かしてみたら全然動かない。
キーを押しても何も反応しないのだが何故かリターンキーは拾える。

理由がさっぱり分からず数時間を費して OnKeyListener クラスの説明文に気が付いた。

View.OnKeyListener
Class Overview

Interface definition for a callback to be invoked when a hardware key event is dispatched to this view. The callback will be invoked before the key event is given to the view. This is only useful for hardware keyboards; a software input method has no obligation to trigger this listener.

orz...
英語読めないってだめね、完全にスルーしてたよ。
なんでこんな仕様なのかは後で分かったが...

しかし、個別のキー入力が拾えないと端末アプリにならないので対策を調べてみた。
結論から言うとソフトキーボードを自前で実装すればOKよ、と言うことらしい。

ソフトキーボードなんてどうやって実装するかと言うとSDKにちゃんとサンプルが用意されてた。

  • android-sdk/samples/android-17/SofKeyboard

具体的にはXMLでキーボードの配列を書いて Keybord クラスにリスースIDを渡してわれば終り。
以外に簡単だった。

KeyboardView keyboardView = new KeyboardView(context, null);
keyboard = new Keyboard(context, R.xml.keymap); // -> res/xml/keymap.xml
keyboardView.setKeyboard(keyboard);
// keyboardViewを適当に配置。

せっかくなので自分好みのキーマップを作ってみた。(US配列風)

あ、ESC忘れた...

これ作って気が付いたのだが Ctrl キーとかは標準のソフトキーボードではサポートされないので 端末アプリで使う場合には結局自前で実装せざる得なかった訳だ。 理屈は分かったけどもう少し分かり易いようにしておいて欲しいよ。


2013/09/14

Androidの非同期処理を簡単にする実験

Androidでは通信等の時間のかかる処理はUIスレッドでは無く別スレッドで行えと言われる。
しかし、通信結果を別スレッドから画面に反映すると UIスレッドで実行しろと怒られる。

どないせいっちゅーねん! (ノ`Д)ノ彡 ┻━┻∴

対策として AsyncTask が用意されているが使い方は結構めんどくさい。

なんとか非同期処理を簡単にする方法は無いかと思いアノテーションと AsyncTask を組み合わせる方法で試してみた。

テスト実装してみたアプリは入力されたURLをWebから取得してテキスト表示するだけの単純な物。

こんな感じ。

ソースコード

MainActivity.java:
  • Activity はいたって普通。

package org.kotemaru.android.logicasync.sample;

import android.os.Bundle;
import android.app.Activity;
import android.app.AlertDialog;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;

public class MainActivity extends Activity implements UIAction {

    private MyLogic logic = new MyLogic(this);

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        final EditText textUrl = (EditText)findViewById(R.id.text_url);
        Button btnGo = (Button)findViewById(R.id.btn_go);
        btnGo.setOnClickListener(new OnClickListener() {
            @Override   public void onClick(View btn) {
                String url = textUrl.getText().toString();
                logic.async.doGetHtml(url);  // <=Webへ通信を非同期実行
            }
        });
    }
    @Override
    protected void onDestroy() {
        super.onDestroy();
        logic.async.close();
    }

    @Override
    public void updateView(String html) {
        TextView  textHtml = (TextView)findViewById(R.id.text_html);
        textHtml.setText(html);
    }

    @Override
    public void errorDialog(String message) {
        AlertDialog.Builder dialog = new AlertDialog.Builder(this);
        dialog.setTitle("Error!");
        dialog.setMessage(message);
        dialog.show();
    }
}


UIAction.java:
  • ロジックとActivityを明確に分離したかったのでインターフェース化。

package org.kotemaru.android.logicasync.sample;
public interface UIAction {
    void updateView(String html);
    void errorDialog(String message);
}


MyLogic.java:
  • アノテーションを使うロジック部分。
  • @Task() の指定されたメソッドが非同期実行用。
    • MyLogicAsync.java にスタブが自動生成される。
  • @Task("UI") は UIスレッドで実行される事を意味する。

package org.kotemaru.android.logicasync.sample;

import java.io.Serializable;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.BasicResponseHandler;
import org.apache.http.impl.client.DefaultHttpClient;
import org.kotemaru.android.logicasync.annotation.Logic;
import org.kotemaru.android.logicasync.annotation.Task;

@Logic
public class MyLogic implements Serializable {
    private static final long serialVersionUID = 1L;

    public MyLogicAsync async = new MyLogicAsync(this);
    private UIAction uiAction;

    public MyLogic(UIAction uiAction) {
        this.uiAction = uiAction;
    }

    @Task
    public void doGetHtml(String url) {
        // HTTPリクエストを行う処理。
        DefaultHttpClient httpClient = new DefaultHttpClient();
        HttpGet request = new HttpGet(url);
        try {
            String html = httpClient.execute(request, new BasicResponseHandler());
            async.doGetHtmlFinish(html); // <= 結果反映の非同期実行。このメソッド終了後実行される。
        } catch (Exception e) {
            async.doGetHtmlError(e); // <= エラー表示の非同期実行。このメソッド終了後実行される。
        } finally {
            httpClient.getConnectionManager().shutdown();
        }
    }

    @Task("UI")
    public void doGetHtmlFinish(String html) {
        // 通信結果反映処理。
        uiAction.updateView(html);
    }

    @Task("UI")
    public void doGetHtmlError(Exception e) {
        uiAction.errorDialog(e.getMessage());
    }
}


MyLogicAsync.java:
  • アノテーション プロセッサによって自動生成されたソース。
  • MyLogicクラスのメソッドを非同期実行する。

//  Generated stub.
package org.kotemaru.android.logicasync.sample;
import org.kotemaru.android.logicasync.TaskThread;
import org.kotemaru.android.logicasync.Task;

import android.util.Log;

public class MyLogicAsync implements java.io.Serializable
{
    private static final long serialVersionUID = 1L;
    private static final String TAG = "LogicAsync";

    private final TaskThread thread = new TaskThread();
    private final MyLogic origin;

    public MyLogicAsync( MyLogic origin ) {
        this.origin = origin;
    }
    public final void close() {
        thread.stop();
    }

    public void doGetHtml(final java.lang.String url) {
        Task task = new Task(){
            private static final long serialVersionUID = 1L;
            @Override public void run() {
                origin.doGetHtml(url);
            }
        };
        thread.addTask(task);
    }

    public void doGetHtmlError(final java.lang.Exception e) {
        Task task = new Task(){
            private static final long serialVersionUID = 1L;
            @Override public void run() {
                origin.doGetHtmlError(e);
            }
        };
        task.setThreadType(Task.UI);
        thread.addTask(task);
    }

    public void doGetHtmlFinish(final java.lang.String html) {
        Task task = new Task(){
            private static final long serialVersionUID = 1L;
            @Override public void run() {
                origin.doGetHtmlFinish(html);
            }
        };
        task.setThreadType(Task.UI);
        thread.addTask(task);
    }
}

まとめ

かなりすっきり記述できている気がする。

メリット:

  • Javaの言語仕様から逸脱していないので eclipse の「宣言を開く」等でソースが追える。
  • ロジックとビューの分離がしやすい。

デメリット:

  • アノテーション プロセッサの設定がちょっとめんどくさい。

あと、リトライ処理ぐらい有れば充分、軽量フレームワークとして使えそう。

ダウンロード

eclipseのプロジェクトです。

サンプルのコンパイル時に NullPointerException となるときは eclipse を再起動して下さい。


2013/09/08

Nexus7を外部モニタ化 (3)

前の記事で 野良ビルドがインストールできなかった件は単純に署名してなかったから。 オレオレ証明で署名したら実機にインストールできたよ。

野良ビルドって無署名の事だと思い込んでた。orz

野良ビルドのAPKはこちらに置いておきます。

当然ながら無保証、無責任です。各位、自己責任でお使い下さい。

Nexus7でしか確認してませんが、4.1以降のOSなら動作すると思います。


プロフィール
20年勤めた会社がリーマンショックで消滅、紆余曲折を経て現在はフリーランスのSE。 失業をきっかけにこのブログを始める。

サイト内検索

登録
RSS/2.0

カテゴリ

最近の投稿

リンク

アーカイブ