目的

AndroidアプリにJavascriptエンジンを搭載する

背景

ユーザーがアプリ上の機能を自由に拡張できるような仕組みを作るため

解決策

Java上でJavaScriptを実行(eval)させる方法はいくつかあるが、下記のサイトを参考にDuktape Androidを使うことにした

comparison-shopping-searching-for-javascript-engines-for-android

duktape-android
基本的には上記の公式に従って導入すれば良い

依存関係の追加

  1. build.gradleの変更
implementation 'com.squareup.duktape:duktape-android:1.3.0'

とりあえずjavascriptを実行させる

自分はkotlinで書いているので下記になる

Duktape.create().use { x ->
    Log.d("Greeting", x.evaluate("'hello world'.toUpperCase();").toString())         }

javaの人は公式のままで良いはず

Duktape duktape = Duktape.create();
try {
  Log.d("Greeting", duktape.evaluate("'hello world'.toUpperCase();").toString());
} finally {
  duktape.close();
}

実行してみて下記のようなログが出ればとりあえず導入は成功

2018-10-29 17:09:36.801 19934-19934/? D/Greeting: HELLO WORLD

javaからjavascriptへ値を渡す

やり方は色々あるが、まずは単純に整数をjsへ渡してみる

Duktape.create().use { x ->
    x.set("test", Int::class.java, Integer.valueOf(100))
    Log.d("Greeting", x.evaluate("'hello world'.toUpperCase() + test;").toString())
}

として実行するが...

java.lang.UnsupportedOperationException: Only interfaces can be bound. Received: class java.lang.Integer

と出るので、どうもインタフェース経由でしかやり取りできないっぽい

なので、下記のようにインタフェース経由で値を渡すようにする

  1. インタフェースを定義
interface Test {
    fun test():Int
}
  1. オブジェクトを渡してjsで実行
    ここではとりあえずtest.test()を実行すると100を返す関数を定義
Duktape.create().use { x ->
    x.set("test", Test::class.java, object: Test{
        override fun test(): Int {
            return 100
        }
    })
    Log.d("Greeting", x.evaluate("'hello world'.toUpperCase() + test.test();").toString())
}
  1. 結果確認
2018-10-29 17:44:22.055 21881-21881/* D/Greeting: HELLO WORLD100

javaからjavascriptの実行結果を取得する

こちらもインタフェースとして渡す

  1. js側で実装するためのインタフェースを定義
    先程使ったインタフェースをそのまま使いまわす
interface Test {
    fun test():Int
}
  1. js側でオブジェクトを作成してグルーバル変数へ入れる
Duktape.create().use { x ->
    Log.d("Greeting", x.evaluate("var test = { test:function(){ return -1; } }; ").toString())
}
  1. java側で取得して実行
Log.d("Greeting", x.get("test", Test::class.java).test().toString())

が、エラーが出た

java.lang.IllegalArgumentException: In proxied method "test.test": Unsupported JavaScript return type int

javaからの引数としてサポートされているがjsからの戻り値としてintはサポートされていないようだ
なのでDoubleとして渡すことにする

インタフェースの戻り値をIntからDoubleへ

interface Test {
    fun test():Double
}

実行すると成功した

2018-10-29 17:57:53.896 22867-22867/* D/Greeting: -1.0

課題

セキュリティには注意したほうが良い
今回はユーザーを信用することを前提としているのであまり気を使っていないが、もし一般配布する場合は実行範囲に制限を設けるなどをしないと任意のコードを注入されることになるため危険
また成果物であるスクリプトを共有できるような仕組みがある場合は更に危ない