ラベル Android の投稿を表示しています。 すべての投稿を表示
ラベル Android の投稿を表示しています。 すべての投稿を表示

2015年12月13日日曜日

カスタムROMとGoogleパスワード変更に潜む孔明の罠


Chromaに乗り換えたのでメモ。
ついでにXposedも入れてみた。

Chroma 12-10-2015
http://forum.xda-developers.com/nexus-6/development/rom-chroma-01-11-2015-t3000003

hellsCore b12-M
http://hc-kernel.blogspot.jp/2015/12/n6-b12-m.html

SuperSU 2.62
http://forum.xda-developers.com/showpost.php?p=64161125

Xposed
http://repo.xposed.info/module/de.robv.android.xposed.installer


という記事を書くはずだった。


が、何をやってもwi-fiが繋がらない。
wi-fiは一旦無視してログインしようとしたところ、なぜか認証時にブロックされる。

そして不正なログイン見つけたよ的なメールが届いて、強制的にパスワードを変更させられる。

諦めてROMをカタクリに変更してみたところ、wi-fiは接続成功。
そして相変わらずgoogleの認証が通らない。

Googleに問い合わせてみたところ、「君のアカウントは生きてるよ。良かったね。」みたいな自動返信が返ってくるだけ。

途方に暮れる。

ログイン失敗時に「この端末の所有者のアカウントを利用してログインしてください」と出るので、端末側かもしれないと思い完全に初期化。

さよならすべてのデータ達。
あっ。。バックアップを母艦に移してない。

。。。


「この端末の所有者のアカウントを利用してログインしてください」
「この端末の所有者のアカウントを利用してログインしてください」
「この端末の所有者のアカウントを利用してログインしてください」

あああああああああああああああああああああああああああああああ!!!!!!!!!!!(ブリブリブリブリュリュリュリュリュリュ!!!!!!ブツチチブブブチチチチブリリイリブブブブゥゥゥゥッッッ!!!!!!! )


で、色々調べたところパスワード変更後72時間は端末にログイン出来ない仕様とのこと。

これ何?常識?常識なの?

カスROMインストール⇒ログイン⇒ブロック⇒パスワード強制変更⇒72時間ブロック
いや、なんとも巧妙な罠ですわ。

とりあえず古いバックアップから復元したところ起動は出来た。
が、やはりモバイルからはGoogle系サービスが使えない。

ということで3日間のGoogle断ち生活(モバイルのみ)が始まる。



一応まともに動いたほう(はず)のカタクリの構成もメモしとく。



俺、この3日間の喪が明けたら、カタクリ入れるんだ。。

Read More...

2015年4月16日木曜日

Navigation DrawerとFragmentをAPI level 11未満で使いたい


ちょっといまさらですが、NavigationDrawerについて調べてみました。

NavigationDrawerを実装するには、サポートライブラリのandroid.support.v4.widget.DrawerLayoutを使います。

NavigationDrawerに限らず、画面を切り替えたりするのにFragmentを使うと便利なのですが、API level 11以上です。

API level11というとAndroid 3.0以降ということになりますが、Android 2.3.3 - 2.3.7がまだ1割弱は現存しているため、割り切ってしまうのもどうかなぁという気もします。

そこで方法がないか調べてみたところ、同じくサポートライブラリのandroid.support.v4.app.Fragmentを使えば可能でした。


ということで今回は、android.support.v4.widget.DrawerLayoutとandroid.support.v4.app.Fragmentを使って、NavigationDrawerを実装してみます。

NavigationDrawerから設定画面と詳細設定画面を切り替えるサンプルを作成します。

まずは、パーツを作成します。
設定画面と詳細設定画面のFragment用のレイアウトをそれぞれ作成します。

fragment_settings.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/content_settings"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Settings"
        android:id="@+id/textView1" />
</LinearLayout>

fragment_advanced_settings.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/content_advanced_settings"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Advanced Settings"
        android:id="@+id/textView2" />
</LinearLayout>

何の変哲も無いレイアウトです。

次に、android.support.v4.app.Fragmentを継承したクラスをそれぞれ作成します。

import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

public class SettingsFragment extends Fragment{

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        return inflater.inflate(R.layout.fragment_settings, container, false);
    }
}

import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

public class AdvancedSettingsFragment extends Fragment {

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        return inflater.inflate(R.layout.fragment_advanced_settings, container, false);
    }
}

onCreateViewメソッドをオーバーライドし、先ほどのxmlレイアウトファイルをそれぞれ紐付けます。

これでパーツは出来たので、次にメイン画面を作成します。

activity_main.xml

<android.support.v4.widget.DrawerLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/drawer_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <FrameLayout
        android:id="@+id/content_frame"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
    </FrameLayout>

    <LinearLayout
        android:id="@+id/drawer"
        android:orientation="vertical"
        android:layout_width="320dp"
        android:layout_height="match_parent"
        android:layout_gravity="start"
        android:background="#ffffffff">

        <TextView
            android:layout_width="match_parent"
            android:layout_height="48dp"
            android:textAppearance="?android:attr/textAppearanceLarge"
            android:text="Menu1"
            android:id="@+id/menuText1"
            android:textColor="#ff505050" />

        <TextView
            android:layout_width="match_parent"
            android:layout_height="48dp"
            android:textAppearance="?android:attr/textAppearanceLarge"
            android:text="Menu2"
            android:id="@+id/menuText2"
            android:textColor="#ff505050" />
    </LinearLayout>

</android.support.v4.widget.DrawerLayout>

android.support.v4.widget.DrawerLayoutをルートに配置し、一つ目の子がコンテンツ用、二つ目の子がNavigationDrawer用です。

コンテンツはFragmentを使って実装し、NavigationDrawerは今回はそのままここに書いてしまいます。

それでは組み立てます。

import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.widget.DrawerLayout;
import android.support.v7.app.ActionBarActivity;
import android.support.v7.app.ActionBarDrawerToggle;
import android.view.MenuItem;
import android.view.View;
import android.widget.TextView;

public class ActivityMain extends ActionBarActivity {

    private DrawerLayout mDrawerLayout;
    private ActionBarDrawerToggle mDrawerToggle;

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

        changeFragment(new SettingsFragment());
    }

    private void initDrawer() {
        mDrawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout);
        mDrawerToggle = new ActionBarDrawerToggle(this, mDrawerLayout, R.string.app_name, R.string.app_name);
        mDrawerToggle.setDrawerIndicatorEnabled(true);
        mDrawerLayout.setDrawerListener(mDrawerToggle);

        getSupportActionBar().setHomeAsUpIndicator(R.drawable.ic_drawer);
        getSupportActionBar().setDisplayHomeAsUpEnabled(true);
        getSupportActionBar().setDisplayShowHomeEnabled(true);
    }

    private void initDrawerMenu() {
        TextView menu1 = (TextView) findViewById(R.id.menuText1);
        menu1.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                changeFragment(new SettingsFragment());
                setTitle("Settings");
            }
        });
        TextView menu2 = (TextView) findViewById(R.id.menuText2);
        menu2.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                changeFragment(new AdvancedSettingsFragment());
                setTitle("Advanced Settings");
            }
        });
    }

    private void changeFragment(Fragment f) {
        FragmentManager fragmentManager = getSupportFragmentManager();
        fragmentManager.beginTransaction()
                .replace(R.id.content_frame, f)
                .commit();
        mDrawerLayout.closeDrawers();
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        if (mDrawerToggle.onOptionsItemSelected(item)) {
            return true;
        }
        return super.onOptionsItemSelected(item);
    }
}

今時っぽくするためにActionBarを使いたいので、またもやサポートライブラリからActionBarActivityを継承してクラスを作成します。

initDrawer()メソッドの中で、NavigationDrawerとActionBarの設定を行っています。

getSupportActionBar().setHomeAsUpIndicator(R.drawable.ic_drawer)で左上の三本線のアイコンを設定しています。
これ、なぜだか分かりませんが、あんまりネットで参考になる日本語サイトがなく、結構ハマりました。

アイコン画像はここで拾って来て、drawableに放り込んでおきます。

アイコンからNavigationDrawerを呼べるように、onOptionsItemSelected()内でActionBarDrawerToggleのonOptionsItemSelected()を呼びます。

initDrawerMenu()メソッドの中で、NavigationDrawerのメニューの設定を行っています。

今回はそれぞれOnClickListenerを設定し、コンテンツの変更とついでにActionBarのタイトルの変更を行っています。


実際のコンテンツの変更については、changeFragment()メソッドの中で実装しています。

FragmentManagerを利用してコンテンツを変更し、NavigationDrawerをクローズする処理を行っています。


出来ました。

まぁメニュー二つとかそんなシンプルな構成であれば、NavigationDrawerを使う必要もないのですが、とりあえず最近のアプリっぽくなりました。

Read More...

2015年3月10日火曜日

IngressのGlyph Hackを自動化する ~Input Subsystem~


お蔵入り寸前だったGlyph Hackの自動化ですが、Translaterメダルやら、ハッキングボーナスやスピードボーナスの上限解放やら、なんとかモチベーションを持ち直すニュースがあったので、再チャレンジします。

ということで、今回はInput Subsystemとやらを調べてみました。

まず、Input Subsystemで利用するイベントデバイスファイルは/dev/input/にあるとのことなので確認してみます。

自分の環境では、/dev/input/配下に以下が格納されていました。

event0
event1
event2
event3
event4
mice

それぞれが何らかのデバイスに対応しているそうですが、これだけでは分からないので、BusyBoxをインストールして、hexdumpコマンドで覗いてみます。

# /system/xbin/hexdump /dev/input/event0
0000000 1d21 0003 4cbf 0004 0003 0039 69d4 0000
0000010 1d21 0003 4cbf 0004 0003 0035 023c 0000
0000020 1d21 0003 4cbf 0004 0003 0036 028a 0000
0000030 1d21 0003 4cbf 0004 0003 003a 004a 0000
0000040 1d21 0003 4cbf 0004 0003 0030 0006 0000
0000050 1d21 0003 4cbf 0004 0000 0000 0000 0000
0000060 1d21 0003 8987 0004 0003 003a 0018 0000
0000070 1d21 0003 8987 0004 0003 0030 0002 0000
0000080 1d21 0003 8987 0004 0000 0000 0000 0000
0000090 1d21 0003 c0fa 0004 0003 0039 ffff ffff
00000a0 1d21 0003 c0fa 0004 0000 0000 0000 0000

なにやら出力されてきました。
しかし、何かやろうとすると、毎回バイナリを解読させられる運命にあるようですね。

ざっと画面をタッチしたりしながら眺めていると、はじめの2byteはイベントが発生した時間のようです。

次の2byteは入力のタイプEV_ABSを表す3かと思いましたが、なんだか違いそうです。

次の2byteもx軸かy軸かを表すもの、、、じゃなさそうです。
座標は後ろから2番目の2byteのようですね。
うーん。。分からない。

ということでもうちょっと良い方法がないか調べたところありました。
これはBusyBoxのインストールも不要です。

# getevent /dev/input/event0
0003 0039 00006be2
0003 0035 0000029d
0003 0036 000002aa
0003 003a 0000004d
0003 0030 00000008
0000 0000 00000000
0003 003a 00000037
0003 0030 00000006
0000 0000 00000000
0003 0039 ffffffff
0000 0000 00000000

type、code、valueってことなんだろうけど、なんか分かりそうで分からない。

さらに色々調べたところ、geteventコマンドに素敵なオプションを発見。

# getevent -lt /dev/input/event0
[  320377.729707] EV_ABS       ABS_MT_TRACKING_ID   00009411
[  320377.729707] EV_ABS       ABS_MT_POSITION_X    00000296
[  320377.729707] EV_ABS       ABS_MT_POSITION_Y    00000348
[  320377.729707] EV_ABS       ABS_MT_PRESSURE      00000048
[  320377.729707] EV_ABS       ABS_MT_TOUCH_MAJOR   00000007
[  320377.729707] EV_SYN       SYN_REPORT           00000000
[  320377.766317] EV_ABS       ABS_MT_TRACKING_ID   ffffffff
[  320377.766317] EV_SYN       SYN_REPORT           00000000

これなら読み解こうという気になります。

では、まずは登場人物を整理します。

type
EV_ABS 3EV_SYN 0

code
SYN_REPORT 0
ABS_MT_TRACKING_ID 57
ABS_MT_POSITION_X 53
ABS_MT_POSITION_Y 54
ABS_MT_PRESSURE 58
ABS_MT_TOUCH_MAJOR 48

value
ABS_MT_TRACKING_IDのIDや、ABS_MT_POSITION_Xのx座標など。

次に、タッチイベントを発生させるための流れを整理します。

まずEV_ABSABS_MT_TRACKING_IDで一連のイベントのIDを設定します。
Android側の動きをgeteventで見ていると、1ずつインクリメントされ連番になっているようです。
ただ、適当な値でも特に文句は言われませんので、乱数とかでも大丈夫なのかも??

次に座標を送信します。
ABS_MT_POSITION_Xと、ABS_MT_POSITION_Yでイベントが発生した座標を設定します。
続いてABS_MT_PRESSUREで圧力、ABS_MT_TOUCH_MAJORで断面積ということで、いまいち良く分かりませんが設定します。
最後に、EV_SYNSYN_REPORTを送信します。
ここまでが座標送信のワンセットです。

座標、ABS_MT_PRESSUREABS_MT_TOUCH_MAJORSYN_REPORTのセットを連続で複数送信するとスワイプイベントになります。

イベント終了時にはABS_MT_TRACKING_IDにFFFFFFFFを設定して送信し、EV_SYNSYN_REPORTを送信して完了です。


ではやってみましょう。

# sendevent /dev/input/event0 3 57 3800
# sendevent /dev/input/event0 3 53 1222
# sendevent /dev/input/event0 3 54 1237
# sendevent /dev/input/event0 3 58 72
# sendevent /dev/input/event0 3 48 7
# sendevent /dev/input/event0 0 0 0
# sendevent /dev/input/event0 3 57 -1
# sendevent /dev/input/event0 0 0 0

ちゃんと動きました。

さらに複数座標を送信して、動きをつけて軌跡を描けるか試しました。

成功!ちなみに描いたのは「SUCCESS」です。
なぜこんなに滑らかな曲線になるのかは謎です。


しかし、rootとってshellからなら何でも出来てしまうってのは結構恐ろしい事だなぁ、と。
Google先生が練り上げたAndroidのセキュリティの壁も、いとも簡単に超えてしまうわけですね。

まぁ当然なんですが。

とりあえずroot系アプリ入れるときは、ちょっと注意したほうがいいなと思いましたとさ。

Read More...

2015年2月16日月曜日

IngressのGlyph Hackをハックする


IngressのGlyph Hackをハック中。

フレームバッファのバイナリをBitmapに変換して必要なとこだけキャプチャするのは完了。
こうして人の脳は退化していくわけですね。

ついでに自前の検出器を作成して、それがどの図柄なのかを検出するのも完了。
画面上の表示位置が変わらないし図柄がすごく単純なので、OpenCVなんて使う必要は全くありませんでした。

そしてあとは自動で絵を描くだけだ~と思ったら、後一歩のところで壁にぶつかりました。

そもそも、画面キャプチャ、検出器作成まで出来たら、あとはイージーだと思って何も調べてなかったのですが、結構厄介な問題がありました。

よそ様のアプリを操作するためにタッチイベントを生成するには、android.permission.INJECT_EVENTSが必要なのですが、こいつがいわゆるシステム権限らしくうまく動きません。

AndroidManifest.xmlで宣言しても、SecurityExceptionが発生します。


システム権限ということであれば、/system/app配下に放り込めば良いのかと考え、以下のようにしてADB Shellから無理やり放り込んでシステムアプリ化してみました。

$ adb shell
$ su
# mount -o rw,remount /system
# cp /sdcard/AutoGlyph.apk /system/app
# chmod 644 /system/app/AutoGlyph.apk
# reboot

が、やはり動きません。


INJECT_EVENTSprotectionLevelsignatureで、公式の説明にはthe application that declared the permissionと同じ証明書で署名されていないとダメだとあります。
googleの署名じゃないとダメってことでしょうか?

もしかするとprotectionLevelsignatureOrSystemであれば、システムアプリ化するだけでいけたのかもしれませんが。。

これはお蔵入りの予感。。


ただ、LMT Launcherとかは普通に動いてるわけで、何かやり方はあるんでしょう。
セキュリティ的には出来ちゃダメな気もしますが。

システムアプリを偽装できるのか、Input Subsystemあたりを利用するのか。
LMT Launcherのpackagenamecom.android.lmtなのが気になりますが、それだけではダメな気がします。

う~ん。。


Read More...

2015年2月15日日曜日

Androidアプリからスクリーンショット /system/bin/screencap


前回に続きスクリーンショットについてです。

/system/bin/screencap -p test.png

前回は上記のように-pオプションを付けて実行し、ファイルの拡張子も.pngにしていたので、pngファイルで保存されていました。

普通の用途ではそれでいいんだと思いますが、今回は訳あって1200ミリ秒ぐらいの中で全部やりたいので、時間的にギリギリです。

そこで、pngへの変換を省いて生のBitmap画像を得る方法について実験します。


/system/bin/screencapコマンドについては、pオプションやファイル名を指定せずに実行すると、いわゆるフレームバッファの生データが標準出力に出力されます。

データのはじめにはwidth、height、formatの各4byteで12byteのヘッダが付いてくるそうです。

とりあえずヘッダの中身を見てみます。

 2684682240
 655360
 16777216

うーん、まったく意味が分からない。。
実際のwidthやheightで割ってみても意味のありそうな数字になりません。

LITTLE_ENDIANで読み直してみます。

 1440
 2560
 1

これだ!

そしてその後には14,745,600byteのデータが飛んできました。

Nexus6で実験しているので、画面サイズは縦×横が2,560×1,440です。

2560 * 1440 = 3686400

14745600 / 3686400 = 4

1ピクセルあたり4byteということは、普通に考えれば32bitということでしょうか。
ただ、ネットで調べると16bitのダブルバッファリングだという説があったので、適当に16bitのヘッダを付加して表示してみます。

はい。ぜんぜんダメでした。

素直に32bitのヘッダを付加してみます。

上下が逆転しています。

Bitmapのデータはなぜか左下から右方向に向かって格納されているそうなのですが、フレームバッファには普通に並んでいるので、byte配列を逆さにしてからBitmapに食わせます。

色情報の順番も変わってしまいました。また、左右は入れ替えなくて良いようです。

行単位で逆にします。

まだちょっと色がおかしい。
どうやらRとBが逆になっているようですね。

このサンプルだと分かりにくいのでもう少しカラフルなサンプルでRとBを入れ替えると。

完成。


正体の良く分からないバイナリをいい感じに加工して読めるデータにする作業って、何か高度なパズルを解いているようで楽しいです。

まぁちゃんと調べれば正体は分かるんでしょうけど。。


Read More...

2015年2月8日日曜日

Androidアプリからスクリーンショット /system/bin/screencap -p


Androidアプリからスクリーンショットを取るには、JNIとかでフレームバッファを直接見に行くしかないのかと思っていたのですが、Android4.0から/system/bin/screencapでスクリーンショットが可能になったとのことです。

root化は必要なものの、ずいぶん楽になったようですね。

try {
    final String SAVE_DIR = "/test/";
    String fileName = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date()) + ".png";
    String fullpath = Environment.getExternalStorageDirectory().getPath() + SAVE_DIR + fileName;
    Process p = Runtime.getRuntime().exec("su");
    OutputStream os = p.getOutputStream();
    os.write(("/system/bin/screencap -p " + fullpath).getBytes("ASCII"));
    os.flush();
    os.close();
    p.waitFor();
    p.destroy();
} catch (IOException e) {
    e.printStackTrace();
} catch (InterruptedException e) {
    e.printStackTrace();
}

これ終わったらちゃんとdestroy()で開放してあげないと、色々と後で死にそうですね。

Read More...

2014年11月29日土曜日

AndroidアプリのIn-app Billing version2からversion3への更新 ③テスト


In-app Billing version3の最終回です。
今回は作ったものがまともに動くかテストする方法を確認します。

テストには二通りの方法があり、ある程度の動きはサンドボックステストで確認し、最終的に実購入テストという流れかと思います。


  • サンドボックステスト
サンドボックステストのために、以下4つの予約済みアイテムIDが用意されています。

購入成功(android.test.purchased)
キャンセル、クレジットカード無効など(android.test.canceled)
払い戻し(android.test.refunded)
存在しないアイテム(android.test.item_unavailable)

android.test.canceledandroid.test.refundedの場合は、RESPONSE_CODEBILLING_RESPONSE_RESULT_USER_CANCELEDあたりが返ってくるのかと思ったのですが、BILLING_RESPONSE_RESULT_OKが返ってきます。

ただし、INAPP_PURCHASE_DATAINAPP_DATA_SIGNATUREはnullです。

android.test.item_unavailableの場合は、onActivityResultに渡されてくるresultCodeRESULT_CANCELEDが返ってきます。

こっちのActivityのRESULT_CANCELEDは0で、RESULT_OKが-1です。In-app Billingで返ってくるレスポンスコードはBILLING_RESPONSE_RESULT_OKが0なのでややこしいです。


ちなみにandroid.test.purchasedで一度購入成功すると、二度と購入出来なくなるのですが、「設定→アプリ→Google Play→データを削除」でデータを削除すると、再度購入出来るようになります。

  • 実購入テスト
サンドボックステストに飽きたら実購入テストに入ります。
実購入テストでは、Google Playと実際に通信して購入処理をテストします。


アルファ版配布用に、google グループで適当にグループを作成し、テスト用のgmailアドレスを招待しておきます。

テスト購入で課金が発生したりしないように、Developer Consoleにログインし、テストアカウントを設定します。
「設定→アカウントの詳細→ライセンス テスト→テスト用のアクセス権がある Gmail アカウント」にテスト用のgmailアドレスを入力します。

テストしたいapkをリリース用のKeyStoreで署名して、アルファ版テストにアップロードします。

「テスターのリストを管理→アプリのアルファ テスト版を提供するユーザー」という画面で先ほど作成したグループを登録すると、オプトインのためのアドレスが表示されます。

テスターが「テスターになる」をタップすると設定は完了で、あとは実際にGoogle Playから実機にインストールして試すだけです。

ただ、アルファ版が有効になるまで結構時間がかかる(半日とか1日とか)ようなので、作ってはアップロード作ってはアップロードという試し方は出来なそうな雰囲気です。

とは言え、やはり課金関連で失敗するとひどいことになるので、十分なテストの上で公開したいところです。

時間を置くと、実際にGoogle Playからダウンロード出来るようになります。

ダウンロードしたアルファ版のアプリで購入ボタンを押すと、しっかりとテスト注文である旨が表示されます。
(アプリ名やカード番号は消しました)

アルファテストで購入完了すると、Googleウォレット販売者コンソールでは以下のように「追加済み」と表示されます。

ということで、苦手意識のあったIn-app Billingですが、version3になって本当にすっきり分かりやすくなりました。

食わず嫌いだった人はこれを機に、是非再チャレンジしてみると良いと思います。


Read More...

AndroidアプリのIn-app Billing version2からversion3への更新 ②レスポンスを受け取る


前回はIn-app Billing version3のリクエスト送信方法について触りましたので、今回はそのレスポンス処理方法についてです。

  • レスポンスを受け取る
startIntentSenderForResultのレスポンスはActivityのonActivityResultで受け取ります。

startIntentSenderForResultの第2引数で渡したリクエストコードとの一致を確認した上で、resultCoderesponseCodeが問題ないか確認し、レスポンスの中身を取り出します。

purchaseDataはJSON形式になっていて、その中のproductIdを確認すると、どの商品が購入されたかが分かります。

protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    if (requestCode == REQUEST_CODE) {
        int responseCode = data.getIntExtra("RESPONSE_CODE", -1);
        if (resultCode == RESULT_OK && responseCode == BILLING_RESPONSE_RESULT_OK) {
            String purchaseData = data.getStringExtra("INAPP_PURCHASE_DATA");
            if(purchaseData != null) {
                String productId = "";
                try {
                    JSONObject jsonObj = new JSONObject(purchaseData);
                    productId = jsonObj.optString("productId");
                } catch (JSONException e) {
                    e.printStackTrace();
                }
                //必要な処理
            }
        }
    }
}

ちなみに、getIntExtradefaultValueを0にしているサンプルを良く見かけるのですが、0だとBILLING_RESPONSE_RESULT_OKになってしまって微妙な気がしたので-1にしてみました。
なんか理由があるのかもしれませんが。

  • 署名の確認
テストアカウントであればサンドボックステストでも署名が返ってくるはずですが、うまくいかなかったので実購入テストで試しました。

登場人物はonActivityResultに渡されてきたIntentに入っている、INAPP_PURCHASE_DATAINAPP_DATA_SIGNATUREと、公開鍵であるBASE64_ENCODED_PUBLIC_KEYです。

BASE64_ENCODED_PUBLIC_KEYは、Developers Consoleで確認出来る公開鍵です。

INAPP_PURCHASE_DATAはレスポンスのjson文字列で、INAPP_DATA_SIGNATUREはそれを公開鍵で署名したものです。

署名検証にはjava.security.Signatureを利用し、流れとしてはSignaturegetInstanceメソッドで生成し、initVerifyメソッドで公開鍵で初期化。updateメソッドでレスポンスのjson文字列をバイト配列に変換したものを渡しverifyメソッドで検証します。

protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    if (requestCode == REQUEST_CODE) {
        int responseCode = data.getIntExtra("RESPONSE_CODE", -1);
        if (resultCode == RESULT_OK && responseCode == BILLING_RESPONSE_RESULT_OK) {
            String purchaseData = data.getStringExtra("INAPP_PURCHASE_DATA");
            String signatureData = data.getStringExtra("INAPP_DATA_SIGNATURE");
            if (purchaseData != null && signatureData != null) {
                if(verify(generatePublicKey(BASE64_ENCODED_PUBLIC_KEY),purchaseData, signatureData)) {
                    //署名OK
                }
            }
        }
    }
}

private final String SIGNATURE_ALGORITHM = "SHA1withRSA";
private final String KEY_FACTORY_ALGORITHM = "RSA";

public boolean verify(PublicKey publicKey, String signedData, String signature) {
    Signature sig;
    try {
        sig = Signature.getInstance(SIGNATURE_ALGORITHM);
        sig.initVerify(publicKey);
        sig.update(signedData.getBytes());
        if (!sig.verify(Base64.decode(signature, Base64.DEFAULT))) {
            return false;
        }
        return true;
    } catch (InvalidKeyException e) {
    } catch (NoSuchAlgorithmException e) {
    } catch (SignatureException e) {
    }
    return false;
}

public PublicKey generatePublicKey(String encodedPublicKey) {
    PublicKey pKey = null;
    try {
        byte[] decodedKey = Base64.decode(encodedPublicKey, Base64.DEFAULT);
        KeyFactory keyFactory = KeyFactory.getInstance(KEY_FACTORY_ALGORITHM);
        pKey = keyFactory.generatePublic(new X509EncodedKeySpec(decodedKey));
    } catch (NoSuchAlgorithmException e) {
    } catch (InvalidKeySpecException e) {
    }
    return pKey;
}

署名のところはもう少し色々と考えなくてはいけないこともありますが、とりあえずは検証出来ました。
次回はここまで作成したもののテスト方法についてです。

Read More...

AndroidアプリのIn-app Billing version2からversion3への更新 ①リクエストを送る



GoogleがIn-app Billing Version 2 APIを殺すそうなので、もう長らく更新していなかったAndroidアプリを更新する羽目になっています。

昔version2の頃にアプリ内課金を実装したときは、正直意味が分からないけどサンプル通りに書いたら動いたというレベルだったので、結構苦手意識があります。

ただ、すごくシンプルになったという話も聞くので、これを機にまとめてみようと思います。


  • AIDLファイルの追加
サンプルアプリTrivialDriveに含まれている、Android Definititon Language(AIDL)ファイルを利用し、インターフェースファイルIIAppBillingService.javaを作ります。

Android SDK ManagerでGoogle Play Billing Libraryをインストールします 。


<android-sdk>/extras/google/play_billing/samples/TrivialDrive/からサンプルプロジェクトをインポートします。

TrivialDrivaサンプルの/srcディレクトリからIInAppBillingService.aidlファイルをコピーし、自分のアプリの/srcディレクトリへ貼り付けます。

/genディレクトリ内にIInAppBillingService.javaが自動的に作成されます。

  • AndroidManifestの更新
これはversion2と変わっていないようです。

AndroidManifest.xmlに以下のパーミッションを追加します。

<uses-permission android:name="com.android.vending.BILLING" />

  • ServiceConnectionの作成とIInAppBillingServiceへのバインド
接続時と切断時のコールバックメソッドを持つServiceConnectionを実装し、bindServiceメソッドでバインドします。

ServiceConnectionを実装します。

private IInAppBillingService mIInAppBillingService;
private ServiceConnection mServiceConnection= new ServiceConnection() {
    @Override
    public void onServiceDisconnected(ComponentName name) {
        mIInAppBillingService = null;
    }

    @Override
    public void onServiceConnected(ComponentName name, IBinder service) {
        mIInAppBillingService = IInAppBillingService.Stub.asInterface(service);
    }
};

IInAppBillingServiceを使用できるようにするため、ActivityonCreateメソッドでbindServiceメソッドを呼びます。
Intentの引数は、クラス名のIInAppBillingServiceではなく、InAppBillingServiceなのが紛らわしいですね。

Intent serviceIntent = new Intent("com.android.vending.billing.InAppBillingService.BIND");
bindService(serviceIntent, mServiceConnection, Context.BIND_AUTO_CREATE);

終了処理として、Activityが終了時にonDestroyメソッドでunbindServiceメソッドでアンバインドします。

if (mServiceConnection != null) {
    unbindService(mServiceConnection);
}


なお、このあたりで大きくハマったのですが、bindServiceを呼んでからServiceConnectiononServiceConnectedが呼ばれるまでにはタイムラグがあり、直後にIInAppBillingServiceをいじくろうとするとぬるぽで死にます。

bindService自体はtrueで返ってくるのに、IInAppBillingServiceはnullのまま。

時間を計ってみたところ、50~150ミリ秒後ぐらいでまちまちでしたが、何にしろタイムラグがあります。

直後にスレッド止めたりして待ち合わせをしても、ほぼ同時に返ってくるものの結局ぬるぽ。

まぁ何のためにonServiceConnectedメソッドを実装する必要があるのか考えれば、分かりそうなものでしたが。。

ということで、わざわざコールバックメソッドを実装させられている訳なので、onServiceConnectedからトリガーしてあげればぬるぽにはならない訳です。

  • サポート状況の確認
version3のAPIをサポートしているか確認します。

int response = mIInAppBillingService.isBillingSupported(3, getPackageName(), "inapp");
if(response == BILLING_RESPONSE_RESULT_OK) {
    //OKの場合の処理
}

  • 購入状況の確認
getPurchasesメソッドを呼ぶと、購入済みアイテムの情報を取得出来ます。

RESPONSE_CODEや、INAPP_PURCHASE_ITEM_LISTなど、いくつかの情報を含むBundleが返されるので、必要に応じて取り出し利用します。

Bundle bundle = mIInAppBillingService.getPurchases(3, getPackageName(), "inapp", null);
if(bundle.getInt("RESPONSE_CODE") == BILLING_RESPONSE_RESULT_OK) {
    ArrayList<String> purchases = bundle.getStringArrayList("INAPP_PURCHASE_ITEM_LIST");
    for (int i = 0, n = purchases.size(); i < n; i++) {
        //purchases.get(i)の中身によって処理を変える。
    }
}

  • 購入処理
getBuyIntentメソッドを呼んで、返されたBundleからBUY_INTENTキーのPendingIntentを取得します。
取得したPendingIntenstartIntentSenderForResultメソッドを呼び出します。

getBuyIntentメソッドの第4引数はDeveloper Payloadというやつで、空文字でもnullでもレスポンス時には空文字が返ってきます。
本来は任意の文字列を入れて、海賊版対策に使います。

startIntentSenderForResultメソッドの第2引数はコールバックを受けたときにリクエストを識別するための任意のint値です。

Bundle buyIntentBundle = mIInAppBillingService.getBuyIntent(3,
        getPackageName(), itemId, "inapp", "");
if (buyIntentBundle.getInt("RESPONSE_CODE") == BILLING_RESPONSE_RESULT_OK) {
    PendingIntent pendingIntent = buyIntentBundle
        .getParcelable("BUY_INTENT");
    startIntentSenderForResult(pendingIntent.getIntentSender(),
        REQUEST_CODE, new Intent(), Integer.valueOf(0),
        Integer.valueOf(0), Integer.valueOf(0));
}

なお、サポート状況の確認から購入処理まで、IInAppBillingServiceのメソッドを呼ぶ際には、RemoteExceptionが発生するので、適時catchします。

あとはActivityのonActivityResultへレスポンスが来るので、適時処理するだけです。


次回はstartIntentSenderForResultのレスポンス部分、onActivityResultでの処理についてです。


Read More...

2014年11月15日土曜日

AdMobをGoogle Mobile Ads SDKを使って最新のライブラリで更新する



定期的に最新のライブラリへ更新を要求してきて、既存アプリのメンテを余儀なくさせるAdMobについてです。

しばらく見ない間に、Google Admob ads SDKだの、Google Play Services SDKだの、Google Mobile Ads SDKだの、ライブラリも良く分からない状況になっています。

軽く調べたところ、最新はGoogle Play 開発者サービスのGoogle Mobile Adsに内包されているようなので、既存のアプリを切り替えていきます。

かなりの期間放置していたので、何世代か前の状態からの更新になるかと思います。


まずはGoogle Mobile Ads SDKですが、Android3.2以降でのコンパイルが必要だそうです。
つまり、project.propertiestargetがandroid-13以降に設定されている必要があります。

次に、Android 2.3以降のランタイムが必要だそうです。
AndroidManifest.xmlandroid:minSdkVersion9以降に設定されている必要があります。

今回更新するアプリのminSdkVersionは今まで7にしていたので、今回の更新で2.1、2.2ユーザを切り捨てることになります。

悩ましいところですが、こういうことは定期的に発生し、どこかで割り切る必要があります。


では実際に既存アプリを修正していきます。


  • ライブラリの追加

Android SDK ManagerからGoogle Play servicesをインストールします。

完了すると<android-sdk>/extras/google/google_play_services/libproject/配下にプロジェクトが配置されます。

配置されたgoogle-play-services_libライブラリプロジェクトを、FileImportAndroidExisting Android Code into Workspaceでインポートします。

今回更新するプロジェクトを右クリックして、プロパティAndroidライブラリー追加から追加します。

ちなみに、既存アプリに上記の流れでライブラリを追加したところ、以下のエラーが出ました。

Conversion to Dalvik format failed: Unable to execute dex:
Multiple dex files define Lcom/google/ads/AdRequest$ErrorCode;

どうやらライブラリのコンフリクトというやつで、古いGoogleAdMobAdsSdkライブラリがライブラリフォルダに入っていたので削除で解決しました。


  • AndroidManifestの設定

AndroidManifest.xmlをいくつか修正します。

まずは、以下のmeta-dataタグを追加します。

<meta-data android:name="com.google.android.gms.version"
       android:value="@integer/google_play_services_version"/>

はじめは以下のエラーが出ていました。

No resource found that matches the given name (at 'value' with value'@integer/google_play_services_version').

ライブラリのところを見てみると、以下のように×がついています。

結局良く分からないのですが、色々調べながらgoogle-play-servicesのインポートをやり直したりしている内に直りました。

ただ、直った後に別のアプリに同じことをしていたらまた同じエラー出たり、良く分からない動きをします。ネットでもハマっている人は多そうでした。

結局、google-play-servicesのほうではなく、アプリのほうのインポートをやり直すとうまくいきました。

google-play-servicesライブラリを追加した時に、アプリによって絶対パスで表示されたり、相対パスで表示されたりしていたので、アプリ側のインポート時に問題があったのかも知れません。


次に、com.google.android.gms.ads.AdActivityを宣言します。

<activity android:name="com.google.android.gms.ads.AdActivity"
       android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|uiMode|screenSize|smallestScreenSize" />

ちなみに古いコードでは、android:namecom.google.ads.AdActivityだったので、ここだけ変更しました。


最後に、ネットワーク権限を設定します。

<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>

これは前と同じだったので触っていません。


  • Viewの追加
xmlで設定する場合は、com.google.android.gms.ads.AdViewを追加します。

<com.google.android.gms.ads.AdView android:id="@+id/adView"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       ads:adUnitId="MY_AD_UNIT_ID"
       ads:adSize="BANNER"/>

com.google.ads.AdViewだったものを、com.google.android.gms.ads.AdViewに変更するだけです。

ただ、AdViewだけの変更だと、以下のIllegalStateExceptionが出ました。

Ad Size and Ad unit id must be set before loadAd is called

これはXMLネームスペースの設定を変更していないため発生していました。
前はxmlns:ads="http://schemas.android.com/apk/lib/com.google.ads"だったものを、xmlns:ads="http://schemas.android.com/apk/res-auto"に変更します。


また、MY_AD_UNIT_IDの部分が、前はパブリッシャー IDというaから始まる15文字の文字列だったのに対し、現在は広告ユニットIDというca-app-pub-xxxxxxxxxxxxxxxx/nnnnnnnnnnという形式の文字列に置き換わりました。

これはAdMob管理画面内の収益化タブで確認出来ます。


ちなみにいつからあるのか知りませんが、ads:adSizeSMART_BANNERが選択出来るようになっています。

BANNERからSMART_BANNERに変更するだけで、自動的に画面サイズにあわせて真ん中に表示してくれます。

昔は自分で何かしら設定して、センタリングさせたと記憶しています。
これは便利ですね。


  • リソースのルックアップと読み込み

ActivityonCreateに以下コードを書きます。

AdView adView = (AdView)this.findViewById(R.id.adView);
AdRequest adRequest = new AdRequest.Builder().build();
adView.loadAd(adRequest);

自分の昔のコードを見るとAdRequestを直接newしていたのですが、AdRequestの生成方法が変わったようです。


  • テストデバイスの指定

そのまま実機で起動すると、テスト起動時にも普通に広告が表示されてしまうので、テスト用デバイスを指定しておきます。

一旦画面を表示させると、LogCatに以下のように端末IDのようなものが表示されます。

Use AdRequest.Builder.addTestDevice("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx") to get test ads this device.

上記のIDをコピーして、AdRequestを以下のように生成すると、指定した実機ではテスト用のバナー広告が表示されるようになります。

AdRequest request = new AdRequest.Builder()
    .addTestDevice("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx")
    .build();

こんな感じになります。

ちゃんとセンタリングもされています。


ちなみに、実機でテストを実行するときに、製品版がインストールされてた場合、前はエラーになってた気がしますが、アンインストールするか聞いてくれるようになっています。

色々とイラっとしていた部分が便利になっていたりして、久しぶりに触ると楽しいです。
Read More...

2013年5月18日土曜日

Androidで音声解析!Visualizerによる高速フーリエ変換(FFT)


今回は、音の周波数を解析して音階を特定するというのをやってみたいと思います。

音階の特定には、時間軸を周波数軸に変換するフーリエ変換という処理で周波数解析を行います。

\hat{f}(\xi) := \int_{-\infty}^{\infty} f(x)\ e^{- 2\pi i x \xi}\,dx

数式を載せてみたものの、これが本当にフーリエ変換の式なのかすらよく分かりません。
数学的な知識はまったく無いので、フーリエ変換についての説明や考察は一切しません。いや、出来ません。

基本的に勘で進めているので、間違った記述もあるかもしれませんが、取りあえず動いたので完全に間違いという事ではないでしょう。


まずはシンプルに440Hz(Aの音)のsin波を鳴らして、それが440Hzであることが分かればゴールとします。


フーリエ変換を行うには、波形を表示したりといったことにも活用出来るandroid.media.audiofx.Visualizerを利用します。
android.media.audiofxはAPIレベル9(Android2.3)で追加されたパッケージで、イコライザ機能などを簡単に実装するものです。


public Visualizer (int audioSession)

 int audioSession 分析対象のMediaPlayerやAudioTrackのセッションID



流れとしては、Visualizerのコンストラクタに解析したいMediaPlayerなどのセッションIDを渡してやり、setCaptureSize()メソッドでキャプチャサイズを設定し、setDataCaptureListener()でリスナーを登録します。

キャプチャサイズを設定するときに利用するVisualizer.getCaptureSizeRange()で返ってくる配列はlengthが2で、index[0]は128、index[1]は1,024です。
つまり、キャプチャサイズは128か1,024しか選択出来ないようです。
129や2,048など別の数字を入れてもエラーにはなりませんが、VisualizerのgetCaptureSize()で確認すると、1,024になっていたので、自動で修正されているようです。

public int setDataCaptureListener (Visualizer.OnDataCaptureListener listener, int rate, boolean waveform, boolean fft)

Visualizer.OnDataCaptureListener listener 登録するリスナー
int rate         キャプチャ更新のレート
boolean waveform trueならonWaveFormDataCapture()が呼ばれる
boolean fft      trueならonFftDataCapture()が呼ばれる


キャプチャ更新レートの最大値はVisualizer.getMaxCaptureRate()で20,000です。

登録するリスナーはVisualizer.OnDataCaptureListenerインターフェースを実装したもので、以下二つの抽象メソッドがあります。

public abstract void onFftDataCapture (Visualizer visualizer, byte[] fft, int samplingRate)

public abstract void onWaveFormDataCapture (Visualizer visualizer, byte[] waveform, int samplingRate)


onFftDataCapture()がフーリエ変換後のデータを利用するもので、onWaveFormDataCapture()は生のWaveデータを利用するものです。


それではいよいよonFftDataCapture()についてみていきましょう。

まずドキュメントでVisualizerのgetFft (byte[] fft)部分を見ると、index[1]を除くと、byte配列には実数部と虚数部が交互に格納されているようなことが書いてあります。

ソースをちゃんと見ていませんが、onFftDataCapture()で渡されてくるbyte配列も恐らくこれと同じでしょう。

実数?虚数?という感じですが、複素数平面に拡張するには訳があります。

ある波を複数のsin波の合計で表現することがフーリエ変換なのですが、位相のズレを表現するためには虚数が必要なんです。
位相のズレたsin波をsinやcosをフル活用して表現する的な感じですかね。

はい。

あんまり良く分かりません。

まぁここで重要なのは、onFftDataCapture()のbyte配列の実数部がcos波、虚数部がsin波を表すってところです。

※という割には実数部を見ても虚数部を見ても同じところにピークが来ていました。また、ためしにcos波を混ぜてみたらまったく想定していない結果になり、謎は深まるばかりです。


ちなみにどの程度細かく分析が可能かに触れていませんでしたが、実はそんなに細かく分析出来ません。

サンプルレートが44,100Hzであれば、いわゆるナイキスト周波数は22,050Hz(44,100Hz / 2)で、キャプチャサイズが1,024であれば、分解能は21.5332031Hz(44,100Hz / 2 / 1,024)となります。

これはG♭3とG3ぐらいの差なので、音階を完全に特定するには十分とはいえない気がしますが、これが最大値なようなので仕方が無いです。
低周波になってくると音階の区別がつかなくなるってことでしょう。


つまり、サンプルレートが44,100Hzで、キャプチャサイズが1,024であれば、分解能は21.5332031Hzとなり、FFTの結果のbyte配列は恐らく以下のよう解釈すれば良いと思われます。

index[0]  実数部 0Hz~10.76660155Hz
index[1]  実数部 10.76660155Hz~21.5332031Hz
index[2]  実数部 21.5332031Hz~43.0664062Hz
index[3]  虚数部 21.5332031Hz~43.0664062Hz
index[4]  実数部 43.0664062Hz~64.5996093Hz
index[5]  虚数部 43.0664062Hz~64.5996093Hz




440HzのAの音であれば、以下のindex[41]にピークがくるということです。

index[41]  虚数部 430.664062Hz~452.1972651Hz

では実際にやってみましょう。
まずはFFTの結果を拾うところまで。


AudioTrack mAudioTrack;
Visualizer mVisualizer;

//サンプルレート
static int SAMPLE_RATE = 44100;
int bufSize = SAMPLE_RATE * 5;

//ストリームモードでAudioTrackを利用します
mAudioTrack = new AudioTrack(
  AudioManager.STREAM_MUSIC, SAMPLE_RATE,
  AudioFormat.CHANNEL_OUT_MONO,
  AudioFormat.ENCODING_DEFAULT,
  bufSize, AudioTrack.MODE_STREAM,
  420);//セッションID
mAudioTrack.play();


mVisualizer = new Visualizer(420);

// 1024
int captureSize = Visualizer.getCaptureSizeRange()[1];
mVisualizer.setCaptureSize(captureSize);


mVisualizer.setDataCaptureListener(
  new Visualizer.OnDataCaptureListener() {
   // Waveデータ
   public void onWaveFormDataCapture(
     Visualizer visualizer,
     byte[] bytes, int samplingRate) {
   }

   // フーリエ変換
   public void onFftDataCapture(
     Visualizer visualizer,
     byte[] bytes, int samplingRate) {

    //このbytesがFFT後のデータ
    //何度も呼ばれるのでこのデータを分析する

   }, Visualizer.getMaxCaptureRate(),
   false,//trueならonWaveFormDataCapture()
   true);//trueならonFftDataCapture()

mVisualizer.setEnabled(true);

byte[] audioData = new byte[bufSize];

// A単音
double freqA = 440;
double t = 0.0;
double dt = 1.0 / SAMPLE_RATE;
for (int i = 0; i < audioData.length; i++, t += dt) {
 audioData[i] = (byte) (Byte.MAX_VALUE * (Math.sin(2.0 *
   Math.PI * t * freqA)));
}
mAudioTrack.write(audioData, 0, audioData.length);


これでフーリエ変換後のデータを取得出来ました。
あとはピークがどこに来ているか確認し、分解能を元に周波数を特定すれば良いだけです。

ただし、結果を見てみると概ね正しく解析出来ますが、おかしな数字が出てくることもあります。

index[41] : 441.4306640625
index[41] : 441.4306640625
index[41] : 441.4306640625
index[73] : 785.9619140625
index[73] : 785.9619140625
index[141] : 1518.0908203125
index[169] : 1819.5556640625
index[123] : 1324.2919921875
index[41] : 441.4306640625
index[41] : 441.4306640625
index[41] : 441.4306640625
index[41] : 441.4306640625
index[49] : 527.5634765625
index[41] : 441.4306640625
index[149] : 1604.2236328125
index[149] : 1604.2236328125
index[41] : 441.4306640625
index[41] : 441.4306640625
index[41] : 441.4306640625

正しいindex[41]以外はバラバラですね。

虚数部しか見ていなかったので、位相のズレなどが正しく解釈出来ていないということでしょうか??

まぁちょっと勉強が必要ですね。

Read More...

2013年4月22日月曜日

テストも簡単!Intel XDKで書いたアプリをAndroidでテストする


前回エミュレータ上では、AndroidでもiPhoneでも動いたthree.jsのサンプルですが、今回は実際に実機でテストしてみます。

XDKのエミュレータの右上にある「App Tester」をクリックします。

作成したProjectがクラウドに転送されます。

転送完了。
右矢印をクリックすると手順の説明が出てきます。

ここからはappMobiのAndroidアプリ「app•lab」をインストールし、Android側で作業します。

アプリを起動し、「enter the lab」から先へ進みます。

「log in now」からブラウザを起動します。
どうやらはじめから画面の右上に表示されている「MY APPS」からでも同じく飛べるようです。

XDKに登録した際のIDとPWを入力してログインします。

先ほど転送したProjectを選択します。

「App Tester」を選択します。
「Test Local」からでも起動出来ますが、その差は良く分かりません。

「Launch」から起動します。

はい、見事動きません。

html5の互換性の問題なのか何なのか。。。

他のサンプルアプリを飛ばしてみたところ普通に動いたので、AndroidのChromeブラウザとthree.jsの問題かもしれません。

たしかにthree.jsで書いたアプリがAndroidで動いているサンプルってあまり見ないんですよね。

そもそもこのXDKのエミュレータは、ブラウザによる対応状況の差異まで考慮されていないのかもしれません。画面サイズによる差異を見る程度の話で、HTML5の互換性を100%と過程しているのだとするとあまり意味無いですね。

何にせよ、3Dで物理演算までの道のりはまだまだ長いです。。


Read More...

2012年4月24日火曜日

エッジを検出する その3 - OpenCV for Android


続いてCannyフィルタです。

ガウシアンフィルタとSobelフィルタを組み合わせてエッジを検出します。
まず、ガウシアンフィルタでぼけた画像を作り、その画像にSobelフィルタをかけることでエッジを検出します。
実際には処理はそれだけではなく、色々とやっているようです。

Cannyフィルタを利用するには、Imgproc.Canny()メソッドを利用します。

Imgproc.Canny(Mat image, Mat edges, double threshold1,
double threshold2, int apertureSize, boolean L2gradient)

Mat src          処理したい元画像のMat
Mat edges        変換後Mat
double threshold1   第1閾値
double threshold2   第2閾値
int apertureSize    Sobelのアパーチャサイズ
boolean L2gradient  L2ノルム利用有無


threshold1、threshold2については、値が小さいほうがエッジ同士を接続するために用いられ、大きいほうが強いエッジの初期検出に用いられます。
順番を変えても結果は同じです。

apertureSizeについては、デフォルトが3で、3、5、7が選択出来ます。

L2gradientについては、画像勾配の強度を求めるために、精度の高いL2ノルムを利用するか、高速なL1ノルムを利用するかを設定します。
デフォルトではL1ノルム(false)です。


それでは、実際にやってみましょう。

元画像です。


Imgproc.Canny(mat, dstMat, 50, 200);


Imgproc.Canny(mat, dstMat, 100, 200);


Imgproc.Canny(mat, dstMat, 100, 300);


Imgproc.Canny(mat, dstMat, 50, 200, 5);


Imgproc.Canny(mat, dstMat, 50, 200, 7);


Imgproc.Canny(mat, dstMat, 100, 200, 3, false);


Imgproc.Canny(mat, dstMat, 100, 200, 3, true);


ちなみに平滑化⇒Sobelで同じような結果になるんでしょうか?
Imgproc.GaussianBlur(mat, mat, new Size(3, 3), 0, 0);
Imgproc.Sobel(mat, dstMat, mat.type(), 1, 1, 5);


そう単純では無いようですねw


Read More...

エッジを検出する その2 - OpenCV for Android


続いてラプラシアンフィルタです。

ラプラシアンフィルタは2次微分を計算するフィルタで、Imgproc.Laplacian()メソッドを利用します。

Imgproc.Laplacian(Mat src, Mat dst, int ddepth,
int ksize, double scale, double delta, int borderType)

Mat src     処理したい元画像のMat
Mat dst     変換後Mat
int ddepth    変換後のビット深度
int ksize     カーネルサイズ
double scale  計算されたデリバティブの値の任意のスケールファクタ
double delta  オプションのデルタ値
int borderType ピクセル外挿手法


引数はその1でやったImgproc.Sobel()に出てきたものと同じです。

それでは、実際にやってみましょう。

おなじみの元画像です。


Imgproc.Laplacian(mat, dstMat, mat.type(), 1);



ksizeを変更してみます。
Imgproc.Laplacian(mat, dstMat, mat.type(), 7);



scaleを設定してみます。
Imgproc.Laplacian(mat, dstMat, mat.type(), 1, 50.0);



deltaを設定してみます。
Imgproc.Laplacian(mat, dstMat, mat.type(), 1, 1.0, 100.0);


x方向、y方向が無いので、Imgproc.Sobel()より単純ですね。


Read More...

2012年4月15日日曜日

エッジを検出する その1 - OpenCV for Android


いよいよ画像解析っぽくなってきました。

エッジ検出とは、画素の明るさが急激に変化する場所を探し、画像の輪郭を算出する処理を指します。
明るさの変化は、微分演算を利用することで算出し、グラディエント(1次微分)とラプラシアン(2次微分)があります。

1次微分を計算するSobelフィルタを利用するには、Imgproc.Sobel()メソッドを利用します。

imgproc.Imgproc.Sobel(Mat src, Mat dst, int ddepth,
int dx, int dy, int ksize, double scale, double delta, int borderType))

Mat src     処理したい元画像のMat
Mat dst     変換後Mat
int ddepth    変換後のビット深度
int dx       dx
int dy       dy
int ksize     カーネルサイズ
double scale  計算されたデリバティブの値の任意のスケールファクタ
double delta  オプションのデルタ値
int borderType ピクセル外挿手法

dx、dyについては、ksizeが1の場合を除き、必ずksize未満である必要があります。
ksizeが1の場合は、3 x 1もしくは1 x 3のカーネルが利用されますので、dx、dyの最大値は2です。

ksizeについては、ksizeが1の場合を除き、導関数を計算するためにksize × ksizeのカーネルが利用されます。
ksizeは奇数かつ31以下である必要があります。

ksizeが特殊な値「Imgproc.CV_SCHARR」の場合、3×3のSobelフィルタより精度が高い、3×3のScharrフィルタに対応します。
Scharrフィルタを利用する場合、dxかdyのどちらかが1、どちらかが0である必要があります。


それでは、実際にやってみましょう。

グレースケールに変換してから処理します。

まずは元画像。


Imgproc.Sobel(mat, dstMat, mat.type(), 1, 0, 1);


dxを変更してみます。
Imgproc.Sobel(mat, dstMat, mat.type(), 2, 0, 1);


ドキュメントには、このメソッドはほとんどの場合(dx = 1, dy = 0, ksize = 3)もしくは(dx = 0, dy = 1, ksize = 3) の引数で呼び出されるとあるので、それぞれ試してみます。
Imgproc.Sobel(mat, dstMat, mat.type(), 1, 0, 3);


Imgproc.Sobel(mat, dstMat, mat.type(), 0, 1, 3);


ksizeを変更してみます。
Imgproc.Sobel(mat, dstMat, mat.type(), 1, 1, 7);


Imgproc.Sobel(mat, dstMat, mat.type(), 1, 1, 21);


Scharrフィルタを利用してみます。
Imgproc.Sobel(mat, dstMat, mat.type(), 1, 0,
  Imgproc.CV_SCHARR);


Imgproc.Sobel(mat, dstMat, mat.type(), 0, 1,
  Imgproc.CV_SCHARR);


scaleを変更してみます。
Imgproc.Sobel(mat, dstMat, mat.type(), 1, 1, 3, 50.0);


deltaを変更してみます。
Imgproc.Sobel(mat, dstMat, mat.type(), 1, 1, 3, 1.0,
  100.0);


ところで微分って何?おいしいの?

Read More...