GPUImage for Androidを試してみた

GPUImageでiOSのカメラアプリを作るエントリーを読んでたら、GPUImage for Androidがあるとのことなのでサンプルを試してみた。

GPUImageで高速フィルター!iOSカメラアプリの作り方(まとめ・サンプルコードあり)
GPUImage for Android

サンプルコード

githubからcloneしたプロジェクトはmavenのプロジェクトになっているのだが、eclipseにインポートする際はAndroidのプロジェクトとしてインポートした。
どうもandroid-maven-pluginが上手く動いていないのか、SDKやNDKのパスが無いとか、maven installはできてもエラーのマークがプロジェクトから消えないとか、よく分からないのでeclipse上でのmavenは諦めた。
ここを参考に結構頑張ってみたけど。。。
EclipseでAndroidプロジェクトをMavenで管理するための環境構築 + Emacsキーバインドの設定 (2012年版)

Androidのプロジェクトとしてインポートしたら、コマンドラインでlibraryを

mvn clean install

して、サンプルプロジェクトをeclipseから実行すればよい。
ビルド時にminSDKのAPI levelが低いと怒られるので、AndroidManifest.xmlのAPI levelを14に修正した。

手元に開発機がなかったのでエミュレータを使ったら、実行時にsoがloadできないと言われてしまってちょっとハマった。
高速化用のIntel版のイメージを使ってたせいで、armビルドしたsoはloadできないのだった。
当たり前っちゃ当たり前なんだけど、普段エミュレータなんてほとんど使わないせいですっかり頭から抜け落ちていた。
arm版のイメージのエミュレータだとOpen GL 2.0がサポートされてないようなので、libraryをx86用にビルドすることにした。

ndk-build APP_ABI=x86

これでx86用にビルドしなおして実行したところエミュレータでもリアルタイムにフィルターが掛かるサンプルが動いた。
device-2013-02-03-005409
端末の向きを90度回転すると何故か上下逆になってしまうので縦のままw
フィルターは「Grouped Filters」を選択している。

フィルターを作ってみたいからフィルターのソース読んでOpenGLを勉強してみようかな。

IPC(プロセス間通信)におけるException

Androidのサービスを別プロセスからバインドしたい時に使うIPC絡みのメモ。
android.os.BinderのexecTransact(int, int, int, int)の実装。

     // Entry point from android_util_Binder.cpp's onTransact
    private boolean execTransact(int code, int dataObj, int replyObj,
            int flags) {
        Parcel data = Parcel.obtain(dataObj);
        Parcel reply = Parcel.obtain(replyObj);
        // theoretically, we should call transact, which will call onTransact,
        // but all that does is rewind it, and we just got these from an IPC,
        // so we'll just call it directly.
        boolean res;
        try {
            res = onTransact(code, data, reply, flags);
        } catch (RemoteException e) {
            reply.writeException(e);
            res = true;
        } catch (RuntimeException e) {
            reply.writeException(e);
            res = true;
        }
        reply.recycle();
        data.recycle();
        return res;
    }

onTransact()でRemoteExceptionかRuntimeExceptionが発生したら、android.os.ParcelのwriteException(Exception)が呼ばれる。

    /**
     * Special function for writing an exception result at the header of
     * a parcel, to be used when returning an exception from a transaction.
     * Note that this currently only supports a few exception types; any other
     * exception will be re-thrown by this function as a RuntimeException
     * (to be caught by the system's last-resort exception handling when
     * dispatching a transaction).
     * 
     * <p>The supported exception types are:
     * <ul>
     * <li>{@link BadParcelableException}
     * <li>{@link IllegalArgumentException}
     * <li>{@link IllegalStateException}
     * <li>{@link NullPointerException}
     * <li>{@link SecurityException}
     * </ul>
     * 
     * @param e The Exception to be written.
     *
     * @see #writeNoException
     * @see #readException
     */
    public final void writeException(Exception e) {
        int code = 0;
        if (e instanceof SecurityException) {
            code = EX_SECURITY;
        } else if (e instanceof BadParcelableException) {
            code = EX_BAD_PARCELABLE;
        } else if (e instanceof IllegalArgumentException) {
            code = EX_ILLEGAL_ARGUMENT;
        } else if (e instanceof NullPointerException) {
            code = EX_NULL_POINTER;
        } else if (e instanceof IllegalStateException) {
            code = EX_ILLEGAL_STATE;
        }
        writeInt(code);
        StrictMode.clearGatheredViolations();
        if (code == 0) {
            if (e instanceof RuntimeException) {
                throw (RuntimeException) e;
            }
            throw new RuntimeException(e);
        }
        writeString(e.getMessage());
    }

SecurityException, BadParcelableException, IllegalArgumentException, NullPointerException, IllegalStateExceptionの5つのRuntimeExceptionのサブクラスはそのままwriteString()されるが、それ以外のRuntimeExceptionのサブクラス、若しくはExceptionのサブクラスはRuntimeExceptionとしてthrowされる、と。
つまり、上記5つのRuntimeExceptionのサブクラスはIPCの呼び出し元でcatchできる。
API Level 15からはRemoteExceptionのコンストラクターにStringが渡せるようになってるが、API Level 15未満の環境ではこの5つのどれかで代替するというのもアリなのか?
まぁ、名前からしてIllegalArgumentExceptionかIllegalStateExceptionぐらいしか使えそうにないが。

AChartEngineでチャート(グラフ)の非同期更新

Androidでチャートの非同期更新をやったのでメモ。

Androidでチャートを扱う時に便利なライブラリにAChartEngineというモノがある。
チャートのデモもダウンロードできるので、ソースを読めば理解が早い。

サポートしているチャートは色々あるが、今回はラインチャート(折れ線グラフ)を使った。
デモの「AverageTemperatureChart.java」のソースを参考に、チャート表示に必要なコードを流用し簡単なラインチャートを書いた。

package com.nalabjp.example.achart;

import java.util.ArrayList;
import java.util.List;

import org.achartengine.ChartFactory;
import org.achartengine.GraphicalView;
import org.achartengine.chart.PointStyle;
import org.achartengine.model.XYMultipleSeriesDataset;
import org.achartengine.model.XYSeries;
import org.achartengine.renderer.XYMultipleSeriesRenderer;
import org.achartengine.renderer.XYSeriesRenderer;

import android.app.Activity;
import android.graphics.Color;
import android.graphics.Paint.Align;
import android.os.Bundle;
import android.os.Handler;

public class AChartExampleActivity extends Activity {
	private XYMultipleSeriesDataset dataset;
	private GraphicalView graphicalView;

	@Override
	public void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);

		String[] titles = new String[] { "Blue", "Green" };
		List<double[]> x = new ArrayList<double[]>();
		for (int i = 0; i < titles.length; i++) {
			x.add(new double[] { 1, 2, 3, 4, 5 });
		}
		List<double[]> values = new ArrayList<double[]>();
		values.add(new double[] { 1, 2, 3, 4, 5 });
		values.add(new double[] { 18, 17, 16, 15, 14 });
		int[] colors = new int[] { Color.BLUE, Color.GREEN };
		PointStyle[] styles = new PointStyle[] { PointStyle.CIRCLE,	PointStyle.DIAMOND };
		XYMultipleSeriesRenderer renderer = buildRenderer(colors, styles);
		int length = renderer.getSeriesRendererCount();
		for (int i = 0; i < length; i++) {
			((XYSeriesRenderer) renderer.getSeriesRendererAt(i))
					.setFillPoints(true);
		}
		setChartSettings(renderer, "Average temperature", "Horizontal axis",
				"Vertical axis", 0.5, 12.5, -10, 40, Color.LTGRAY, Color.LTGRAY);
		renderer.setXLabels(12);
		renderer.setYLabels(10);
		renderer.setShowGrid(true);
		renderer.setXLabelsAlign(Align.RIGHT);
		renderer.setYLabelsAlign(Align.RIGHT);
		renderer.setZoomButtonsVisible(true);
		renderer.setPanLimits(new double[] { -10, 20, -10, 40 });
		renderer.setZoomLimits(new double[] { -10, 20, -10, 40 });

		dataset = buildDataset(titles, x, values);
		
		graphicalView = ChartFactory.getLineChartView(
				getApplicationContext(), dataset, renderer);

		setContentView(graphicalView);
	}

	private XYMultipleSeriesDataset buildDataset(String[] titles,
			List<double[]> xValues, List<double[]> yValues) {
		XYMultipleSeriesDataset dataset = new XYMultipleSeriesDataset();
		addXYSeries(dataset, titles, xValues, yValues, 0);
		return dataset;
	}

	private void addXYSeries(XYMultipleSeriesDataset dataset, String[] titles,
			List<double[]> xValues, List<double[]> yValues, int scale) {
		int length = titles.length;
		for (int i = 0; i < length; i++) {
			XYSeries series = new XYSeries(titles[i], scale);
			double[] xV = xValues.get(i);
			double[] yV = yValues.get(i);
			int seriesLength = xV.length;
			for (int k = 0; k < seriesLength; k++) {
				series.add(xV[k], yV[k]);
			}
			dataset.addSeries(series);
		}
	}

	private XYMultipleSeriesRenderer buildRenderer(int[] colors,
			PointStyle[] styles) {
		XYMultipleSeriesRenderer renderer = new XYMultipleSeriesRenderer();
		setRenderer(renderer, colors, styles);
		return renderer;
	}

	private void setRenderer(XYMultipleSeriesRenderer renderer, int[] colors,
			PointStyle[] styles) {
		renderer.setAxisTitleTextSize(16);
		renderer.setChartTitleTextSize(20);
		renderer.setLabelsTextSize(15);
		renderer.setLegendTextSize(15);
		renderer.setPointSize(5f);
		renderer.setMargins(new int[] { 20, 30, 15, 20 });
		int length = colors.length;
		for (int i = 0; i < length; i++) {
			XYSeriesRenderer r = new XYSeriesRenderer();
			r.setColor(colors[i]);
			r.setPointStyle(styles[i]);
			renderer.addSeriesRenderer(r);
		}
	}

	private void setChartSettings(XYMultipleSeriesRenderer renderer,
			String title, String xTitle, String yTitle, double xMin,
			double xMax, double yMin, double yMax, int axesColor,
			int labelsColor) {
		renderer.setChartTitle(title);
		renderer.setXTitle(xTitle);
		renderer.setYTitle(yTitle);
		renderer.setXAxisMin(xMin);
		renderer.setXAxisMax(xMax);
		renderer.setYAxisMin(yMin);
		renderer.setYAxisMax(yMax);
		renderer.setAxesColor(axesColor);
		renderer.setLabelsColor(labelsColor);
	}
}

ほとんどデモソースそのまま。 チャートを見やすくするためにプロットするデータは簡略化した。 また、リアルタイム更新処理を実装するので、ラインチャートの表示はIntentをstartActivityするのではなく、ラインチャートのGraphicalViewを生成してインスタンス変数として保持しておき、Activityに貼り付けた。 この状態だと静的なラインチャートが表示されるだけで、更新処理はまだ実装されていない。 更新処理の実装はラインチャートのデータセットに追加表示したいデータをセットし、チャートを再描画すれば良い。 実に簡単。 当たり前だが、更新処理は非同期で行いたい場面がほとんどのはずなのでHandlerを使用する。 インスタンス変数に以下のコードを追加。

	private double addX = 6;
	private double plus = 6;
	private double minus = 13;
	private Handler handler = new Handler();
	private Runnable updateRunnable = new Runnable() {
		@Override
		public void run() {
			dataset.getSeriesAt(0).add(addX, plus);
			dataset.getSeriesAt(1).add(addX, minus);
			addX++;
			plus++;
			minus--;
			graphicalView.repaint();
			if (addX < 20) handler.postDelayed(updateRunnable, 1000);
		}
	};

8〜9行目、チャートデータのdatasetにデータを追加する。
addXは横軸のメモリ、plus, minusが追加するデータ。
13行目、チャートを再描画。
14行目、横軸のメモリが20より小さければ、1秒後に再度データの更新。
これでデータの更新処理は実装できたので、onCreate()の一番最後の行に以下の1行を追加。

handler.postDelayed(updateRunnable, 1000);

これで、アプリ起動後に非同期でチャートが更新される。
今回は1秒ごとに定期的に更新させているが、実際に使用するシーンでは、ネット、Bluetooth、ソケット等々を経由して更新すべきデータを受信した際に、その都度チャートを更新するような感じになると思う。

ソース

起動中プロセスの状態

Androidにおける起動中のプロセスの状態を知りたい時はRunningAppProcessInfoから知ることができる。
バックグラウンドプロセスをkillする場合等で状態チェックが必要な時に使える。
以下、ソースコード。

package com.nalabjp.example.rapi;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

import android.app.Activity;
import android.app.ActivityManager;
import android.app.ActivityManager.RunningAppProcessInfo;
import android.content.Context;
import android.os.Bundle;
import android.widget.ScrollView;
import android.widget.TextView;

public class RunningAppProcessInfoExampleActivity extends Activity {
    
    private ActivityManager manager;
    private ScrollView sc;
    private TextView tv;
    private static Map<Integer, String> importance = new HashMap<Integer, String>();
    private static Map<Integer, String> reason = new HashMap<Integer, String>();
    
    static {
    	importance.put(RunningAppProcessInfo.IMPORTANCE_FOREGROUND, "IMPORTANCE_FOREGROUND");
    	importance.put(RunningAppProcessInfo.IMPORTANCE_PERCEPTIBLE, "IMPORTANCE_PERCEPTIBLE");
    	importance.put(RunningAppProcessInfo.IMPORTANCE_VISIBLE, "IMPORTANCE_VISIBLE");
    	importance.put(RunningAppProcessInfo.IMPORTANCE_SERVICE, "IMPORTANCE_SERVICE");
    	importance.put(RunningAppProcessInfo.IMPORTANCE_BACKGROUND, "IMPORTANCE_BACKGROUND");
    	importance.put(RunningAppProcessInfo.IMPORTANCE_EMPTY, "IMPORTANCE_EMPTY");
    	reason.put(RunningAppProcessInfo.REASON_PROVIDER_IN_USE, "REASON_PROVIDER_IN_USE");
    	reason.put(RunningAppProcessInfo.REASON_SERVICE_IN_USE, "REASON_SERVICE_IN_USE");
    	reason.put(RunningAppProcessInfo.REASON_UNKNOWN, "UNKNOWN");
    }
	
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        manager = (ActivityManager)getSystemService(Context.ACTIVITY_SERVICE);
        sc = new ScrollView(this);
        tv  = new TextView(this);
    	sc.addView(tv);
    	setContentView(sc);
    }
    
    @Override
    public void onResume() {
    	super.onResume();
    	searchRunningAppProcesses();
    }
    
    private void searchRunningAppProcesses() {
    	final List<RunningAppProcessInfo> apps = manager.getRunningAppProcesses();
    	StringBuilder sb = new StringBuilder();
    	for (RunningAppProcessInfo rapi : apps) {
    		StringBuilder tmp = new StringBuilder();
    		tmp.append("processName : ").append(rapi.processName).append("\n");
    		tmp.append("importance : ").append(importance.get(rapi.importance)).append("\n");
    		tmp.append("importanceResonCode : ").append(reason.get(rapi.importanceReasonCode)).append("\n");
    		tmp.append("------\n");
    		sb.append(tmp);
    	}
    	tv.setText(sb.toString());
    }
}

結果はこんな感じ。

android.intent.action.BOOT_COMPLETED

android.intent.action.BOOT_COMPLETEDをintent-filterに設定していても、BOOT_COMPLETEDを検知できないことがある。
ググってみたらこんな記事が。
http://commonsware.com/blog/2011/07/13/boot-completed-regression-confirmed.html
どうやらAndroid3.1かららしい。
インストール後に一度もユーザが起動させていない場合はBOOT_COMPLETEDを拾えないようだ。
インストール直後はstoppedというstateになっているらしく、一度でもユーザが起動しているとstoppedのstateではなくなるみたい。
http://developer.android.com/intl/ja/sdk/android-3.1.html#launchcontrols
セキュリティ上の問題なのかな?

因みにandroid.intent.action.BOOT_COMPLETEDに必須のパーミッションであるandroid.permission.RECEIVE_BOOT_COMPLETEDにバグがある模様。
パーミッションがなくてもandroid.intent.action.BOOT_COMPLETEDはintent-filterに引っかかるとかで、検証したら確かにパーミッション不要でした。
http://code.google.com/p/android/issues/detail?id=14044
ICSでfixされる模様だけどどうなったんだろ?

ActivityManagerとActivityManagerNative

「フォアグラウンドプロセスとは別のプロセスからフォアグラウンドプロセスを終了させる」
という要求があったのでActivityManager関連を調べてみた。

ActivityManager#killBackgroundProcessesを使用すればバックグラウンドのプロセスがkillできるので、フォアグラウンドプロセスをバックグラウンドに移行させてやればよい。
フォアグラウンドプロセスとは別のプロセスが、フォアグラウンドプロセスをバックグラウンドに移行させる公開APIは存在しないようだが、ActivityManager#moveTaskToFrontを使って一つ前にフォアグラウンドにいたプロセスをフォアグラウンドに持ってきてやればよさそうだ。

しかし、このAPIはAPILevel11(Android3.0)からしか使えないようだ。
ではAPILevel10以前ではどうすればいいか?
ActivityManagerのソースを読んでみるとmoveTaskToFrontの実装は次のようになっていた。

public void moveTaskToFront(int taskId, int flags) {
    try {
        ActivityManagerNative.getDefault().moveTaskToFront(taskId, flags);
    } catch (RemoteException e) {
        // System dead, we will be dead too soon!
    }
}

ActivityManagerNative.getDefault()の戻り値はIActivityManagerで、実体はActivityManagerProxyになっている。
このインタフェース自体は非公開ではあるが、実装さえあればリフレクションで呼び出すことは可能なので、APILevel4(Android1.6)のソースでgrepしてみたら見事にヒットした。
結構昔から存在してたのね。
ということで、APILevel10以前の環境でも非公開だが使用することは可能であるという結論に。

因みに、moveTaskToBack(int task),killApplicationWithUid(String pkg, int uid)なんてメソッドもある。
APILevel5(Android2.0)にはkillApplicationProcess(String processName, int uid)というメソッドも追加されている。
しかし、killApplication〜のメソッドは、起動中のActivityから自分自身のパッケージ名とProcess.myUid()で取得できるUIDを渡したがSecurityExceptionが発生して動作しなかった。
指定したUIDではkillできないようだったが、起動しているプロセスのUIDと同じだったのでダメな理由がよく分からなかった。。。

追記
プロセスをバックグラウンドに移行させても、バックグラウンドに移行するのに数100msecかかってしまい、killしようとしてもタイミング的にkillできない場合がある。
そんな時はプロセスの状態を確認して確実にバックグラウンドに移行したのを確認してからkillしてやればよい。

%d人のブロガーが「いいね」をつけました。