2014/03/01

USB赤外線リモコン アプリ

Nexus7からUSB赤外線リモコンを操る(前編)「(中編)」 「(後編)」 を元に簡単なリモコンアプリを作りました。

使い方

デバイスの準備

専用のデバイスが必要です。
Nexus7からUSB赤外線リモコンを操る(前編)」 を参照してください。

起動

アプリをインストールした状態でデバイスを接続するとダイアログが表示されるので「OK」をタップするとアプリが自動的に起動します。

赤外線の登録

アプリが起動したらメニューの「登録モード on/off」を選択して登録モードにします。

登録モードはボタンの枠が水色になります。
この状態で登録したいボタンをタップします。

ダイアログが出るのでデバイスに向けてリモコンを操作してください。

正常に登録できるとダイアログが消えるので続けて他のボタンを登録します。

赤外線の送信

もう一度、メニューの「登録モード on/off」を選択して登録モードを解除し各ボタンをタップすれば登録した赤外線が送信されます。

リモコンの選択

横方向にスワイプするとリモコンを選択できます。

登録データの保存と復元

メニューの「登録データ保存」/「登録データ復元」を選択すると登録データの保存と復元が行えます。

現在のところ保存先は Android/data/org.kotemaru.android.irrc/IrData.db に固定です。
ファイル形式は Sqlite です。

リモコン画面のカスタマイズ

現状ではリモコン画面のカスタマイズは Android の開発環境が無いとできません。
SVN からプロジェクトを落として Eclipse で開いてください。

リモコン画面は HTML で記述されています。 テンプレートとなる assets/remocon/1.TV.html を同じフォルダに別名をつけてコピーしてください。 自動的に新しいリモコンとして追加されます。

HTMLに id 属性の付いた <button> を配置すればそのまま登録可能なリモコンのボタンになります。 <button> の id 属性は DB 上のキーとなるので HTMLページ内で一意でなければなりません。

<title> タグはリモコン選択時にアプリタイトルとして表示されるので適当な物を指定して下さい。

アイコンについて

ボタンのアイコンは こちら からお借りしました。 300種類くらい有るので適当な物が見つかると思います。

雑感

本当は、リピート機能やリモコン画面の登録機能も付けたかったのですが需要が不明なのでここまでとしました。 このデバイスを使っている人は基本開発者だと思うで後は好きにしてください。(^^;

Android アプリとしては WebView の JavaScript から USBデバイスの制御をしているので中々面白いものになっていると思います。


2014/02/23

Nexus7からUSB赤外線リモコンを操る(後編)

Nexus7からUSB赤外線リモコンを操る(前編)「(中編)」の続きです。 先に前/中編をみてください。

赤外線リモコンキットのプロトコル

フォーラムにもなぜかプロトコルについての資料がありません。 ファームウェアのソース公開されているので自分で調べろ(or決めろ)って事でしょうか。

仕方ないのでこちらのサイトを参考にファームのソースからプロトコルを調べました。

基本形

パケットは64バイトの固定長です。

要求パケットの1バイト目にコマンドのコードが入り、 応答パケットの1バイト目に同じコマンドのコードが入って戻ってきます。

パケットの2バイト目以降が要求パラメータまたは応答の戻り値となっています。

応答にエラーコードと言うものは無いようなので Timeout で検出と思われます。

家電のリモコンから赤外線データの受信

リモコンから赤外線データを受信するにはデバイスを受信モードにしデータを取得してから受信モードを終了します。

操作パケットデータ(64byte固定)
受信モード開始
(1)  要求->0x53,0x01,0xff…0xff
(2)  応答<-0x53,0x00,0x00…0x00
データ取得(繰り返す)
(3)  要求->0x52,0xff,0xff…0xff
(4)  応答<-0x52,0xXX,0xXX…0xXX
受信モード終了
(5)  要求->0x53,0x00,0xff…0xff
(6)  応答<-0x53,0x00,0x00…0x00

リモコンからまだ赤外線データを受け取っていない場合は (4) の応答の2バイト目が 0x00 となる為、データが取得できるまで (3),(4) を繰り返します。

デバイスから赤外線データの送信

取得したデータを送信します。

(4) で受け取ったデータの1バイト目を送信コマンド(0x61)に差し替えて要求するだけです。
投げっぱなしで応答は有りません。

操作パケットデータ(64byte固定)
データ送信
(7)  要求->0x61,0xXX,0xXX…0xXX

プロトコルの実装

家電のリモコンから赤外線データの受信

応答が非同期となるのでリスナインターフェースを用意します。 後は、パケットを作って非同期タスクに投げるだけです。

public interface IrrcResponseListener {
    public void onIrrcResponse(byte[] data);
}

public void startReceiveIR(IrrcResponseListener listener) {
    byte[] buff = initBuffer(new byte[PACKET_SIZE], (byte) 0xff);
    buff[0] = RECEIVE_IR_MODE_CMD;
    buff[1] = 1;
    new RequestAsyncTask(listener).request(buff, true, false);
}

public void endReceiveIR(IrrcResponseListener listener) {
    byte[] buff = initBuffer(new byte[PACKET_SIZE], (byte) 0xff);
    buff[0] = RECEIVE_IR_MODE_CMD;
    buff[1] = 0;
    new RequestAsyncTask(listener).request(buff, true, false);
}

public void getReveiveIRData(IrrcResponseListener listener) {
    byte[] buff = initBuffer(new byte[PACKET_SIZE], (byte) 0xff);
    buff[0] = RECEIVE_IR_DATA_CMD;
    new RequestAsyncTask(listener).request(buff, true, true);
}

デバイスから赤外線データの送信

パケットを作って非同期タスクに投げるだけです。

public void sendData(byte[] buff) {
    buff[0] = SEND_IR_CMD;
    new RequestAsyncTask(null).request(buff, false, false);
}

非同期タスク

応答有無、リトライ有無の指定にしたがってプロトコルにそった送受信を行っているだけです。
異常系やキャンセル処理への考慮は不十分です。
#APIが混乱しているのは仕様ですw

private class RequestAsyncTask extends AsyncTask<byte[], Void, byte[]> {
    private IrrcResponseListener listener;
    private boolean withResponse = false;
    private boolean withRetry = false;

    public RequestAsyncTask(IrrcResponseListener listener) {
        this.listener = listener;
    }

    public void request(byte[] buff, boolean withResponse, boolean withRetry) {
        this.withResponse = withResponse;
        this.withRetry = withRetry;
        execute(buff);
    }

    @Override
    protected byte[] doInBackground(byte[]... args) {
        Log.d(TAG, "RequestAsyncTask start");
        try {
            byte[] reqData = args[0];
            byte[] resData = null;
            boolean isRetry = false;
            do {
                doRequest(reqData);
                if (withResponse) {
                    resData = doResponse();
                    if (resData[0] != reqData[0]) {
                        Log.e(TAG, "Bad resposne code " + resData[0]);
                        return null;
                    }
                    if (withRetry && resData[1] == 0x00) {
                        sleep(500);
                        isRetry = true;
                    } else {
                        isRetry = false;
                    }
                }
            } while (isRetry);
            return resData;
        } catch (Throwable t) {
            Log.e(TAG, t.getMessage(), t);
            return null;
        }
    }

    @Override
    protected void onPostExecute(byte[] result) {
        if (listener != null) {
            listener.onIrrcResponse(result);
        }
    }
    private void doRequest(byte[] buff) throws IOException {
        …省略(中編参照)
    }
    private byte[] doResponse() throws IOException {
        …省略(中編参照)
    }
}

動かしてみる

受信と送信のボタン2つだけの Activity を作って動かしてみました。

Nexus7に繋げるのですがここで一つ問題が。
赤外線リモコンキットのコネクタは Mini-USB なので micro-USB と直結しようとするとレアなケーブルが必要で手持ちに有りませんでした。

結果こんな事に(笑)

それはそれとして、
アプリの受信ボタンをタップしてからデバイスの受光部分に向けてリモコンを操作します。
電源ボタンを押してみました。

デバイスを家電機器に向けてアプリの送信ボタンをタップすると無事、家電の電源が入って実験成功です。

 

まとめ

これで Nexus7 から赤外線リモコンキットを操作することが可能になりました。

基本的に Android から USBデバイスを操作するのは同じ流れで行けると思うのですが やはり OSが一部デバイスをアプリに使わせてくれないのは致命的な問題のような気がします。 普通のUSBデバイスはファームの書き換えなんてさせてくれませんから。

追々、リモコンアプリを作って行きたいのですが ボタン配置のカスマイズをできるようにしないといけないので以外に難しそうです。

ソース全体は以下のSVNを参照して下さい。


2014/02/22

Nexus7からUSB赤外線リモコンを操る(中編)

Nexus7からUSB赤外線リモコンを操る(前編)」の続きです。先に前編をみてください。

以前 USB 接続をやった時は PC がホストで Android がデバイスだったけれど今回は Android がホストになるのでやること多いです。

基本的なやり方はいつもの勝手に翻訳さんのサイトを参照しました。

但し、割と内容が薄く書いて無い注意事項が結構あります。

マニフェスト

マニフェストはドキュメント通りです。

AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="org.kotemaru.android.irrc" android:versionCode="1" android:versionName="1.0" >
    <uses-sdk android:minSdkVersion="12" />
    <permission android:name="android.hardware.usb.host" ></permission>
    <uses-feature android:name="android.hardware.usb.host" android:required="true" />

    <application
        android:allowBackup="true" android:icon="@drawable/ic_launcher" android:label="@string/app_name" android:theme="@style/AppTheme" >
        <activity
            android:name="org.kotemaru.android.irrc.MainActivity"
            android:configChanges="orientation|screenSize"
            android:label="@string/app_name" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
            <intent-filter>
                <action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" />
            </intent-filter>
            <meta-data
                android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED"
                android:resource="@xml/device_filter" />
        </activity>
    </application>
</manifest>

USB_DEVICE_ATTACHED で Activity を起動する設定にすると 既に Activity が起動していても USB_DEVICE_ATTACHED で Activity が onCreate() から再起動されます。

LAUNCHER からも起動できるようにして有るとちょっとややこしい感じになります。

res/xml/device_filter.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <usb-device vendor-id="8938" product-id="30" />
</resources>

デバイスを特定する情報を記述します。 注意事項は値の記法が10進数なことです。 最初、16進数で書いてはまりました。

USBデバイスの構造

USBデバイスの構造は少し複雑なので整理します。
1つのUSBデバイスは複数のインターフェース(機能)を持ちます。通常は1つです。
各インターフェースは入出力の Endpoint を複数持ちます。通常は1つか2つです。

赤外線リモコンキットは4つのインタフェースを持ち以下のような構造になっていました。
但し、データ通信意外は前編のファームウェアによりダミーになっています。

  • UsbDevice : 赤外線リモコンキット
    • UsbInterface[0] : キーボード
      • UsbEndpoint[0] : IN(INTERRAPT)
      • UsbEndpoint[1] : OUT(INTERRAPT)
    • UsbInterface[1] : マウス
      • UsbEndpoint[0] : IN(INTERRAPT)
    • UsbInterface[2] : ボリュームコントローラ
      • UsbEndpoint[0] : IN(INTERRAPT)
    • UsbInterface[3] : データ通信
      • UsbEndpoint[0] : IN(INTERRAPT)
      • UsbEndpoint[1] : OUT(INTERRAPT)

UsbEndpoint のタイプは CONTROL,ISOC,BULK,INTERRAPT の4種類あります。 通常アプリが使用するのは BLUK か INTERRAPT で赤外線リモコンキットは IN/OUT ともに INTERRAPT(非同期) で通信します。

詳細は以下のサイトが詳しいです。

デバイスの認識

起動直後の処理

public static IrrcUsbDriver init(MainActivity activity, String permissionName) {
    IrrcUsbDriver driver = new IrrcUsbDriver(activity, permissionName);
    // USB_DEVICE_ATTACHEDから起動された場合は intent がデバイスを持っている。
    UsbDevice device = activity.getIntent().getParcelableExtra(UsbManager.EXTRA_DEVICE);
    if (device == null) {
        // LAUNCHER からの起動の場合は接続済デバイス一覧から検索する。
        device = findDevice(driver.usbManager, VENDER_ID, PRODUCT_ID);
    }
    /*
     * USB_DEVICE_ATTACHED で起動するように AndroidManifest.xml を記述すると
     * USB_DEVICE_ATTACHED で必ず onCreate() が呼ばれるので Activity から設定した Receiver は呼ばれない。
     * 従って、ここで onAttach() を呼ぶ。
     */
    driver.onAttach(device);
    return driver;
}

USB_DEVICE_ATTACHED から Activity が起動された場合は intent がデバイスを持っているでそのままデバイス認識の処理に入ります。

LAUNCHER から起動された場合は自前でデバイス一覧から検索します。 それでも見つからない場合の処理はアプリしだいです。

デバイス認識の処理

public String onAttach(UsbDevice device) {
    Log.d(TAG, "onAttach:" + device);
    usbDevice = device;
    if (usbDevice == null) {
        Log.e(TAG, "Not found USB Device.");
        return "Not found USB Device.";
    }
    if (usbManager.hasPermission(usbDevice)) {
        return onStart(usbDevice);
    } else {
        // デバイスの利用許可をユーザに求める。
        // 結果は UsbReceiver.onReceive()にコールバック。
        usbManager.requestPermission(usbDevice, permissionIntent);
    }
    return null;
}

アプリがUSBデバイスを使用するにはユーザの許可が要ります。 許可が無い場合はユーザに許可を求めるリクエストを投げて Receiver で受け取ります。

許可があればデバイスとの接続を開始します。

デバイス接続の処理

public String onStart(UsbDevice device) {
    Log.d(TAG, "onStart:" + device);
    if (! device.equals(usbDevice)) {
        return "No device attach.";
    }
    if (! usbManager.hasPermission(usbDevice)) {
        return "No device permission.";
    }

    usbConnection = usbManager.openDevice(usbDevice);
    // TODO:インターフェースの検出は端折ってます。
    UsbInterface usbIf = usbDevice.getInterface(INTERFACE_INDEX);

    // EndPointの検索。分かってる場合は直接取り出しても良い。
    for (int i = 0; i < usbIf.getEndpointCount(); i++) {
        UsbEndpoint ep = usbIf.getEndpoint(i);
        Log.d(TAG, "tye=" + ep.getType());
        if (ep.getType() == UsbConstants.USB_ENDPOINT_XFER_INT) {
            if (ep.getDirection() == UsbConstants.USB_DIR_IN) {
                endpointIn = ep;
            } else if (ep.getDirection() == UsbConstants.USB_DIR_OUT) {
                endpointOut = ep;
            }
        }
    }
    if (endpointIn == null || endpointOut == null) {
        Log.e(TAG, "Device has not IN/OUT Endpoint.");
        return "Device has not IN/OUT Endpoint.";
    }
    // デバイスの確保
    usbConnection.claimInterface(usbIf, true);
    isReady = true;
    return null;
}

この辺りはドキュメント通りです。
Endpoint のタイプは分かっていますがあえてチェックしています。

デバイスの終了処理

public String onDetach(UsbDevice device) {
    Log.d(TAG, "onDetach:" + device);

    if (!device.equals(usbDevice)) {
        Log.d(TAG, "onDetach: Other device.");
        return "Other device";
    }

    if (usbConnection != null) {
        UsbInterface usbIf = usbDevice.getInterface(INTERFACE_INDEX);
        usbConnection.releaseInterface(usbIf);
        usbConnection.close();
    }
    usbConnection = null;
    usbDevice = null;
    isReady = false;
    return null;
}

ここもドキュメント通りです。注意事項はありません。

レシーバ

レシーバの登録

public static UsbReceiver init(MainActivity activity, Driver driver, String permissionName) {
    UsbReceiver receiver = new UsbReceiver(activity, driver, permissionName);
    IntentFilter filter = new IntentFilter();
    filter.addAction(permissionName);  // USBデバイスの利用許可の通知を受ける。
    filter.addAction(UsbManager.ACTION_USB_DEVICE_DETACHED);
    activity.registerReceiver(receiver, filter);
    return receiver;
}

デバイスの利用許可とDETACHEDを受け取ります。

レシーバの処理

@Override
public void onReceive(Context context, Intent intent) {
    String action = intent.getAction();
    Log.d(TAG,"onReceive:"+action);
    UsbDevice device = (UsbDevice)intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
    if (permissionName.equals(action)) {
        String errorMeg = driver.onStart(device);
        if (errorMeg != null) {
            activity.errorDialog(errorMeg);
        }
    } else if (UsbManager.ACTION_USB_DEVICE_DETACHED.equals(action)) {
        if (driver.onDetach(device) == null) {
            activity.finish();
        }
    }
}

デバイス使用許可が来たらデバイスの接続開始処理を呼びます。
DETACHED で Activity を終わらせていますがアプリの仕様しだいです。

データの送受信

USB_ENDPOINT_XFER_INT(非同期) の通信は UI スレッドでは行えません。 必ず、AsyncTask か Thread で行います。

非同期パケット送信

    private void doRequest(byte[] buff) throws IOException {
        Log.d(TAG, "request:" + dump(buff));

        ByteBuffer buffer = ByteBuffer.allocate(buff.length);
        UsbRequest request = new UsbRequest();
        buffer.put(buff);

        request.initialize(usbConnection, endpointOut);
        request.queue(buffer, buff.length);

        UsbRequest finishReq;
        while ((finishReq = usbConnection.requestWait()) != request) {
            if (finishReq == null) throw new IOException("Request failed.");
            sleep(100);
        }
    }

非同期パケット送信の方法はこれでほぼ定形だと思います。

requestWait() の戻り値は同時に走っている他の UsbRequest の場合もあるのでループでチェックします。 null は何らかのエラーが有ったとき返るようです。

非同期パケット受信

    private byte[] doResponse() throws IOException {
        ByteBuffer buffer = ByteBuffer.allocate(endpointIn.getMaxPacketSize());
        buffer.clear();
        UsbRequest request = new UsbRequest();
        request.initialize(usbConnection, endpointIn);
        request.queue(buffer, endpointIn.getMaxPacketSize());

        UsbRequest finishReq;
        while ((finishReq = usbConnection.requestWait()) != request) {
            if (finishReq == null) throw new IOException("Request failed.");
            sleep(100);
        }

        // Note: OSバージョンにより flip() の必要性が異なる気がする...
        if (buffer.remaining() == 0) buffer.flip();

        byte[] buff = new byte[buffer.remaining()];
        buffer.get(buff);
        Log.d(TAG, "response:" + dump(buff));
        return buff;
    }

非同期パケット受信もこれでほぼ定形だと思います。

謎なのは エミュレータ(4.0.4) では buffer.flip() は必要無かったのですが Nexus7(4.4.2) では必要になりました。 赤外線リモコンキットはパケットが固定長なのでこのコードで どちらも動作しますがデバイスによっては OSバージョンのチェックが必要かもしれません。

感想

通信できるようになるまで作法と言うか手順が多いですね。
通信自体も非同期だと一手間かかります。
Android の USBホスト実装は基本的にこの形に乗りそうです。

ソース全体は以下のSVNを参照して下さい。

いよいよリモコンを操ります。
Nexus7からUSB赤外線リモコンを操る(後編)」に続きます。


2014/02/21

Nexus7からUSB赤外線リモコンを操る(前編)

スマホを汎用の赤外線リモコンにしたいと言う需要は結構有そうに思うのだがどうだろう。
少なくとも私は以前から欲しかった。

で密林さんで見つけたのがこれ

FRISKのケースに入ると言うUSB接続のマルチリモコンのキットだ。
安いのでキットの方をポチったら翌々日に到着。

部品数が少ないのでハンダごて使える人なら簡単に作れます。

完成品:

PCに メーカのサイト から落としてきたアプリを入れて動作確認するとサクッと動きました。

Android との接続

いきなりはまります。
接続してアプリから UsbManager.getDeviceList() としても何も取得できません。 調べると OS レベルでは認識しているようなのですがアプリから見えないようです。

「UsbManager.getDeviceList empty」でググると一杯出てくるのですがどうも マウス、キーボード、USBメモリはアプリには見せないようになっているらしいです。
仕様として明確になっている記述は見つかりません。
機種によって動く動かないの記事がみられるので USBホスト機能自体ベンダー依存なのかもしれません。

この赤外線リモコンキットはキーボードとマウスのインターフェースを持っているため OSに誤認されデバイス全体が隠蔽されてしまっているようです。

ファームウェアの書き換え

このキットはファームウェアのソースが公開されていてフォーラムからDLできます。 そこでファームウェアを書き換えてキーボード、マウスの機能を無効化する事にします。

フォーラムからファームウェア書換えツールをDLして修正したファームウェアに書き換えます。

ファームウェア書き換え手順

  1. デバイスを外して 赤いスイッチを BOOT側 にします。
  2. デバイスを接続して書換えツール(HIDBootLoader.exe) を起動します。
  3. 「Open hex file」ボタンを押してAndroid用修正ファームウェア(RemoconServant-for-android.hex)を選択します。
  4. 「Program/Verify」ボタンを押して Complete と表示されれば書き換え終了です。
  5. デバイスを外して 赤いスイッチを元に戻します。

ファームウェア修正内容

USBのdescriptor定義をいじってキーボード、マウスと認識されないようにしただけです。

自分でビルドしたい人は以下の修正パッチを当てて -D__FOR_ANDROID でコンパイルして下さい。

ビルドツールが見つけ辛いのでリンクを張って置きます。

ファームウェア書き換え結果

ファームウェア書き換え後、Android に接続するとちゃんとアプリから認識できました。

リモコン アプリ

感想

さらっと書いてますが、ここまでめっちゃ大変でした。

アプリから認識されないUSBデバイスの種類が曖昧だし、 ファームウェアのコンパイラはチップメーカのサイトから消えてるし。

ともあれやっとこれで Android のプログラムに入れます。

Nexus7からUSB赤外線リモコンを操る(中編)」に続きます。


2014/02/17

AndroidのWebViewをPCのChromeでデバッグ

WebView 上で Web アプリを開発する場合に JavaScript のデバッグが大変そうだなと思いどうすれば良いのか調べてみた。

結論から言うと PC の Chrome に ADB プラグインをインストールすると リモートで Chrome のデバッガが使える事が分かった。

しかも WebView がデバックできるようになったのが 4.4(KitKat) からと言うタイムリーさ。

インストール

以下の Chrome ウェブストアからプラグインをインストールすれば終わりです。
(なぜかウェブストアの検索では出てこないのでリンクを直接叩く必要があります。)

「+無料」のボタンをクリックでインストール開始します。

エミュレータの準備

WebView のデバッグは 4.4(KitKat) からなので andrid-sdk の SDK Manager を起動して 4.4.2(Level-19) 以上の開発環境を一式落とします。

次に AVD Manager を起動して 4.4.2(Level-19) 以上のエミュレータイメージを作成して起動します。

Target: の項目が 4.4.2(Level-19) 以上になっていれば他の項目は何でも良いです。

試してませんが 4.4 の実機があればそちらでもデバッグ可能なようです。

WebViewアプリの準備

アプリのどこかに以下のコードを入れてリモートデバッグを有効にします。

if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
    WebView.setWebContentsDebuggingEnabled(true);
}

API Level-19 以前には存在しないメソッドなのでバージョンチェックが必要です。 クラスメソッドなので WebView インスタンスは必要ありません。

コンパイルしなおしてエミュレータで実行しておけば準備完了です。

デバッガの起動

PC の Chrome を起動するとドロイド君のアイコンが有るのでクリックして 「View Inspection Targets」を選択します。

デバッグ可能なブラウザの一覧が表示されるので「inspect」をクリックします。

別ウインドウでデバッガが起動するので後は通常の Chrome と同じようにデバッグできます。

ブレークポイントで止めて見たところ。

所感

JavaScript のデバッグにおいては Chrome のデバッガは圧倒的に使いやすいのでこれは大変ナイスです。

これで Android におけるWebハイブリッドアプリを開発する為の環境はほぼ揃った用に思えます。


2014/02/13

AndroidでWebViewとNativeのハイブリッドアプリ

最近、WebViewを使ったWebとNativeのハイブリッドアプリが流行っている。

iPhoneとAndroidで同一アプリを開発する場合、Web部分が共通化できるメリットが大きいからだろう。

と言うわけで WebView を使ったハイブリッドアプリの作り方を調べてみた。

iPhoneってどうしてるの?

私は iPhone はやってません。(だって開発するだけで金取られるんだもんw)
とは言え iPhone との互換性を無視しては意味ないのでやり方だけは調べて置きます。

ググると一杯出てきますが、通信のリクエストをフックして Native のコードで WebAPI を擬似的に実装するようです。

JavaScriptとの接続方法としては無理が無く良い方法だと思われます。

一方、Android は...

WebView#addJavascriptInterface() を使って Java のオブジェクトを登録すると JavaScript からそのまま使えると言う直接的な方法です。

が、これ結構致命的なセキュリホールが報告されています。
JavaScript から getClass() 等も呼べてしまうため、 WebView に信頼できないサイトの JavaScript が紛れ込むと何でも出来てしまいます。

スマホの運用条件を考えると PKI でサーバを固め、 アプリも XSS に細心の注意を払う必要がありそうです。
現実的にはかなりハードル高いっすね。

但し、API Level 17 (4.2.2) からは @JavascriptInterface アノテーションが追加されメソッドのアクセス制限ができる用になったので この問題は解決されています。
って、最近過ぎるだろw

それはそれとして使ってみる

最近のブラウザはカメラやバイブレーションの API も最初から持っていて以外に追加機能のネタが無かったりします。

なのでクロスドメイン可能な XMLHttpRequestXS とか追加してさらにセキュリティホールを広げて見たいと思います。

Javaオブジェクトの登録

基本ドキュメントの通りですが登録しているのはファクトリです。 ファクトリから得た Java オブジェクトも JavaScript から使えます。

登録処理抜粋:

private class SampleWebViewClient extends WebViewClient {
    @Override
    public void onPageStarted (WebView webview, String url, Bitmap favicon) {
        // 拡張XMLHttpRequestファクトリの初期化。
        XMLHttpRequestXSFactory factory = getXMLHttpRequestXSFactory();
        factory.setAccessControlList(_accessControlList);
        factory.setWebView(webview);
        webview.addJavascriptInterface(factory, "XMLHttpRequestXSFactory");
    }
    …省略
}

登録されるクラス抜粋:

public class XMLHttpRequestXSFactory {
    private WebView _webview;
    private AccessControlList _accessControlList;
    public XMLHttpRequestXSFactory() { }
    @JavascriptInterface
    public XMLHttpRequestXS getXMLHttpRequestXS() {
        return new XMLHttpRequestXS(this);
    }
    …省略
}

登録名に abc.XMLHttpRequestXSFactory とかしてみましたがダメでした。 navigator.~ もダメです。 グローバルの直下のみに登録できるようです。

登録のタイミングは onCreate() だけで無く WebViewClient#onPageStarted() にも必要なようです。

JavaScript から Java の呼び出し

登録されたJavaオブジェクトの呼び出しはほぼそのままです。 但し、フィールドにはアクセス出来ません。

JavaScript抜粋:

function XMLHttpRequestXS() {
    this._native = XMLHttpRequestXSFactory.getXMLHttpRequestXS();
    …省略
};
XMLHttpRequestXS.prototype = {
    open : function(method, url, async) {
        var error = this._native.open(method, url, async);
        if (error) throw error;
    },
    …省略
}

呼ばれ側Java抜粋:

public class XMLHttpRequestXS {
    …省略
    @JavascriptInterface
    public String open(String type, String url, boolean isAsync) throws Exception {
        Log.d(TAG,"open:"+type+" "+url);
        try {
            _isAsync = isAsync;
            if (GET.equalsIgnoreCase(type)) {
                _request = new HttpGet(url);
            } else {
                _request = new HttpPost(url);
            }
            _factory.checkDomain(_request.getURI());

            String cookie = CookieManager.getInstance().getCookie(url);
            _request.setHeader("Cookie", cookie);
            setReadyState(OPENED);
            return null;
        } catch (Throwable t) {
            Log.e(TAG, t.getMessage(), t);
            setReadyState(ERROR);
            return t.getMessage();
        }
    }
    …省略
}

ここで気になったのはメソッドへの引数です。 ドキュメントには JS -> Java の変換ルールが見つけられませんでした。

実際に試した方の情報では String,int,double,boolean,int[],String[] が受け取れたようです。

とりあえず、プリミティブまでは大丈夫そうな気がします。
JSON変換できるオブジェクトならこんな感じで渡せるようです。

Android.test(JSON.stringify({abc:"ABC", yyy:2}));

@JavascriptInterface
public void test(String jsonStr) throws JSONException {
    JSONObject json = new JSONObject(jsonStr);
    String abc = json.getString("abc");
    int yyy = json.getInt("yyy");
}

Java から JavaScript の呼び出し

ドキュメントの通り以下で呼び出せます。

webview.loadUrl("javascript:スクリプト");

が、JS->Java->JSと呼び出すと例外になります。

02-13 04:35:39.382: W/webview(3503): java.lang.Throwable: Warning: 
A WebView method was called on thread 'WebViewCoreThread'.
All WebView methods must be called on the UI thread.
Future versions of WebView may not support use on other threads.

JavaScript は WebView のスレッドで走っているので UIスレッドから呼べ、 と言うことらしいです。

つまり、JavaScriptから呼び出されたメソッドからコールバックしようとする場合、 HandlerかAsyncTaskを経由する必要があるようです。

Java から JavaScript オブジェクトの生成

どうも無理そうです。
JavaScript自体がJavaで実装されて無いと思われるので難しいのでしょう。

XMLやJSONは文字列で渡して JavaScript 側でパーズしてもらう事になりそうです。

動かしてみる

完成した XMLHttpRequestXS を jQuery.ajax() で実行してみます。

HTML:

<!DOCTYPE html>
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <script type="text/javascript" src="XMLHttpRequestXS.js" ></script>
    <script type="text/javascript" src="jquery-1.11.0.js" ></script>
    <script type="text/javascript">
jQuery.support.cors = true; // クロスドメインをjQueryでするために必要。

function testJqueryAsync() {
    console.log("testJqueryAsync");
    $.ajax({
        type: "GET", url: "http://www.redmine.org/issues.json",
        dataType: "json",
        success: function(data){
            var issues = data.issues;
            var $table = $("#table");
            for (var i=0; i<issues.length;i++) {
                $table.append($("<tr><td>"+issues[i].id+"</td><td>"+issues[i].subject+"</td></tr>"));
            }
        },
        error: function(xhr, status, error){
            alert("error:"+status+":"+error);
        },
        xhr: function() { // jQueryが使うXHRの差替用API
            return new XMLHttpRequestXS();
        }
    });
}
    </script>
</head>
<body onload="testJqueryAsync()">
    <h3>Ajax result from http://www.redmine.org/issues.json</h3>
    <table id="table" width="100%" border="1" >
        <tr><th>ID</th><th>Subject</th></tr>
    </table>
</body>
</html>

jQuery には XMLHttpRequest 差替え用の API が最初から付いてました。 さすが jQuery です。

実行結果:

外部サイトからちゃんとデータを取って来ています。

所感

一見単純そうに見えてやってみると以外に泥沼w
iPhoneとの共通化は表面を JavaScript の API にしてスタブを2種類用意する感じでしょうか。

課題も多いですが Andorid/iPhone のコード共通化やWebからアプリが更新できるメリットは大きいですね。

PhoneGap や Titanium を使う手もありますが HTML5 の機能拡張が著しいので足りない機能だけ Native で実装すると言う方がフレームワークの縛りが無い分楽かもしれません。

サンプルのソースコード一式は以下のSVNにあります。
注意:XMLHttpRequestXSはXSSのセキュリティホールを持ちます。技術検証以上の利用はしないで下さい。


2013/10/30

AndroidとPCのBluetooth接続のサンプル

そういえば Bluetooth ってやったとこが無いので Android と PC の接続を試してみることにした。

Bluetooth プログラミングの基礎知識

これが分かってなくてちょっと苦労しました。

  • 各Bluetooth機器は固有のMACアドレスを持っている。
    • 6byte。NICのMACアドレスとは別採番。
  • Bluetooth機器内の各サービスは固有のUUIDを持っている。
    • 規定のUUID一覧
    • 独自プロトコルの場合はツールで生成したUUIDで良い。
      • サーバとクライアントが認識していれば良いので衝突とかは考えなくてよい。
  • Bluetoothは非公開にできる。
    • PCやAndroidはデフォルト非公開になっている。
    • 非公開で有ってもペアリング済みなら接続できる。
    • ペアリング未でも公開されていれば検索して接続できる。

android.bluetooth と JSR82

Java で Bluetooth を扱う場合には JSR82 という仕様が存在するのですが Android は独自の API を使います。

つまり、PC と Ancdoid で API を使い分ける必要があると言うことです。orz
#まぁUSBの時はJavaですら無かった訳ですが。

AndroidのAPIはいつもお世話になる「勝手に翻訳」さんのサイトに有ります。

JSR82 の使い方は整理されているところが見つからずexampleを見ながら対応しました。
JSR82 のオープンな実装は以下のプロジェクトが存在します。

現時点(2013/10)での正式リリース版は Win7/x64 に対応していません。
私は bluecore-2.1.1-SNAPSHOT.jar を使いました。

jar の中には DLL とかも入っています。

サーバ側の実装

PCをサーバ側とします。
このクラス1つだけで JSR82 の実装です。 ドキュメントが見つからず example を頼りに調べたので不正確かもしれません。

RfcommServer.java

  • 重要なのは Connecror.open() だけで後の作りはほぼソケットを使ったサーバと同じです。
  • 引数のURL
    • プロトコル:ベースのプロトコルを指定。RFCOMMの場合は「btspp:」。一覧表はどこに有るか不明。
    • MACアドレス:サーバ側は「localhost」に固定。
    • UUID:適当にツールで生成します。
  • サービスをデバイスに登録して置くとクライアントがサービス一覧でUUIDを取得できます。
    • Androidはペアリング済みのデバイスで無いとサービス(UUID)を取得できませんでした。
    • 登録しなくでも接続は可能な様子。
  • 一応、スレッドで同時接続可能にしてあるが実際にできるかは未確認です。

package org.kotemaru.sample.bluetooth;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Date;

import javax.bluetooth.LocalDevice;
import javax.bluetooth.ServiceRecord;
import javax.microedition.io.Connector;
import javax.microedition.io.StreamConnection;
import javax.microedition.io.StreamConnectionNotifier;

/**
 * 英大文字変換エコーバック Bluetooth サーバ。
 */
public class RfcommServer {
    /**
     * UUIDは独自プロトコルのサービスの場合は固有に生成する。
     * - 各種ツールで生成する。(ほぼ乱数)
     * - 注:このまま使わないように。
     */
    static final String serverUUID = "11111111111111111111111111111123";

    private StreamConnectionNotifier server = null;

    public RfcommServer() throws IOException {
        // RFCOMMベースのサーバの開始。
        // - btspp:は PRCOMM 用なのでベースプロトコルによって変わる。
        server = (StreamConnectionNotifier) Connector.open(
                "btspp://localhost:" + serverUUID,
                Connector.READ_WRITE, true
        );
        // ローカルデバイスにサービスを登録。必須ではない。
        ServiceRecord record = LocalDevice.getLocalDevice().getRecord(server);
        LocalDevice.getLocalDevice().updateRecord(record);
    }

    /**
     * クライアントからの接続待ち。
     * @return 接続されたたセッションを返す。
     */
    public Session accept() throws IOException {
        log("Accept");
        StreamConnection channel = server.acceptAndOpen();
        log("Connect");
        return new Session(channel);
    }
    public void dispose() {
        log("Dispose");
        if (server  != null) try {server.close();} catch (Exception e) {/*ignore*/}
    }

    /**
     * セッション。
     * - 並列にセッションを晴れるかは試していない。
     * - 基本的に Socket と同じ。
     */
    static class Session implements Runnable {
        private StreamConnection channel = null;
        private InputStream btIn = null;
        private OutputStream btOut = null;

        public Session(StreamConnection channel) throws IOException {
            this.channel = channel;
            this.btIn = channel.openInputStream();
            this.btOut = channel.openOutputStream();
        }

        /**
         * 英小文字の受信データを英大文字にしてエコーバックする。
         * - 入力が空なら終了。
         */
        public void run() {
            try {
                byte[] buff = new byte[512];
                int n = 0;
                while ((n = btIn.read(buff)) > 0) {
                    String data = new String(buff, 0, n);
                    log("Receive:"+data);
                    btOut.write(data.toUpperCase().getBytes());
                    btOut.flush();
                }
            } catch (Throwable t) {
                t.printStackTrace();
            } finally {
                close();
            }
        }
        public void close() {
            log("Session Close");
            if (btIn    != null) try {btIn.close();} catch (Exception e) {/*ignore*/}
            if (btOut   != null) try {btOut.close();} catch (Exception e) {/*ignore*/}
            if (channel != null) try {channel.close();} catch (Exception e) {/*ignore*/}
        }
    }

    //------------------------------------------------------
    public static void main(String[] args) throws Exception {
        RfcommServer server = new RfcommServer();
        while (true) {
            Session session = server.accept();
            new Thread(session).start();
        }
        //server.dispose();
    }
    private static void log(String msg) {
        System.out.println("["+(new Date()) + "] " + msg);
    }
}

クライアント側の実装

Androidがクライアント側になります。
今回はペアリング済みのデバイスとの接続なのでデバイスの検索はしません。
検索はBluetooth実装の肝だったりするのですが以下の理由からはしょりました。

  • アプリで実装しても設定画面のペアリング処理とほぼ同じになる。
  • AndroidからPCを検索するにはPC側を一時的に公開設定にする必要があり接続毎は非実用的。
    • PCを公開しっ放しはセキュリティ上の問題から不可。

ManifestAndroid.xml

デフォルトにパーミッションを追加しただけです。

<uses-permission android:name="android.permission.BLUETOOTH"></uses-permission>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"></uses-permission>

ADMINは今回は使っていませんがデバイスの検索で必要になるので入れといた方が良いです。

MainActivity.java

  • Blurtooth の機能は BluetoothTask.java に実装しているので BluetoothTask を呼んでいる所以外は普通のActivityです。
package org.kotemaru.android.sample.bluetooth;

import java.util.Set;

import android.os.Bundle;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.ProgressDialog;
import android.bluetooth.BluetoothDevice;
import android.content.DialogInterface;
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 final static int DEVICES_DIALOG = 1;
    private final static int ERROR_DIALOG = 2;

    private BluetoothTask bluetoothTask = new BluetoothTask(this);

    private ProgressDialog waitDialog;
    private EditText editText1;
    private EditText editText2;
    private String errorMessage = "";

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

        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();
                bluetoothTask.doSend(msg);
            }
        });
        Button resetBtn = (Button) findViewById(R.id.resetBtn);
        resetBtn.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                restart();
            }
        });
    }

    @SuppressWarnings("deprecation")
    @Override
    protected void onResume() {
        super.onResume();
        // Bluetooth初期化
        bluetoothTask.init();
        // ペアリング済みデバイスの一覧を表示してユーザに選ばせる。
        showDialog(DEVICES_DIALOG);
    }

    @Override
    protected void onDestroy() {
        bluetoothTask.doClose();
        super.onDestroy();
    }

    public void doSetResultText(String text) {
        editText2.setText(text);
    }

    protected void restart() {
        Intent intent = this.getIntent();
        this.finish();
        this.startActivity(intent);
    }

    //----------------------------------------------------------------
    // 以下、ダイアログ関連
    @Override
    protected Dialog onCreateDialog(int id) {
        if (id == DEVICES_DIALOG) return createDevicesDialog();
        if (id == ERROR_DIALOG) return createErrorDialog();
        return null;
    }
    @SuppressWarnings("deprecation")
    @Override
    protected void onPrepareDialog(int id, Dialog dialog) {
        if (id == ERROR_DIALOG) {
            ((AlertDialog) dialog).setMessage(errorMessage);
        }
        super.onPrepareDialog(id, dialog);
    }

    public Dialog createDevicesDialog() {
        AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(this);
        alertDialogBuilder.setTitle("Select device");

        // ペアリング済みデバイスをダイアログのリストに設定する。
        Set<BluetoothDevice> pairedDevices = bluetoothTask.getPairedDevices();
        final BluetoothDevice[] devices = pairedDevices.toArray(new BluetoothDevice[0]);
        String[] items = new String[devices.length];
        for (int i=0;i<devices.length;i++) {
            items[i] = devices[i].getName();
        }

        alertDialogBuilder.setItems(items, new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                dialog.dismiss();
                // 選択されたデバイスを通知する。そのまま接続開始。
                bluetoothTask.doConnect(devices[which]);
            }
        });
        alertDialogBuilder.setCancelable(false);
        return alertDialogBuilder.create();
    }

    @SuppressWarnings("deprecation")
    public void errorDialog(String msg) {
        if (this.isFinishing()) return;
        this.errorMessage = msg;
        this.showDialog(ERROR_DIALOG);
    }
    public Dialog createErrorDialog() {
        AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(this);
        alertDialogBuilder.setTitle("Error");
        alertDialogBuilder.setMessage("");
        alertDialogBuilder.setPositiveButton("Exit", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                dialog.dismiss();
                finish();
            }
        });
        return alertDialogBuilder.create();
    }

    public void showWaitDialog(String msg) {
        if (waitDialog == null) {
            waitDialog = new ProgressDialog(this);
        }
        waitDialog.setMessage(msg);
        waitDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER);
        waitDialog.show();
    }
    public void hideWaitDialog() {
        waitDialog.dismiss();
    }
}

BluetoothTask.java

Bluetoothのメイン処理です。

  • デバイス検索を行わないBluetoothの最小構成の手順はこれだけです。
1. bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
2. Set devices = bluetoothAdapter.getBondedDevices();
3. bluetoothDevice = devicesから選択;
4. bluetoothSocket = bluetoothDevice.createRfcommSocketToServiceRecord(APP_UUID);
5. bluetoothSocket.connect();
6. btIn = bluetoothSocket.getInputStream();
7. btOut = bluetoothSocket.getOutputStream();
8. btIn.read()/btOut.write()
9. bluetoothSocket.close();
  • ただし、connect()以降は通信が発生するためすべて非同期処理とする必要があります。
  • Bluetoothの使用許可が無い場合にユーザに許可を求める処理ははしょってます。

package org.kotemaru.android.sample.bluetooth;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Set;
import java.util.UUID;

import android.os.AsyncTask;
import android.os.ParcelUuid;
import android.util.Log;
import android.annotation.SuppressLint;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothSocket;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;

public class BluetoothTask {
    private static final String TAG = "BluetoothTask";

    /**
     * UUIDはサーバと一致している必要がある。
     * - 独自サービスのUUIDはツールで生成する。(ほぼ乱数)
     * - 注:このまま使わないように。
     */
    private static final UUID APP_UUID = UUID.fromString("11111111-1111-1111-1111-111111111123");

    private MainActivity activity;
    private BluetoothAdapter bluetoothAdapter;
    private BluetoothDevice bluetoothDevice = null;
    private BluetoothSocket bluetoothSocket;
    private InputStream btIn;
    private OutputStream btOut;

    public BluetoothTask(MainActivity activity) {
        this.activity = activity;
    }

    /**
     * Bluetoothの初期化。
     */
    public void init() {
        // BTアダプタ取得。取れなければBT未実装デバイス。
        bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
        if (bluetoothAdapter == null) {
            activity.errorDialog("This device is not implement Bluetooth.");
            return;
        }
        // BTが設定で有効になっているかチェック。
        if (!bluetoothAdapter.isEnabled()) {
            // TODO: ユーザに許可を求める処理。
            activity.errorDialog("This device is disabled Bluetooth.");
            return;
        }
    }
    /**
     * @return ペアリング済みのデバイス一覧を返す。デバイス選択ダイアログ用。
     */
    public Set<BluetoothDevice> getPairedDevices() {
        return bluetoothAdapter.getBondedDevices();
    }

    /**
     * 非同期で指定されたデバイスの接続を開始する。
     * - 選択ダイアログから選択されたデバイスを設定される。
     * @param device 選択デバイス
     */
    public void doConnect(BluetoothDevice device) {
        bluetoothDevice = device;
        try {
            bluetoothSocket = bluetoothDevice.createRfcommSocketToServiceRecord(APP_UUID);
            new ConnectTask().execute();
        } catch (IOException e) {
            Log.e(TAG,e.toString(),e);
            activity.errorDialog(e.toString());
        }
    }

    /**
     * 非同期でBluetoothの接続を閉じる。
     */
    public void doClose() {
        new CloseTask().execute();
    }

    /**
     * 非同期でメッセージの送受信を行う。
     * @param msg 送信メッセージ.
     */
    public void doSend(String msg) {
        new SendTask().execute(msg);
    }

    /**
     * Bluetoothと接続を開始する非同期タスク。
     * - 時間がかかる場合があるのでProcessDialogを表示する。
     * - 双方向のストリームを開くところまで。
     */
    private class ConnectTask extends AsyncTask<Void, Void, Object> {
        @Override
        protected void onPreExecute() {
            activity.showWaitDialog("Connect Bluetooth Device.");
        }

        @Override
        protected Object doInBackground(Void... params) {
            try {
                bluetoothSocket.connect();
                btIn = bluetoothSocket.getInputStream();
                btOut = bluetoothSocket.getOutputStream();
            } catch (Throwable t) {
                doClose();
                return t;
            }
            return null;
        }

        @Override
        protected void onPostExecute(Object result) {
            if (result instanceof Throwable) {
                Log.e(TAG,result.toString(),(Throwable)result);
                activity.errorDialog(result.toString());
            } else {
                activity.hideWaitDialog();
            }
        }
    }

    /**
     * Bluetoothと接続を終了する非同期タスク。
     * - 不要かも知れないが念のため非同期にしている。
     */
    private class CloseTask extends AsyncTask<Void, Void, Object> {
        @Override
        protected Object doInBackground(Void... params) {
            try {
                try{btOut.close();}catch(Throwable t){/*ignore*/}
                try{btIn.close();}catch(Throwable t){/*ignore*/}
                bluetoothSocket.close();
            } catch (Throwable t) {
                return t;
            }
            return null;
        }

        @Override
        protected void onPostExecute(Object result) {
            if (result instanceof Throwable) {
                Log.e(TAG,result.toString(),(Throwable)result);
                activity.errorDialog(result.toString());
            }
        }
    }

    /**
     * サーバとメッセージの送受信を行う非同期タスク。
     * - 英小文字の文字列を送ると英大文字で戻ってくる。
     * - 戻ってきた文字列を下段のTextViewに反映する。
     */
    private class SendTask extends AsyncTask<String, Void, Object> {
        @Override
        protected Object doInBackground(String... params) {
            try {
                btOut.write(params[0].getBytes());
                btOut.flush();

                byte[] buff = new byte[512];
                int len = btIn.read(buff); // TODO:ループして読み込み

                return new String(buff, 0, len);
            } catch (Throwable t) {
                doClose();
                return t;
            }
        }

        @Override
        protected void onPostExecute(Object result) {
            if (result instanceof Exception) {
                Log.e(TAG,result.toString(),(Throwable)result);
                activity.errorDialog(result.toString());
            } else {
                // 結果を画面に反映。
                activity.doSetResultText(result.toString());
            }
        }
    }
}

実行

ペアリング

まずPCとAndroidのペアリングが必要です。1回やればOKです。

(1) PCの Bluetoothの設定で「このコンピュータの検出を許可する」に設定します。

  • 注:ペアリング終了後、必ず非公開に戻してください。

(2) AndroidのBluetoothの設定で「デバイス検出」をタップして暫く待つとPCが検出されるのでタップします。

  • U24E は私のPCに設定した名前です。当然、お使いのPCの名前が表示されます。

(3) PCに許可を求めるポップアップが出るのでクリックします。

(4) 確認画面が出るので「次へ」をクリックします。

(5) Androidにダイアログが出ているので「ペアを設定する」をタップします。

  • パスキーは本来(4)の画面と一致します。キャプチャの都合です。

(6) PCとAndroidでそれぞれデバイスが認識されていればOKです。

サーバの起動

RfcommServer.java を eclipse から実行します。 Consoleのログに Accept と出ていればOKです。

クライアント実行

アプリを起動するとペアリング済みデバイスの一覧がでるのでPCをタップします。

上の TextField に英小文字を入れて「Send」をタップすると下の TextField に英大文字でエコーバックされればOKです。

雑感

MACアドレスをIPアドレス、UUIDをポート番号と考えれば殆どソケットと同じです。 RFCOMM以外のプロトコルを使うと色々有そうですけど。

調査中に見つけた高木先生の記事がおもしろかった。

携帯端末の Bluetooth を公開設定にしていると行動追跡されちゃうよ、と言うお話で実際に山手線で調査したもの。

Androidは今回の調査で一時的にしか公開状態に出来ない事が分かったので安心。 ノートPCは設定画面に注意書きが有るけど設定の戻し忘れに注意かな。


2013/10/24

Haswell購入してみた

信長の野望の新作が出るのでPCを新調した。
Core2Duoからなので5年ぶりくらい。

最新の Intel Haswell で CPU, M/B, メモリをセット買ってきた。
その他は手持ちから流用。

写真とる前に組み立てちゃったんであんまりおもしろくない完成品の写真。

グラフィックカードはCPUに内臓で光学ドライブも付けないので中はガラガラ。
電源は300Wの古いATXだけど特に問題なさそう。

Windowsエクスペリエンスの値はこんな感じ。

そこそこのハイスペマシンのはずなのだが明かにHDDが足を引っ張ってる...
実際、SSDのノートPCの方が体感速度は早いし。

あれ、何かアマゾンからメール来た。
... 信長、発売日伸びとるやんけー ヽ(`Д´#)ノ ムキー!!

買った物及び買いたい物


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

サイト内検索

登録
RSS/2.0

カテゴリ

最近の投稿

リンク

アーカイブ