2015/08/27

Android Studio でコンパイルの前処理

Android Studio の gradle でコンパイル前に処理を入れる方法を調べたメモ。

  • 環境:Android studio 1.3

build.gradle の拡張については意外に情報が少ないです。
公式の資料は以下です。

翻訳は有志の方がされていますが内容はすこし古いです。

やりたいことは アプリバージョン と GitのコミットID を asstes のファイルに埋め込む事です。

src/main/assets/about-template.html:
    Version:@VERSION@ (Commit:@COMMIT_ID@)
        ↓
src/main/assets/about.html:
    Version:1.0.0 (Commit:cc2e3ac008092db2cd46b80336921471e6716af4)

結論としては app/build.gradle に以下を追加することでできました。

import org.apache.tools.ant.filters.ReplaceTokens

def getCommitId() {
    try {
        def stdout = new ByteArrayOutputStream()
        exec {
            commandLine 'git', 'rev-list', 'master', '--max-count', '1'
            standardOutput = stdout
        }
        return stdout.toString().trim();
    }
    catch (Exception e) {
        println e.toString();
        return "";
    }
}
task makeAboutHtml {
    copy {
        from 'src/main/assets/about-template.html'
        into 'src/main/assets/'
        rename(/-template.html$/,".html");
        filter(ReplaceTokens, tokens: [
                VERSION: android.defaultConfig.versionName,
                COMMIT_ID: getCommitId()
        ])
    }
}
android.applicationVariants.all { variant ->
    variant.mergeAssets.dependsOn('makeAboutHtml')
}

補足:

  • git.exe に実行環境でパスが通っている必要があります。
  • applicationVariants はエディタに Cannot resolve symbol と言われますが無視して問題ありません。
    • これでだいぶ悩みました。
  • 本当は build/ に直接コピーしたいのですがまだそこまでやり方がわかりません。

2015/08/17

Node.js と MongoDB の開発環境構築メモ

最近流行りの Node.js と MongoDB の組み合わせで開発環境を作ったメモ。

DLとインストール

Node.js

Node.js の本体。

  • https://nodejs.org/download/
    • Installer を選択
    • Binary は Node.exe だけで npm が無い

インストールはインストーラに従えばOK。

Eclipse プラグイン

nodeclips という プラグインをインストールする。
express というフレームワークも一緒に入る。

  • http://dl.bintray.com/nodeclipse/nodeclipse/
    • とりあえず配下全部

プラグイン、インストール後に環境設定が必要。

注:node.exe以外はデフォルトの設定を使うこと。

MongoDB

MongoDB 本体はDLしてインストールするだけ。 起動方法法は後述。

  • https://www.mongodb.org/downloads
    • OS は 2008 R2+ にした

MongoDB の教本(通称:薄い本)のPDFをDLして一読しておく。

プロジェクトの作成

素の Node.js プロジェクトと Expless フレームワークのプロジェクトが作れるが今回は Express プロジェクトを作成する。
Express は Node.js を HTTP サーバとするためのフレームワーク。

  • メニューから 「新規 > プロジェクト > その他 > ノード > Node Express Project」 を選択。
  • テンプレートエンジンは好きなほうを選択
    • jade はHTML省略記法、ejs はJSPライクな感じ

作成されたプロジェクトの構成は以下

  • public/** : 静的なHTMLファイル
  • routes/** : JSのロジック
  • views/** : テンプレート
  • app.js : アプリのメイン
  • package.json : アプリの定義ファイル

app.js のメニューから 「実行 > Node Application」 でアプリが起動する。
ブラウザから http://localhost:3000 にアクセスして Welcom ページが表示されればOK。

Node.js と MongoDB の接続

MongoDB サーバの起動

mongod.exe を実行するだけでサーバは起動するが WindowsだとDBフォルダのドライブ指定を明示したほうが良いので起動バッチを作成する。 MongoDBのインストールフォルダに置いておく。

boot-mongod.bat: サーバ起動用

set BASEDIR=%~dp0
cd /d %BASEDIR%
%BASEDIR%\Server\3.1\bin\mongod.exe --dbpath %BASEDIR%\DB

clinet-mongo.bat: シェル起動用

set BASEDIR=%~dp0
cd /d %BASEDIR%
%BASEDIR%\Server\3.1\bin\mongo.exe

クライアントのシェルを起動して繋がればOK。

ドライバの準備

ドライバは Node.js のモジュール 'mongodb' を使用する。
'mongoose' とかも有るようだけどとりあえず標準を使う。

package.json の dependencies に "mongodb" を追加する。

{
  "name": "node-ex-test",
  "version": "0.0.1",
  "private": true,
  "scripts": {
    "start": "node app.js"
  },
  "dependencies": {
    "express": "3.2.6",
    "jade": "*",
    "mongodb": "*"     <--これ追加
  }
}

変更後にメニューから「実行 > npm install」を行うとmongodbのドライバがDLされる。
他のモジュールも同じように追加できる。

ドライバのAPIは以下を参照。

接続サンプルコード

MongoDBにコレクション accounts を作成しユーザ登録と認証を行う簡単なサンプル。
POSTのパラメータに user,pass が設定されるAPIとしている。

簡単な仕様:

メソッドURLリクエスト本文レスポンス
登録POST/accounts/registeruser={ユーザ名}&pass={パスワード}200 or 500
認証POST/accounts/loginuser={ユーザ名}&pass={パスワード}200 or 401

routes/accounts.js:

var MASTER_PASSWD = "ce39325bc505e74089f7e176a380370f";
var mongodb = require('mongodb');
var crypto = require('crypto');
var accounts;

mongodb.MongoClient.connect("mongodb://localhost:27017/test", function(err, database) {
    if (err != null) console.error(err);
    accounts = database.collection("accounts");
});

function pass2hash(pass, solt) {
    var hash = crypto.createHash('sha1');
    hash.update(MASTER_PASSWD).update(pass).update(solt);
    return hash.digest('base64');
}

exports.register = function register(req, res) {
    var solt = crypto.createHash('sha1').update(""+ new Date()).digest('base64');
    var hash = pass2hash( req.body.pass, solt);
    accounts.update({name: req.body.name},
        {name: req.body.name, hash: hash, solt:solt},
        {upsert: true},
        function(err, result) {
            if (err == null && result.result.n == 1) {
                console.log(item);
                res.send("OK");
            } else {
                console.error(err);
                res.statusCode = 500;
                res.send("NG");
            }
        }
    );
}

exports.login = function login(req, res) {
    accounts.findOne({name:{$eq: req.body.name}}, function(err,doc){
        if (err != null) console.error(err);
        var isLoginOk = false;
        if (doc != null) {
            var hash = pass2hash(req.body.pass, doc.solt);
            isLoginOk = (hash == doc.hash);
        }
        if (isLoginOk) {
            res.send("OK");
        } else {
            res.statusCode = 401;
            res.send("NG");
        }
    });
}

apps.js:

var express = require('express');
var http = require('http');
var path = require('path');

var app = express();

// all environments
app.set('port', process.env.PORT || 3000);
app.set('views', __dirname + '/views');
app.set('view engine', 'jade');
app.use(express.favicon());
app.use(express.logger('dev'));
app.use(express.bodyParser());
app.use(express.methodOverride());
app.use(app.router);
app.use(express.static(path.join(__dirname, 'public')));

// development only
if ('development' == app.get('env')) {
    app.use(express.errorHandler());
}

// サンプルのマッピング設定
var accounts = require('./routes/accounts');
app.post("/accounts/register", accounts.register);
app.post("/accounts/login", accounts.login);

http.createServer(app).listen(app.get('port'), function() {
    console.log('Express server listening on port ' + app.get('port'));
});

実行結果

ユーザ登録を行ってMongoDBのシェルからDBにデータが登録された事を確認する。

> db.accounts.find();
{ "_id" : ObjectId("55cd9b6e8ca01a0a50ecd707"), "name" : "abc", "hash" : "+1j0H0OdNH/HFGXspXi+hx6mtS8=", "solt" : "RzB+u5FvnOvHRXvtZi85iX9URno=" }
{ "_id" : ObjectId("55d011f4ba68640523bd0271"), "name" : "xxx", "hash" : "wsB0iPj/2BtLHGXWmSFqeGjqwn4=", "solt" : "1o2QK3qqdWSgw1cFQgbIj4DnpBA=" }
{ "_id" : ObjectId("55d0126cba68640523bd0272"), "name" : "yyyy", "hash" : "lHj8Xu6HNrrpjn9w6i9FnWYUwdI=", "solt" : "BI8xyULlX/CAj19/3VonYxehG/o=" }
{ "_id" : ObjectId("55d012bbba68640523bd0273"), "name" : "zzz", "hash" : "xeJTRfFbVaKSI2m6QePMv9laB4E=", "solt" : "NG42owW9EC7Ntu8FAp6NWXLwbBs=" }

つでに name に Index を作成しておく。

> db.accounts.ensureIndex({name:1},{unique:true});
{
        "createdCollectionAutomatically" : false,
        "numIndexesBefore" : 1,
        "numIndexesAfter" : 2,
        "ok" : 1
}
> db.accounts.getIndexes()
[
        {
                "v" : 1,
                "key" : { "_id" : 1 },
                "name" : "_id_",
                "ns" : "test.accounts"
        },
        {
                "v" : 1,
                "unique" : true,
                "key" : { "name" : 1 },
                "name" : "name_1",
                "ns" : "test.accounts"
        }
]

MongoDBはスキーマレスなのでこういう設定をするタイミングはよくわからない。
_id の役割を完全に理解してないが name の代わりに PKEY として使っても良いのかもしれない。

まとめ

Node.js と MongoDB どちらも JavaScript に慣れている人なら違和感なく入れると思う。
思いの他、学習コストは低いのではないだろうか。
Eclipse でデバッグ実行までできるので開発もしやすそう。

MongoDB の「トランザクションが無い」とか「正規化しない」などの特徴は慣れるまでは難しそうだ。


2015/08/08

Android Studio で自動化テスト

Android Studio でテストコードを実行する方法をまとめておく。

  • Android Studio 1.3 が対象。

準備

古い解説サイトを見ると build.gradle の変更が必要と書いてあるが 1.3 では不要。
プロジェクト生成時に src/androidTest フォルダが作成されていてすぐにテストコードが書けるようになっている。

メニューの Run > Edit Configrations を選択して Android Tests を追加すれば準備完了。

テスト実行環境設定で変更する項目は3つ

  • テストの名前
  • Module は通常 App
  • デバイスは選択にしといたほうがよさげ

テストコード

これは Eclipse と変わらない。

単純な Activity テストのサンプル

package org.kotemaru.test1;

import android.test.ActivityUnitTestCase;
import android.util.Log;
import android.view.View;
import android.widget.TextView;

public class MainActivityTest extends ActivityUnitTestCase<MainActivity> {

    public MainActivityTest(Class<MainActivity> activityClass) {
        super(activityClass);
    }

    // ボタンクリックのテスト
    public void test_onTestClick() throws Exception {
        Log.v("test", "test_onTestClick");
        assertEquals("Hello world!", getText(R.id.textView));
        click(R.id.testButton);
        assertEquals("Test message!", getText(R.id.textView));
    }

    //-----------------------------------------------------------------
    // Util
    private void click(int rid) {
        final View view = getActivity().findViewById(rid);
        getInstrumentation().runOnMainSync(new Runnable() {
            @Override
            public void run() {
                view.performClick();
            }
        });
    }

    private CharSequence getText(int rid) {
        return ((TextView) getActivity().findViewById(rid)).getText();
    }
}

複数の Activity 遷移のサンプル

package org.kotemaru.test1;

import android.app.Activity;
import android.app.Instrumentation;
import android.content.Intent;
import android.test.ActivityTestCase;
import android.test.ActivityUnitTestCase;
import android.util.Log;
import android.view.KeyEvent;
import android.view.View;
import android.widget.TextView;

public class MultiActivityTest extends ActivityTestCase {
    @Override
    protected void setUp() throws Exception {
        super.setUp();
        Intent intent = new Intent(Intent.ACTION_MAIN);
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        intent.setClassName("org.kotemaru.test1", MainActivity.class.getName());
        Activity act = getInstrumentation().startActivitySync(intent);
        setActivity(act);
    }

    /**
     * Main->Sub->Main の Activity 遷移のテスト
     */
    public void test_onSubActivityClick() throws Exception {
        Log.v("test", "test_onSubActivityClick");
        Instrumentation instrumentation = getInstrumentation();
        Instrumentation.ActivityMonitor monitor =
                instrumentation.addMonitor(SubActivity.class.getCanonicalName(), null, false);

        click(R.id.subActivityButton);
        Activity act = instrumentation.waitForMonitorWithTimeout(monitor, 10000);
        assertEquals(SubActivity.class, act.getClass());

        monitor = instrumentation.addMonitor(MainActivity.class.getCanonicalName(), null, false);
        Thread.sleep(1000); // TODO: wait activity resumed
        sendKeys(KeyEvent.KEYCODE_BACK);

        act = instrumentation.waitForMonitorWithTimeout(monitor, 10000);
        assertEquals(MainActivity.class, act.getClass());
    }

    //-----------------------------------------------------------------
    // Util
    private void click(int rid) {
        final View view = getActivity().findViewById(rid);
        getInstrumentation().runOnMainSync(new Runnable() {
            @Override
            public void run() {
                view.performClick();
            }
        });
    }

    private CharSequence getText(int rid) {
        return ((TextView) getActivity().findViewById(rid)).getText();
    }
}

実行

実行の種類を test に変更して実行するだけ。

デバイスの選択画面がでるのでエミュレータか実機を選択。

エミュレータで自動でテストが進む。

失敗するとスタックトレースがでるので当該の assert に飛ぶことができる。

まとめ

余計な手間がなく目的のテストコードだけ書けば良いようになっている。

操作系のユーティリティが必要だなと思ったら robotium というのが既にあるらしい。

問題は UI の仕様変更は頻繁なのでテストが追従できないことなんだよねー。


2015/08/05

Android Studio のカスタマイズ

ADT 終了のお知らせが Google から出たので仕方なく Android Studio を使ってみている。
いろいろ操作に戸惑うのでカスタマイズをメモ。

お勧めのエラー対処方の表示

Eclipseではマウスオーバーだが Studio では ALT+Enter。
マウスオーバーはキーバインドできないので中ボタンに割り当てる。

  1. Settings の Keymap へ移動
  2. 「Show Intention Actions」を検索
  3. 鉛筆のメニューから「Add Mouse shortcut」を選択して設定ダイアログ
  4. マウスアイコンの上で中ボタンをクリック

エディタ設定

行番号と空白の表示

デフォルトで行番号も空白も表示されないのは Eclipse と同じ。
改行の表示方法が不明。

  1. Settings の Editor > General > Apprearaaance へ移動
  2. 「Show line numers」と「Show white space」をチェック

フォントの変更

スキーマを作らないと変更できない?
Windows10 だと Source code pro が選択できたので設定しておいた。

  1. Settings の Editor > Colors&Fonts > Font へ移動
  2. 「Save As...」ボタンで新しいスキーマを作る。
  3. 「Show only monospaced fonts」をチェックしてフォントを選ぶ。
  4. 「Line Spaceng」はフォントにより 0.8~0.9 くらいが良い

コンテキストメニュー

エディタの右クリックメニューによく使う機能だけを割り当てた。
メニューの編集機能が恐ろしく使いずらい。

  1. Settings の Appearance & Behavior > Menus and Toolbars へ移動
  2. 「Editor Popup Menu」を開いて内容を右側のボタンで編集

作ったメニュー

Declaration
Call hierarchy
Super Method
Implements
---
Generate
Toggle Case
Rename
---
Cut
Copy
Peast

削除した項目の「Column Selection mode」は Toolar の Paste の後ろに追加する。

参考、機能一覧:http://gihyo.jp/dev/serial/01/android_studio/0022

ツールバーのアイコン

ツールバーをカスタマイズすると 16x16 のアイコンが必要になる。 指定しないと意味不明なデフォルトアイコンが設定されてしまう。

おすすめのフリーのアイコンセット。16x16 で500種類以上ある。

フラットではないので若干違和感あるけど編集アプリ向けのアイコンが充実している。。

コード・フォーマッタ

Eclipse Code Formatter プラグインを入れて Eclipse の設定を流用する。
移行プロジェクトだと差分出ちゃうから必須。

  1. settings の Plugins へ移動。
  2. 「Browse repositories」ボタンをクリック
  3. 「Eclipse Code Formatter」を検索してインストール
  4. settings の Other Settings > Eclipse Code Formatter へ移動
  5. 「Use the Eclipse code fomtter」をチェック
  6. 「Eclipse Java formatter config file」にエクスポートした定義ファイルを設定する。

GitHub

  1. git.exe をインストールしてパスを設定する。
  2. gitbub のユーザとパスワードを設定する。
  3. メニューから「Create git repository」「Share project on github」の順に実行する。

その他

使ってると色々出てくるから都度追加。

雑感

途中で気が付いたのでが Studio って Android 以外のプロジェクトって作れないのね。
GAE のサポートはあるみたいだけど Tomcat は結局 Eclipse が必要。
IntelliJ も無料版はサーバ開発できないみたいだし。

Eclipse+Gradle ではダメだったんだろうか?


2015/05/31

Androidの動画ストリーミング配信

思う所有ってAndroidで動画のストリーミング配信を受ける方法を調べで見た。

ストリーミング配信のプロトコル

ストリーミング配信で使われるプロトコルとAndroidの対応状況は以下となる。

プロトコルAndroidサポート
RTSP
RTMPライブラリ(Vitamio)
HLSAndroid/3.0以降

Vitamio は個人利用はフリーで法人は有料。 完全にフリーなライブラリもあるかもしれないが調べていない。

サーバ

フリーのサーバは2つ見つけた。

  • Darwin Streaming Server
    • Apple の QuickTime 用のサーバが OSS になったものらしい。
  • Red5
    • Javaで記述されている。
    • ニコ動でも採用されているらしい。

最初、Darwin を試したのだがなんと Yosemite では動かないらしい。 Ubuntu でビルドしてみたが途中で失敗する。

諦めて Red5 を試してみる。 こちらはあっさり動作した。 但し、サイト引越し中なのか情報源のリンク切れが多くちょっと困った。今回はこちらを採用。

Red5 のインストール

以下から最新のバイナリをDLする。

ZIP を展開してできたフォルダの red5.bat を実行する。
これでサーバは起動する。
バッチでエラーが出る場合は、red5.bat に以下の修正を入れてみる。

"%JAVA_HOME%\bin\java" %JAVA_OPTS% -cp "%RED5_CLASSPATH%" %RED5_MAINCLASS% %RED5_OPTS%
     ↓
%JAVA_HOME%\bin\java %JAVA_OPTS% -cp "%RED5_CLASSPATH%" %RED5_MAINCLASS% %RED5_OPTS%

ファイヤーウォールのポート使用確認ダイアログが出たら許可する。

Red5デモアプリの準備

Red5 起動後、ブラウザで http://localhost:5080/ を開くと以下の画面が表示されるので下の方に有る

  • Install a ready-made application

をクリックする。

アプリケーションの一覧から OFLA Demo を選択し「Install」をクリックする。

デモアプリインストール後にブラウザで http://localhost:5080/oflaDemo/ を開くとデモの動画が再生される。

これでサーバ側の準備は完了。

動画を追加する場合には webapps/アプリ/streams の配下に置くだけで良い。

Androidクライアントアプリ

Red5 はプラグインを入れないと RTMP しかサポートしないので Vitamio ライブラリを使用する事にした。

Vitamio は初期化以外は標準の VideoView/MediaPlayer と互換 API なので非常に扱いやすい。
ライブラリ・プロジェクトは以下から取得できる。

Android studio 用のプロジェクトなので Eclipse の場合は自力で変換する必要がある。
簡単に方法を書いておくと

  • 上記リポジトリをZIPでDLしてくる。
  • Android ライブラリプロジェクトを新規作成。
  • ZIPから src,res,libs,AndroidManifest.xml,proguard-project.txt,project.properties をコピー
  • 新規作成の時に自動生成された不要ファイルを削除。

で行けるはず。

実装コードは以下のようになる。

MainActivity.java:
package org.kotemaru.android.rtmpclient;

import io.vov.vitamio.LibsChecker;
import io.vov.vitamio.MediaPlayer;
import io.vov.vitamio.MediaPlayer.OnBufferingUpdateListener;
import io.vov.vitamio.MediaPlayer.OnInfoListener;
import io.vov.vitamio.widget.MediaController;
import io.vov.vitamio.widget.VideoView;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.ProgressBar;

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

    private VideoView mVideoView;
    private PlayerListener mPlayerListener;
    private ProgressBar mProgressBar;
    private long mPosition = 0; // ms

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        if (!LibsChecker.checkVitamioLibs(this)) return; // ※Vitamio で必須

        setContentView(R.layout.activity_main);

        mVideoView = (VideoView) findViewById(R.id.videoView);
        mProgressBar = (ProgressBar) findViewById(R.id.progressBar);
        mPlayerListener = new PlayerListener();

        mVideoView.setOnInfoListener(mPlayerListener);
        mVideoView.setOnBufferingUpdateListener(mPlayerListener);
        mVideoView.setMediaController(new MediaController(this));

        if (savedInstanceState != null) {
            mPosition = savedInstanceState.getLong(KEY_POSITION);
        }
    }

    @Override
    protected void onNewIntent(Intent intent) {
        super.onNewIntent(intent);
        setIntent(intent);
    }

    @Override
    protected void onResume() {
        super.onResume();
        Intent intent = getIntent();
        String path = intent.getDataString();
        Log.d("DEBUG", "path=" + path);
        if (path == null) return;
        mVideoView.setVideoPath(path);
        mVideoView.requestFocus();
        mVideoView.seekTo(mPosition);
        mVideoView.start();
    }

    @Override
    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        outState.putLong(KEY_POSITION, mVideoView.getCurrentPosition());
    }

    private class PlayerListener implements OnInfoListener, OnBufferingUpdateListener {
        @Override
        public boolean onInfo(MediaPlayer mp, int what, int extra) {
            switch (what) {
            case MediaPlayer.MEDIA_INFO_BUFFERING_START:
                if (mVideoView.isPlaying()) {
                    mVideoView.pause();
                    mProgressBar.setVisibility(View.VISIBLE);
                }
                break;
            case MediaPlayer.MEDIA_INFO_BUFFERING_END:
                mVideoView.start();
                mProgressBar.setVisibility(View.INVISIBLE);
                break;
            case MediaPlayer.MEDIA_INFO_DOWNLOAD_RATE_CHANGED:
                break;
            }
            return true;
        }
        @Override
        public void onBufferingUpdate(MediaPlayer mp, int percent) {
            Log.d("DEBUG", "buff=" + percent);
        }
    }
}
AndroidManifest.xml:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="org.kotemaru.android.rtmpclient" android:versionCode="1" android:versionName="1.0">

    <uses-sdk android:minSdkVersion="14" android:targetSdkVersion="15" />
    <uses-permission android:name="android.permission.WAKE_LOCK" />
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <application android:allowBackup="true" android:icon="@drawable/ic_launcher"
        android:label="@string/app_name" android:theme="@style/AppTheme">
        <activity android:name=".MainActivity" android:label="@string/app_name" android:launchMode="singleTop">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
            <intent-filter>
                <action android:name="android.intent.action.VIEW" />
                <category android:name="android.intent.category.DEFAULT" />
                <category android:name="android.intent.category.BROWSABLE" />
                <data android:scheme="rtmp" />
            </intent-filter>
        </activity>
        <!-- ※Vitamio で必須 -->
        <activity android:name="io.vov.vitamio.activity.InitActivity"
            android:configChanges="orientation|screenSize|smallestScreenSize|keyboard|keyboardHidden|navigation"
            android:launchMode="singleTop" android:theme="@android:style/Theme.NoTitleBar"
            android:windowSoftInputMode="stateAlwaysHidden" />
    </application>
</manifest>

特殊なのは LibsChecker.checkVitamioLibs() の呼び出しと io.vov.vitamio.activity.InitActivity の宣言が必要になることだけ。

実行

アプリ起動用のHTMLファイルを用意する。

red5-server-1.0.5-RELEASE/webapps/oflaDemo/test.html:
<ul>
<li><a href="rtmp://192.168.0.9/oflaDemo/Avengers2.mp4">Avengers2</a>
<li><a href="rtmp://192.168.0.9/oflaDemo/test.mp4">test</a>
<li><a href="rtmp://192.168.0.9/oflaDemo/test2.mp4">test2</a>
</ul>
  • 192.168.0.9 は Red5 のIPアドレス。

実行結果

Androidのブラウザから http://192.168.0.9:5080/oflaDemo/test.html を開く。

「Avengers2」をタップすると再生アプリが起動する。

…>

ちょっと待たされて再生成功。

所感

まずサーバの準備が思いの外大変。
Darwin が動かなくて困ったが結果的には Red5 の方が自分で手を入れられて使い勝手良さそう。

それと、プロトコルが色々有ると思っていなかった。
RTSP は時代遅れっぽいが HLS はスマホの主流と目されているようなので次は HLS を試してみたいなーと思っている。


2015/05/23

AndroidのCamera2 APIのサンプル

カメラの実装をしようとしたら android.hardware.Camera が Deprecated だって怒られた。
android.hardware.camera2 と言うのが API Level 21 から出来ていてこちらを使えとのことらしい。
でも Level 21 って、Android/5.0 以上だよね。
いきなり 4.x を Deprecated にするとか Google 強気すぎるだろw

5.0が普及するまで使われる事は無いと思うけど使い方だけも確認しておこうと思い調べてみた。

参考にしたのはGoogle製の以下のサンプル。

毎度の事ながらGoogleのサンプルはスパゲティで訳がわかりません。
サンプルを自分で書き起こす事にしました。

旧Camera API との違い

全部違いますw
とりあえず要点は以下

  • プレビューは SafaceView でなく TextureView を使う。
  • 撮影した画像は android.media.ImageReader 経由で取得する。
  • オートフォーカスや露出はアプリ側がプレビューをハンドリングしながら行う。

旧APIは最初にパラメータ設定して後はお任せな感じでしたが Camera2 API はアプリ側のロジックでカメラを制御していく感じです。 細かな制御ができるようになった分、アプリが複雑になった印象です。

尚、シャッタースピードとか感度設定の新機能は今回試して無いのでよくわかりません。

Camera2 API はコールバック地獄

Camera2 API ではカメラの初期化に数ステップ、 さらに撮影で数ステップのコールバックによる非同期処理が必要になります。

整理すると以下のようになります。

  • 初期化
    • TextureView の準備待
    • カメラデバイス準備待
    • キャプチャセッション接続待
  • 撮影
    • オートフォーカス待
    • オート露出待
    • 画像取得待

そのまま実装するといわいるコールバック地獄になります。
Googleのサンプルコードからこれを読み取るのに1日かかりました。

Camera2 API のオリジナルのサンプル

Googleのサンプルを元に最小構成のサンプルを独自に作ってみます。
簡略化のため以下の機能を削ります。

  • バックグラウンドスレッドと排他制御(全部UIスレッド)
  • 画面回転対応(縦置固定)

状態遷移マシン

コールバック地獄回避のため状態遷移を導入します。
Googleのサンプルから読み取った各状態を整理した物が以下になります。

状態遷移図:

状態説明:
状態名説明
InitSafaceTextureViewの初期化。
- Safaceとして使えるようになるのを待ます。
- 初期化済の場合はスルー。
OpenCameraカメラデバイスの開始。
- カメラの情報から画像サイズ、プレビューサイズを決定し ImageReaderやTexcureViewの設定を行います。
CreateSessionキャプチャセッションの接続。(補足後述)
Previewプレビュー表示中。ここが安定状態となります。
AutoFocusオートフォーカス(AF)中。
- AF要求を発行して焦点が合うまでコールバックを受け続けます。
AutoExposure自動露出(AE)調整中。
- AE要求を発行して露出が合うまでコールバックを受け続けます。
TakePicture画像取得中。
- 撮影要求を発行して完了を待ちます。
- 結果画像は ImageReader のコールバックに返されます。
Abortカメラの終了。全てのリソースを開放します。

キャプチャセッションとは Camera2 API の新しい仕組みでカメラから画像を受け取る為のAPIです。 これに各種要求を発行して結果をコールバックで貰うのが基本パターンになります。
詳細は CameraCaptureSessionクラス のドキュメントを参照してください。

イベント説明:
イベント名説明
open()カメラの利用開始。通常 onResume() から呼ばれる。
close()カメラの利用終了。通常 onParse() から呼ばれる。
takePicture()撮影開始。通常シャッターの onClick() から呼ばれる。

サンプルの実装

完全なソースは GitHub にあります。

Camera2StateMachine.java:

上記、状態遷移図をそのまま実装しただけですが 300行になりました。
これでもカメラとして機能するための必要最小限です。

// Copyright 2015 kotemaru.org. (http://www.apache.org/licenses/LICENSE-2.0)
package org.kotemaru.android.camera2sample;

import java.util.Arrays;
import java.util.List;

import android.app.Activity;
import android.content.Context;
import android.graphics.ImageFormat;
import android.graphics.SurfaceTexture;
import android.hardware.camera2.CameraAccessException;
import android.hardware.camera2.CameraCaptureSession;
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.CameraDevice;
import android.hardware.camera2.CameraManager;
import android.hardware.camera2.CameraMetadata;
import android.hardware.camera2.CaptureRequest;
import android.hardware.camera2.CaptureResult;
import android.hardware.camera2.TotalCaptureResult;
import android.hardware.camera2.params.StreamConfigurationMap;
import android.media.ImageReader;
import android.os.Handler;
import android.util.Log;
import android.util.Size;
import android.view.Surface;
import android.view.TextureView;

public class Camera2StateMachine {
    private static final String TAG = Camera2StateMachine.class.getSimpleName();
    private CameraManager mCameraManager;

    private CameraDevice mCameraDevice;
    private CameraCaptureSession mCaptureSession;
    private ImageReader mImageReader;
    private CaptureRequest.Builder mPreviewRequestBuilder;

    private AutoFitTextureView mTextureView;
    private Handler mHandler = null; // default current thread.
    private State mState = null;
    private ImageReader.OnImageAvailableListener mTakePictureListener;

    public void open(Activity activity, AutoFitTextureView textureView) {
        if (mState != null) throw new IllegalStateException("Alrady started state=" + mState);
        mTextureView = textureView;
        mCameraManager = (CameraManager) activity.getSystemService(Context.CAMERA_SERVICE);
        nextState(mInitSurfaceState);
    }
    public boolean takePicture(ImageReader.OnImageAvailableListener listener) {
        if (mState != mPreviewState) return false;
        mTakePictureListener = listener;
        nextState(mAutoFocusState);
        return true;
    }
    public void close() {
        nextState(mAbortState);
    }

    // ----------------------------------------------------------------------------------------
    // The following private
    private void shutdown() {
        if (null != mCaptureSession) {
            mCaptureSession.close();
            mCaptureSession = null;
        }
        if (null != mCameraDevice) {
            mCameraDevice.close();
            mCameraDevice = null;
        }
        if (null != mImageReader) {
            mImageReader.close();
            mImageReader = null;
        }
    }

    private void nextState(State nextState) {
        Log.d(TAG, "state: " + mState + "->" + nextState);
        try {
            if (mState != null) mState.finish();
            mState = nextState;
            if (mState != null) mState.enter();
        } catch (CameraAccessException e) {
            Log.e(TAG, "next(" + nextState + ")", e);
            shutdown();
        }
    }

    private abstract class State {
        private String mName;

        public State(String name) {
            mName = name;
        }
        //@formatter:off
        public String toString() {return mName;}
        public void enter() throws CameraAccessException {}
        public void onSurfaceTextureAvailable(int width, int height){}
        public void onCameraOpened(CameraDevice cameraDevice){}
        public void onSessionConfigured(CameraCaptureSession cameraCaptureSession) {}
        public void onCaptureResult(CaptureResult result, boolean isCompleted) throws CameraAccessException {}
        public void finish() throws CameraAccessException {}
        //@formatter:on
    }

    // ===================================================================================
    // State Definition
    private final State mInitSurfaceState = new State("InitSurface") {
        public void enter() throws CameraAccessException {
            if (mTextureView.isAvailable()) {
                nextState(mOpenCameraState);
            } else {
                mTextureView.setSurfaceTextureListener(mSurfaceTextureListener);
            }
        }
        public void onSurfaceTextureAvailable(int width, int height) {
            nextState(mOpenCameraState);
        }

        private final TextureView.SurfaceTextureListener mSurfaceTextureListener = new TextureView.SurfaceTextureListener() {
            @Override
            public void onSurfaceTextureAvailable(SurfaceTexture texture, int width, int height) {
                if (mState != null) mState.onSurfaceTextureAvailable(width, height);
            }
            @Override
            public void onSurfaceTextureSizeChanged(SurfaceTexture texture, int width, int height) {
                // TODO: ratation changed.
            }
            @Override
            public boolean onSurfaceTextureDestroyed(SurfaceTexture texture) {
                return true;
            }
            @Override
            public void onSurfaceTextureUpdated(SurfaceTexture texture) {
            }
        };
    };
    // -----------------------------------------------------------------------------------
    private final State mOpenCameraState = new State("OpenCamera") {
        public void enter() throws CameraAccessException {
            String cameraId = Camera2Util.getCameraId(mCameraManager, CameraCharacteristics.LENS_FACING_BACK);
            CameraCharacteristics characteristics = mCameraManager.getCameraCharacteristics(cameraId);
            StreamConfigurationMap map = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);

            mImageReader = Camera2Util.getMaxSizeImageReader(map, ImageFormat.JPEG);
            Size previewSize = Camera2Util.getBestPreviewSize(map, mImageReader);
            mTextureView.setPreviewSize(previewSize.getHeight(), previewSize.getWidth());

            mCameraManager.openCamera(cameraId, mStateCallback, mHandler);
            Log.d(TAG, "openCamera:" + cameraId);
        }
        public void onCameraOpened(CameraDevice cameraDevice) {
            mCameraDevice = cameraDevice;
            nextState(mCreateSessionState);
        }

        private final CameraDevice.StateCallback mStateCallback = new CameraDevice.StateCallback() {
            @Override
            public void onOpened(CameraDevice cameraDevice) {
                if (mState != null) mState.onCameraOpened(cameraDevice);
            }
            @Override
            public void onDisconnected(CameraDevice cameraDevice) {
                nextState(mAbortState);
            }
            @Override
            public void onError(CameraDevice cameraDevice, int error) {
                Log.e(TAG, "CameraDevice:onError:" + error);
                nextState(mAbortState);
            }
        };
    };
    // -----------------------------------------------------------------------------------
    private final State mCreateSessionState = new State("CreateSession") {
        public void enter() throws CameraAccessException {
            mPreviewRequestBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
            SurfaceTexture texture = mTextureView.getSurfaceTexture();
            texture.setDefaultBufferSize(mTextureView.getPreviewWidth(), mTextureView.getPreviewHeight());
            Surface surface = new Surface(texture);
            mPreviewRequestBuilder.addTarget(surface);
            List<Surface> outputs = Arrays.asList(surface, mImageReader.getSurface());
            mCameraDevice.createCaptureSession(outputs, mSessionCallback, mHandler);
        }
        public void onSessionConfigured(CameraCaptureSession cameraCaptureSession) {
            mCaptureSession = cameraCaptureSession;
            nextState(mPreviewState);
        }

        private final CameraCaptureSession.StateCallback mSessionCallback = new CameraCaptureSession.StateCallback() {
            @Override
            public void onConfigured(CameraCaptureSession cameraCaptureSession) {
                if (mState != null) mState.onSessionConfigured(cameraCaptureSession);
            }
            @Override
            public void onConfigureFailed(CameraCaptureSession cameraCaptureSession) {
                nextState(mAbortState);
            }
        };
    };
    // -----------------------------------------------------------------------------------
    private final State mPreviewState = new State("Preview") {
        public void enter() throws CameraAccessException {
            mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
            mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH);
            mCaptureSession.setRepeatingRequest(mPreviewRequestBuilder.build(), mCaptureCallback, mHandler);
        }
    };
    private final CameraCaptureSession.CaptureCallback mCaptureCallback = new CameraCaptureSession.CaptureCallback() {
        @Override
        public void onCaptureProgressed(CameraCaptureSession session, CaptureRequest request, CaptureResult partialResult) {
            onCaptureResult(partialResult, false);
        }
        @Override
        public void onCaptureCompleted(CameraCaptureSession session, CaptureRequest request, TotalCaptureResult result) {
            onCaptureResult(result, true);
        }
        private void onCaptureResult(CaptureResult result, boolean isCompleted) {
            try {
                if (mState != null) mState.onCaptureResult(result, isCompleted);
            } catch (CameraAccessException e) {
                Log.e(TAG, "handle():", e);
                nextState(mAbortState);
            }
        }
    };
    // -----------------------------------------------------------------------------------
    private final State mAutoFocusState = new State("AutoFocus") {
        public void enter() throws CameraAccessException {
            mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_START);
            mCaptureSession.setRepeatingRequest(mPreviewRequestBuilder.build(), mCaptureCallback, mHandler);
        }
        public void onCaptureResult(CaptureResult result, boolean isCompleted) throws CameraAccessException {
            Integer afState = result.get(CaptureResult.CONTROL_AF_STATE);
            boolean isAfReady = afState == null
                    || afState == CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED
                    || afState == CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED;
            if (isAfReady) {
                nextState(mAutoExposureState);
            }
        }
    };
    // -----------------------------------------------------------------------------------
    private final State mAutoExposureState = new State("AutoExposure") {
        public void enter() throws CameraAccessException {
            mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER,
                    CameraMetadata.CONTROL_AE_PRECAPTURE_TRIGGER_START);
            mCaptureSession.setRepeatingRequest(mPreviewRequestBuilder.build(), mCaptureCallback, mHandler);
        }
        public void onCaptureResult(CaptureResult result, boolean isCompleted) throws CameraAccessException {
            Integer aeState = result.get(CaptureResult.CONTROL_AE_STATE);
            boolean isAeReady = aeState == null
                    || aeState == CaptureResult.CONTROL_AE_STATE_CONVERGED
                    || aeState == CaptureRequest.CONTROL_AE_STATE_FLASH_REQUIRED;
            if (isAeReady) {
                nextState(mTakePictureState);
            }
        }
    };
    // -----------------------------------------------------------------------------------
    private final State mTakePictureState = new State("TakePicture") {
        public void enter() throws CameraAccessException {
            final CaptureRequest.Builder captureBuilder =
                    mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE);
            captureBuilder.addTarget(mImageReader.getSurface());
            captureBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
            captureBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH);
            captureBuilder.set(CaptureRequest.JPEG_ORIENTATION, 90); // portraito
            mImageReader.setOnImageAvailableListener(mTakePictureListener, mHandler);

            mCaptureSession.stopRepeating();
            mCaptureSession.capture(captureBuilder.build(), mCaptureCallback, mHandler);
        }
        public void onCaptureResult(CaptureResult result, boolean isCompleted) throws CameraAccessException {
            if (isCompleted) {
                nextState(mPreviewState);
            }
        }
        public void finish() throws CameraAccessException {
            mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_CANCEL);
            mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH);
            mCaptureSession.capture(mPreviewRequestBuilder.build(), mCaptureCallback, mHandler);
            mTakePictureListener = null;
        }
    };
    // -----------------------------------------------------------------------------------
    private final State mAbortState = new State("Abort") {
        public void enter() throws CameraAccessException {
            shutdown();
            nextState(null);
        }
    };
}
Camera2Util.java:

ツール化できそうな部分を分離しました。

package org.kotemaru.android.camera2sample;

import android.graphics.SurfaceTexture;
import android.hardware.camera2.CameraAccessException;
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.CameraManager;
import android.hardware.camera2.params.StreamConfigurationMap;
import android.media.ImageReader;
import android.util.Size;

public class Camera2Util {
    public static String getCameraId(CameraManager cameraManager, int facing) throws CameraAccessException {
        for (String cameraId : cameraManager.getCameraIdList()) {
            CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(cameraId);
            if (characteristics.get(CameraCharacteristics.LENS_FACING) == facing) {
                return cameraId;
            }
        }
        return null;
    }

    public static ImageReader getMaxSizeImageReader(StreamConfigurationMap map, int imageFormat) throws CameraAccessException {
        Size[] sizes = map.getOutputSizes(imageFormat);
        Size maxSize = sizes[0];
        for (Size size:sizes) {
            if (size.getWidth() > maxSize.getWidth()) {
                maxSize = size;
            }
        }
        ImageReader imageReader = ImageReader.newInstance(
                //maxSize.getWidth(), maxSize.getHeight(), // for landscape.
                maxSize.getHeight(), maxSize.getWidth(), // for portrait.
                imageFormat, /*maxImages*/1);
        return imageReader;
    }

    public static Size getBestPreviewSize(StreamConfigurationMap map, ImageReader imageSize) throws CameraAccessException {
        //float imageAspect = (float) imageSize.getWidth() / imageSize.getHeight(); // for landscape.
        float imageAspect = (float) imageSize.getHeight() / imageSize.getWidth(); // for portrait
        float minDiff = 1000000000000F;
        Size[] previewSizes = map.getOutputSizes(SurfaceTexture.class);
        Size previewSize = previewSizes[0];
        for (Size size : previewSizes) {
            float previewAspect = (float) size.getWidth() / size.getHeight();
            float diff = Math.abs(imageAspect - previewAspect);
            if (diff < minDiff) {
                previewSize = size;
                minDiff = diff;
            }
            if (diff == 0.0F) break;
        }
        return previewSize;
    }
}
MainActivity.java:

状態遷移マシンを使用したカメラActivityです。
撮影した画像を ImageView で表示します。

package org.kotemaru.android.camera2sample;

import java.nio.ByteBuffer;

import android.app.Activity;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.media.Image;
import android.media.ImageReader;
import android.os.Bundle;
import android.view.KeyEvent;
import android.view.View;
import android.widget.ImageView;

public class MainActivity extends Activity {
    private AutoFitTextureView mTextureView;
    private ImageView mImageView;
    private Camera2StateMachine mCamera2;

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

        mTextureView = (AutoFitTextureView) findViewById(R.id.TextureView);
        mImageView = (ImageView) findViewById(R.id.ImageView);
        mCamera2 = new Camera2StateMachine();
    }

    @Override
    protected void onResume() {
        super.onResume();
        mCamera2.open(this, mTextureView);
    }
    @Override
    protected void onPause() {
        mCamera2.close();
        super.onPause();
    }
    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {
        if (keyCode == KeyEvent.KEYCODE_BACK && mImageView.getVisibility() == View.VISIBLE) {
            mTextureView.setVisibility(View.VISIBLE);
            mImageView.setVisibility(View.INVISIBLE);
            return false;
        }
        return super.onKeyDown(keyCode, event);
    }
    public void onClickShutter(View view) {
        mCamera2.takePicture(new ImageReader.OnImageAvailableListener() {
            @Override
            public void onImageAvailable(ImageReader reader) {
                // 撮れた画像をImageViewに貼り付けて表示。
                final Image image = reader.acquireLatestImage();
                ByteBuffer buffer = image.getPlanes()[0].getBuffer();
                byte[] bytes = new byte[buffer.remaining()];
                buffer.get(bytes);
                Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
                image.close();

                mImageView.setImageBitmap(bitmap);
                mImageView.setVisibility(View.VISIBLE);
                mTextureView.setVisibility(View.INVISIBLE);
            }
        });
    }
}

実行結果

Nexus5 では問題なく実行できました。
分かりづらいですが左がプレビュー中で右が撮影した画像をImageViewで表示しているところです。

プレビュー中 撮影結果表示中

所感

最初は戸惑いましたが構造が分かってくるとこちらの方が扱い易い気がしてきました。
アプリは複雑になりますがカメラ・デバイスを完全に制御できる感じです。

機種依存問題が解決されるのか否かは 5.0 対応機がもう少し増えないと判断できませんが期待したいところです。


2015/05/18

iOSとAndroidのライフサイクル比較

iOSとAndroidの差異で一番気になっていたライフサイクルの違いを調べてみました。

とりあえず意味的に近そうな主要メソッドの対応表です。

漠然と AppDelegate=Application、ViewController=Activity と考えていたのですが 整理して見ると役割がだいぶ違うようです。

iOS では素直に AppDelegate=アプリ、ViewController=画面制御 と考えて良さそうですが、 Android では Application の役割が希薄で Activity=サブアプリ+画面制御 になっています。 ViewController は Fragment として考えたほうが良いのかもしれません。

モジュールの対比はこんな感じでしょうか?

iOS→Android の移植の場合には AppDelegate=Activity、ViewController=Fragment とするとシンプルに行きそうです。

Android→iOS の移植は難しそうです。複数の Activity を持つアプリの場合、 単純にマッピングできないのでケースバイケースで対応を考えるしかなさそうです。

Android は iOS をもっとパクっていると思っていたのですが基本的な考え方から異なるようですね。 アプリの根幹の部分の差異なので共通アプリを作る場合には設計時点で考慮しておく必要がありそうです。

所感

こんな情報はネットにいくらでも転がってると思っていたんだけど以外に見つからない。 iOSとAndroidの相互移植は頻繁に発生してると思うんだが...

思う所有って iOS の勉強を始めたのだが iOS の開発ってハードル高いよね。
まず Mac 買わなきゃいけない。 勉強目的なので Mac book は予算オーバーで mini を購入。

しかも、ユーザにはメモリ拡張すらさせないボッタクリ。 ますますアップルが嫌いになりますw


2015/04/10

Android-x86-4.4r2 の設定メモ

Android-x86とVirtualBoxを使った開発用の環境の設定方法メモです。
以前 4.0 で作ったのですが 4.4 になったらだいぶ変わっていたので備忘録として残します。

画面のバランスこんな感じです。 (モニタは 1920x1200/22inch、端末は6inchくらいに見えます。)

標準のエミュレータと同じように使えますがこちらは実機並にサクサク動きます。

VirtualBox

ダウンロード

インストール

  • 普通に .exe 叩くだけ。
  • Extension Pack は VirtualBox にリモートデスクトップ接続するときに必要。
    • 「ファイル」->「環境設定」->「拡張機能」 からインストールする。

Android-x86

ダウンロード

インストール

  • VirtualBox で仮想マシンを作る。
    • OSは Other Linux(32bit)
    • HDDは最低2G
    • メモリは512M~1G
    • ネットワークはブリッジ
    • リモートディスプレイを有効
  • android-x86-4.4-r2.iso をCDにマウントして起動。
    • インストーラでインストール。

ネットワーク設定

固定IPを Android-x86 に設定する。

  • Android-x86 をデバックモードで起動
  • /system/etc/init.sh の最後に以下を追加。
    • 最後といっても return 0 の行の前。
ifconfig eth0 {ip-addr} netmask {netmask}
route add default gw {gateway-ip-addr} dev eth0
setprop net.eth0.dns1 {dns-ip-addr}
  • プロキシが必要な場合の設定
sqlite3 /data/data/com.android.providers.settings/databases/settings.db
INSERT INTO system VALUES(99, 'http_proxy', ':');

縦画面モード設定

開発時に適当なサイズとなる 270x480 ピクセルを設定。

  • VirtualBox に画面モード追加
C:> VBoxManage.exe setextradata {VM name} CustomVideoMode1 270x480x16
  • Android をデバックモードで起動
    • /mnt/grub/menu.lst を編集して kernel のオプションに以下を追加
video=-16 UVESA_MODE=240x480
  • DPIを120に設定
C:> adb connect {ip-addr}
C:> adb shell wm density 120

Eclipse との接続

  • VM 起動後に以下のコマンドを実行
C:> adb disconnect {ip-addr}
C:> adb connect {ip-addr}
  • DDMS が自動的に認識してくれる。

その他

  • Android-x86 は VirtualBox のマウス統合が使えないので結構不便。
  • リモートデスクトップ接続すると解消される。
    • 但し、描画は遅くなる。

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

サイト内検索

登録
RSS/2.0

カテゴリ

最近の投稿

リンク

アーカイブ