Intent anywhere

1. はじめに

たまたま抱えていた問題が解決したタイミングで「Android Advent Calendar 2012」の案内が飛んでいたので思わずエントリーしてしまいました。ということで12月29日担当します@awwa500です。よろしくお願いいたします。

2. 概要

CGIからのIntentの送信について書きます。ネタは薄いです。

3. きっかけ

Androidのアプリ開発を初めて3年ちょっとくらいになりますが、Intentの仕組みを知った当時、衝撃を受けました。Intentが世界を変える、とまで思ったのはそれほど大げさではありません。
ところが、そんな信念を打ち砕かんとする事態が発生しました。今回はその辺の話を書いてみようと思います。

4. Intentは最強だよね

IntentについてググるとTechBoosterさんのブログがヒットします。Intentを使えば画面遷移も、アプリ間連携も、サービスの利用も、とにかくいろいろとできるわけです。

Intentで画面遷移する(明示的Intent)
Intentで画面遷移する(暗黙的Intent)
Serviceを使う(1) LocalServiceによる常駐型アプリ
JNIを介してNativeなコードからもIntentを投げることもできます。
Android: How to broadcast intent from native code?
IntentServiceと組み合わせるとキューも簡単に実装できちゃいます。
IntentServiceを使って非同期処理を行う
コマンドラインからもIntentを投げることもできちゃいます。
ターミナルからIntentを投げる

だんだんTechBoosterさんのリンク集みたいになってきましたね。このまま終わってしまってもいい感じになってきました。

5. 挫折

というわけで、どんな状況でもIntentがなんとかしてくれると思っていました。でもこれまでの情報では解決できない事態になりました。例えば、たまにAndroid上でApacheを動かしたくなることがありますよね?(ちょっとこの設定に無理があるような気がしますが、まあ、あるんです)このApacheが駆動するCGIと通常のAndroidアプリケーションを連携させたかったのです。でも、CGIなのでJNIEnvを参照できないためJNIは使えません。でも、amコマンドがあるじゃないか。CGIからamコマンド叩けば大丈夫、そう思っていました。

でもダメでした。orz
※CGIからamコマンドを叩いてIntentをbroadcastした時のLogCat
D/AndroidRuntime(1542): >>>>>> AndroidRuntime START com.android.internal.os.RuntimeInit <<<<<<
D/AndroidRuntime(1542): CheckJNI is OFF
E/dalvikvm(1542): ERROR: must specify non-'.' bootclasspath
W/dalvikvm(1542): CreateJavaVM failed: dvmClassStartup failed
E/AndroidRuntime(1542): JNI_CreateJavaVM failed

JavaVMの生成に失敗しています。エラーメッセージから察するにbootclasspath周りが期待した通りになっていないようです。 Intentを愛するものとしては由々しき事態です。

6. ところでamって?

ところでamコマンドの実体ってどうなってるんでしょう?amコマンドが何をやっているのか見てみる事にします。amコマンド自体はシェルスクリプトです(というか、今回初めて見ました)。中ではam.jarのAmクラスを指定して起動しているようです。

※Emulatorイメージ内の/system/bin/amの内容。
# Script to start "am" on the device, which has a very rudimentary
# shell.
#
base=/system
export CLASSPATH=$base/framework/am.jar
exec app_process $base/bin com.android.commands.am.Am "$@"

次にAmクラスのコードを見てみます。引数をActivityManagerNative経由でIntent発行しているようです。
※Androidソースの/frameworks/base/cmds/am配下を潜った先のAm.javaの内容。
自分自身をnewしてrun()を呼び出しています。
public static void main(String[] args) {
    try {
        (new Am()).run(args);
    } catch (IllegalArgumentException e) {
        showUsage();
        System.err.println("Error: " + e.getMessage());
    } catch (Exception e) {
        e.printStackTrace(System.err);
        System.exit(1);
    }
}
run()では引数をみて、それぞれの処理を実行しています。今回見たいのは"broadcast"です。
private void run(String[] args) throws Exception {
    if (args.length < 1) {
        showUsage();
        return;
    }

    mAm = ActivityManagerNative.getDefault();
    if (mAm == null) {
        System.err.println(NO_SYSTEM_ERROR_CODE);
        throw new AndroidException("Can't connect to activity manager; is the system running?");
    }

    mArgs = args;
    String op = args[0];
    mNextArg = 1;

    if (op.equals("start")) {
        runStart();
    } else if (op.equals("startservice")) {
        // :
    } else if (op.equals("broadcast")) {
        sendBroadcast();
        // :
    }
    // :
sendBroadcast()では、Intentを生成してbroadcastしています。なんか思ったより普通ですね。
private void sendBroadcast() throws Exception {
Intent intent = makeIntent();
    IntentReceiver receiver = new IntentReceiver();
    System.out.println("Broadcasting: " + intent);
    mAm.broadcastIntent(null, intent, null, receiver, 0, null, null, null, true, false);
    receiver.waitForFinish();
}

7. 解決

ここで改めてCGIからamコマンド起動時のエラーメッセージに立ち戻ります。bootclasspathが期待した状態になっていない。確かにCGIにしてみたらそんな環境変数のことなんか知らないはずです。
それなら環境変数を設定してやればいいはず、ということで、printenvした結果をamシェルスクリプトの冒頭に挿入したファイルを作成します。たぶん必要なのはBOOTCLASSPATH周りだけだと思いますが、諸事情(確認が面倒)により全部挿入します。雑ですみません。

※anywhere-amと名付けます。(anywhereと言いつつ、printenvした環境でしか動作する保証はありませんが)
# Script to start "am" on the device, which has a very rudimentary
# shell.
#
base=/system
export CLASSPATH=$base/framework/am.jar

export LD_LIBRARY_PATH=/vendor/lib:/system/lib
export HOSTNAME=android
export BOOTCLASSPATH=/system/framework/core.jar:/system/framework/core-junit.jar:/system/framework/bouncycastle.jar:/system/framework/ext.jar:/system/framework/framework.jar:/system/framework/android.policy.jar:/system/framework/services.jar:/system/framework/apache-xml.jar
export PATH=/sbin:/vendor/bin:/system/sbin:/system/bin:/system/xbin
export LOOP_MOUNTPOINT=/mnt/obb
export ANDROID_DATA=/data
export ANDROID_ROOT=/system
export SHELL=/system/bin/sh
export MKSH=/system/bin/sh
export USER=root
export ANDROID_PROPERTY_WORKSPACE=8,32768
export EXTERNAL_STORAGE=/mnt/sdcard
export ANDROID_ASSETS=/system/app
export TERM=vt100
export RANDOM=6011
export ASEC_MOUNTPOINT=/mnt/asec
export HOME=/data
export ANDROID_BOOTLOGO=1
export PS1=$(precmd)$USER@$HOSTNAME:${PWD:-?} #

exec app_process $base/bin com.android.commands.am.Am "$@" > /dev/null 2>> logs/am.log

#echo "end am shell"

anywhere-amコマンドをassetsに格納→インストール時にfilesに展開→chmodで実行権限付与とかすればアプリに組み込んで利用可能になります。もちろんarmビルドしたApacheもassetsに突っ込んで同様に展開します。
あとはBroadcastReceiver側の実装を普通にやって実行するだけです。
ちなみに、環境変数は/init.rcに書かれています。ここから必要な分だけロードするのが正攻法なんでしょうね。

8.おまけ

ところで、上記の流れの後でJava側からCGIに結果を返すにはどうすればいいのでしょう?さすがにIntentを投げ返すわけにもいかず、悩んだ末にIntentより最強のファイル渡しにしました。CGIがIntentを投げた後、特定のフォルダを見張っていて、Java側から結果をファイルに書き出す、というちょっとアレな方法です。Intentで解決できなくて悔しいです。他に何かいい方法ないですかね?
CGIの状態を気にしなくても良いのであればApacheにリクエストを投げつけても良いと思います。

Intentよりファイル渡しが最強みたいなオチになっちゃいましたね。まぁ、いいか。

9.最後に

楽しいイベントに参加させて頂きありがとうございました。主催の@youten_redoさんに感謝です。
また、本投稿の薄いネタを補強していただいたTechBoosterさんにも感謝です。
明日は@shonanshachuさん。今日の裏は@takkeさんです。



それでは良いお年を。

コメント

このブログの人気の投稿

Joinノードを使う(その1)

Execノードを使う

Joinノードを使う(その4)