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/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/08/11

Nexus7のカメラでQRコードを読込んでみた

Androidのカメラアプリを作ってみようと思ったのだが Nexus7 にはフロントカメラしか無い。orz
QRコード・リーダーぐらいならなんとかなるかと思い手を付けてみた。

カメラの準備

普通は以下のようにするとカメラが準備できるのだが Nexus7 だとリアカメラを開こうとして失敗する。

Camera camera = Camera.open();

Nexus7 のフロントカメラを開くにはこうする。

int cameraId = 0;
Camera camera = Camera.open(cameraId);

cameraId はハードのカメラの実装状況によりまちまちなので正しくはこんなことする必要が有りそう。

Camera.CameraInfo info = new Camera.CameraInfo();
for (int id=0; id<Camera.getNumberOfCameras(); id++) {
    Camera.getCameraInfo(id, info);
    if (info.??? == ???) { // 使いたいカメラの条件式
        cameraId = id;
    }
}

AnsroidManufest.xmlにカメラのパーミッションも必要。

<uses-permission android:name="android.permission.CAMERA"/>
<uses-feature android:name="android.hardware.camera" android:required="true"/>
<!--
<uses-feature android:name="android.hardware.camera.front" android:required="true"/>
-->

android.hardware.camera.front は無くても良いらしい...

プレビュー

SurfaceView を使う。 ググれば解説多数なので割愛。

QRコードのライブラリ

QRコードのライブラリは ZXing を使う。 google が開発しオープン化されている物。

ここから落せる。

しかし、現行の ZXing-2.2.zip にはソースしか入っていないので以下から2.3-SNAPのjarを落してきた。

  • https://oss.sonatype.org/content/repositories/snapshots/com/google/zxing/

必要なものは core-xxxxx.jar と javase-xxxxxx.jar のみ。
eclipse の Android プロジェクトの libs に入れておけば利用可能。

使い方はサンプル・コードの onPreviewFrame() 内で完結している。

注意するのはフロントカメラは左右反転しているので PlanarYUVLuminanceSource コンストラクタの最後の引数が ture になっている必要がある。

サンプル・コード

このソースだけ。リソースも参照していない。

package org.kotemaru.sample.camera;

import java.io.IOException;
import com.google.zxing.BinaryBitmap;
import com.google.zxing.MultiFormatReader;
import com.google.zxing.PlanarYUVLuminanceSource;
import com.google.zxing.Reader;
import com.google.zxing.Result;
import com.google.zxing.common.HybridBinarizer;

import android.app.Activity;
import android.content.res.Configuration;
import android.hardware.Camera;
import android.hardware.Camera.AutoFocusCallback;
import android.os.Bundle;
import android.util.Log;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.ViewGroup;
import android.widget.Toast;

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

    private int CAMERA_ID = 0; // for Nexus7

    private Camera camera;
    private SurfaceView surfaceView;
    private CameraListener cameraListener = new CameraListener();

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        surfaceView = new SurfaceView(this);
        setContentView(surfaceView);
    }

    @Override
    protected void onPostCreate(Bundle savedInstanceState) {
        super.onPostCreate(savedInstanceState);
        SurfaceHolder holder = surfaceView.getHolder();
        holder.addCallback(cameraListener);
    }

    private int getOrientation() {
        return getResources().getConfiguration().orientation;
    }

    private class CameraListener implements
            SurfaceHolder.Callback,
            AutoFocusCallback,
            Camera.PictureCallback,
            Camera.PreviewCallback
    {

        @Override
        public void surfaceCreated(SurfaceHolder holder) {
            camera = Camera.open(CAMERA_ID);
            try {
                camera.setPreviewDisplay(holder);
            } catch (IOException e) {
                Log.e(TAG, e.toString(), e);
            }
        }

        @Override
        public void surfaceChanged(SurfaceHolder holder, int format,
                int width, int height) {

            // カメラのプレビューサイズをViewに設定
            Camera.Parameters parameters = camera.getParameters();
            Camera.Size size = parameters.getSupportedPreviewSizes().get(0); // 0=最大サイズ 
            parameters.setPreviewSize(size.width, size.height);
            camera.setParameters(parameters);

            // 画面回転補正。
            ViewGroup.LayoutParams layoutParams = surfaceView.getLayoutParams();
            if (getOrientation() == Configuration.ORIENTATION_PORTRAIT) {
                camera.setDisplayOrientation(90);
                layoutParams.width = size.height;
                layoutParams.height = size.width;
            } else {
                camera.setDisplayOrientation(0);
                layoutParams.width = size.width;
                layoutParams.height = size.height;
            }
            surfaceView.setLayoutParams(layoutParams);

            // オートフォーカス設定。
            camera.autoFocus(cameraListener);

            camera.startPreview();
        }

        @Override
        public void surfaceDestroyed(SurfaceHolder holder) {
            camera.autoFocus(null);
            camera.setPreviewCallback(null);
            camera.release();
            camera = null;
        }

        @Override
        public void onAutoFocus(boolean success, Camera camera) {
            if (success) {
                Log.d(TAG, "focus");
                // プレビューのデータ取得。
                camera.setPreviewCallback(cameraListener);
                // フルサイズ画像はTODO
                //camera.takePicture(null,null,cameraListener);
            }
        }

        @Override
        public void onPictureTaken(byte[] data, Camera camera) {
            // フルサイズ画像もやることは同じ。
            onPreviewFrame(data, camera);
        }

        @Override
        public void onPreviewFrame(byte[] data, Camera camera) {
            // 処理は1回なのでコールバック取り消し
            camera.setPreviewCallback(null);

            // 基礎データ取得
            Camera.CameraInfo info = new Camera.CameraInfo();
            Camera.getCameraInfo(CAMERA_ID, info);
            int w = camera.getParameters().getPreviewSize().width;
            int h = camera.getParameters().getPreviewSize().height;
            //int w = camera.getParameters().getPictureSize().width;
            //int h = camera.getParameters().getPictureSize().height;
            boolean isMirror = (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT);

            // プレビュー画像の型変換
            PlanarYUVLuminanceSource source = new PlanarYUVLuminanceSource(
                    data, w, h, 0, 0, w, h, isMirror);
            BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));

            // QRコード読み込み。
            Reader reader = new MultiFormatReader();
            try {
                Log.d(TAG, "decode");
                Result result = reader.decode(bitmap);
                String text = result.getText();

                Toast.makeText(MainActivity.this, text, Toast.LENGTH_LONG).show();
                Log.i(TAG, "result:" + text);
                camera.stopPreview();
                camera.autoFocus(null);
            } catch (Exception e) {
                // QRコード認識失敗でも例外発生する。
                Log.d(TAG, "decode-fail:" + e.toString());
                camera.autoFocus(cameraListener);
            }
        }
    };
}

実行結果

さすがにエミュレータでは動かないので実機で確認。

一応、QRコードを認識する事には成功した。 但し、QRコード自体をかなり拡大しないと認識できない。 小さいとカメラを非常に近づける必要があるためピントが合わないのかもしれない。

それ以前にプレビューを見るのに横からのぞき込まないといけないと言う致命的欠陥が...
(最初から分かっていた訳だがw)

技術的に可能だけど使い物にならないと言う予想通りの結論に達したのでこれで終りにしよう。



参考にさせて頂いたサイト:


2013/05/06

antでファイル一覧を生成する。

JavaScriptで1クラス=1ファイルでコードを書いていると .js ファイルが沢山できる。

これを一々、<script> タグにして HTML に埋め込むのが以外に面倒くさい。

で、ant の fileset で何とかならないかと思って調べてみたらこうなった。

<taskdef resource="net/sf/antcontrib/antcontrib.properties">
  <classpath>
    <pathelement location="${project.root}/lib/ant-contrib-1.0b3.jar"/>
  </classpath>
</taskdef>

<target name="main">
    <antcall target="makeListFile">
        <param name="includes" value="**/*.js"/>
        <param name="file" value="html.txt"/>
        <param name="prefix" value="&lt;script src='"/>
        <param name="suffix" value="'/>"/>
    </antcall>
</target>

<target name="makeListFile">
    <fileset id="fileset" dir=".">
        <include name="${includes}" />
    </fileset>

    <pathconvert property="files" refid="fileset" pathsep=";" dirsep="/" >
        <map from="${basedir}/" to="" />
    </pathconvert>

    <echo file="${file}"></echo>
    <foreach target="addLine" param="name" inheritall="true"
        list="${files}" delimiter=";"
    >
   </foreach>
</target>
<target name="addLine">
        <echo append="true" file="${file}">${prefix}${name}${suffix}
</echo>
</target>

面倒くせー。 そもそも contrib ずっとベータだし。

次に script タグを試して見ようと思って見付けたのがこちらのサンプル。

scriptタスクのマニュアル より

<?xml version="1.0" encoding="ISO-8859-1"?>
<project name="MyProject" basedir="." default="main">

  <property name="fs.dir" value="src"/>
  <property name="fs.includes" value="**/*.txt"/>
  <property name="fs.excludes" value="**/*.tmp"/>

  <target name="main">
    <script language="javascript"> <![CDATA[

      // import statements
      // importPackage(java.io);
      importClass(java.io.File);

      // Access to Ant-Properties by their names
      dir      = project.getProperty("fs.dir");
      includes = MyProject.getProperty("fs.includes");
      excludes = self.getProject()  .getProperty("fs.excludes");

      // Create a <fileset dir="" includes=""/>
      fs = project.createDataType("fileset");
      fs.setDir( new File(dir) );
      fs.setIncludes(includes);
      fs.setExcludes(excludes);

      // Get the files (array) of that fileset
      ds = fs.getDirectoryScanner(project);
      srcFiles = ds.getIncludedFiles();

      // iterate over that array
      for (i=0; i<srcFiles.length; i++) {

        // get the values via Java API
        var basedir  = fs.getDir(project);
        var filename = srcFiles[i];
        var file = new File(basedir, filename);
        var size = file.length();

        // create and use a Task via Ant API
        echo = MyProject.createTask("echo");
        echo.setMessage(filename + ": " + size + " byte");
        echo.perform();
      }
    ]]></script>
  </target>
</project>

もっと面倒くせー orz

結局、自前でスクリプトを呼ぶTask書くのが早いんじゃね、と思って作ったのがこれ。

package jstask;

import java.io.File;
import java.util.*;
import org.apache.tools.ant.*;
import org.apache.tools.ant.types.*;
import org.apache.tools.ant.types.resources.FileResource;
import org.mozilla.javascript.*;

public class JsTask extends Task  {
    private List<ResourceCollection> rclist = new ArrayList<ResourceCollection>();
    private List<Parameter> params = new ArrayList<Parameter>();
    private String text;

    public void add(ResourceCollection rc) {
        rclist.add(rc);
    }
    public void addParam(Parameter param) {
        params.add(param);
    }
    public void addText(String str) {
        text = str;
    }

    @Override
    public void execute() throws BuildException {
        Context cx = Context.enter();
        try {
            Scriptable scope = cx.initStandardObjects();
            scope.put("task", scope, this);
            scope.put("fileset", scope, toFileList(rclist));
            for (Parameter param : params) {
                scope.put(param.getName(), scope, param.getValue());
            }
            cx.evaluateString(scope, text, "ant.js", 0, null);
        } finally {
            Context.exit();
        }
    }   

    private List<File> toFileList(List<ResourceCollection> rclist) {
        List<File> filelist = new ArrayList<File>(); 
        for (ResourceCollection rc : rclist) {
            for (Iterator<?> ite = rc.iterator(); ite.hasNext();) {
                Resource resource = (Resource) ite.next();
                if (resource instanceof FileResource) {
                    File file = ((FileResource) resource).getFile();
                    filelist.add(file);
                }
            }
        }
        return filelist;
    }
}

使用例:

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<project basedir="." default="test" name="test">
    <taskdef name="js"
        classname="jstask.JsTask"
        classpath="lib/jstask.js;lib/js.jar"
    />
    <target name="test">
        <js>
            <fileset dir="src" includes="**/*.js" />
            <param name="prefix" value="^${basedir}/" />
            <param name="outfile" value="html.txt" />

            <![CDATA[
            var out = new java.io.FileWriter(outfile);
            var ite = fileset.iterator();
            while (ite.hasNext()) {
                var reg = new RegExp(prefix);
                var fname = (""+ite.next()).replace(reg,"");
                out.write("<script src='"+fname+"'></script>\n");
            }
            out.close();
            ]]>
        </js>
    </target>   
</project>

結局これが一番簡単だった...


2013/04/21

Javaで動的に配列を生成するメモ

Javaで任意の型の配列を動的に作る方法を知らない事に気が付いた。

大概は Object[ ] で対処できるので今まで調べたことが無かったっぽい。

結論から言うとこうする。

int[] ary = (int[])java.lang.reflect.Array.newInstance(int.class, 10);

言語仕様上は静的にしか配列の型は決められないので reflect で生成するようになっている。

まぁ、普通使わないよなこんなの...


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

サイト内検索

登録
RSS/2.0

カテゴリ

最近の投稿【java】

リンク

アーカイブ