/*
* Protocoder
* A prototyping platform for Android devices
*
* Victor Diaz Barrales victormdb@gmail.com
*
* Copyright (C) 2014 Victor Diaz
* Copyright (C) 2013 Motorola Mobility LLC
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the Software
* is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
*/
package org.protocoderrunner.apprunner;
import android.app.Activity;
import android.util.Log;
import org.mozilla.javascript.Callable;
import org.mozilla.javascript.Context;
import org.mozilla.javascript.ContextFactory;
import org.mozilla.javascript.ErrorReporter;
import org.mozilla.javascript.Function;
import org.mozilla.javascript.RhinoException;
import org.mozilla.javascript.Scriptable;
import org.mozilla.javascript.ScriptableObject;
import org.mozilla.javascript.commonjs.module.Require;
import org.mozilla.javascript.debug.Debugger;
import java.util.concurrent.atomic.AtomicReference;
/**
* Derived from Droid Script :
* https://github.com/divineprog/droidscript Copyright (c) Mikael Kindborg 2010
* Source code license: MIT
*/
public class AppRunnerInterpreter {
private static final String TAG = "AppRunnerInterpreter";
static ScriptContextFactory contextFactory;
public Interpreter interpreter;
private final android.content.Context a;
private InterpreterInfo mListener;
static String scriptPrefix = "//Prepend text for all scripts \n" + "var window = this; \n";
static final String SCRIPT_POSTFIX = "//Appends text for all scripts \n" + "function onAndroidPause(){ } \n"
+ "// End of Append Section" + "\n";
public AppRunnerInterpreter(android.content.Context context) {
this.a = context;
}
public Object eval(final String code) {
return eval(code, "");
}
// since service doesnt use UIs we dont have to use runOnUiThread
public Object evalFromService(final String code) {
final AtomicReference<Object> result = new AtomicReference<Object>(null);
try {
result.set(interpreter.eval(code, ""));
} catch (Throwable e) {
reportError(e);
result.set(e);
}
return result.get();
}
public Object eval(final String code, final String sourceName) {
final AtomicReference<Object> result = new AtomicReference<Object>(null);
((Activity) a).runOnUiThread(new Runnable() {
@Override
public void run() {
try {
result.set(interpreter.eval(code, sourceName));
} catch (Throwable e) {
reportError(e);
result.set(e);
}
}
});
while (null == result.get()) {
Thread.yield();
}
return result.get();
}
/**
* This works because method is called from the "onXXX" methods which are
* called in the UI-thread. Thus, no need to use run on UI-thread. TODO:
* Could be a problem if someone calls it from another class, make private
* for now.
*/
Object callJsFunction(String funName, Object... args) {
try {
return interpreter.callJsFunction(funName, args);
} catch (Throwable e) {
reportError(e);
return false;
}
}
public void createInterpreter(boolean isActivity) {
// Initialize global mainScriptContext factory with our custom factory.
if (null == contextFactory) {
contextFactory = new ScriptContextFactory(this);
ContextFactory.initGlobal(contextFactory);
Log.i(TAG, "Creating ContextFactory");
}
contextFactory.setActivity(a);
if (null == interpreter) {
// Get the interpreter, if previously created in activity
if (isActivity) {
Object obj = ((Activity) a).getLastNonConfigurationInstance();
if (null == obj) {
// Create interpreter.
interpreter = new Interpreter();
} else {
// Restore interpreter state.
interpreter = (Interpreter) obj;
}
} else {
interpreter = new Interpreter();
}
}
interpreter.setActivity(a);
}
public void addDebugger(Debugger debugger) {
interpreter.mainScriptContext.setDebugger(debugger, interpreter.mainScriptContext);
}
interface InterpreterInfo {
void onError(String message);
}
public void addListener(InterpreterInfo listener) {
this.mListener = listener;
}
public void reportError(Object e) {
// Create error message.
String message = "";
if (e instanceof RhinoException) {
RhinoException error = (RhinoException) e;
message = error.getMessage() + " " + error.lineNumber() + " (" + error.columnNumber() + "): "
+ (error.sourceName() != null ? " " + error.sourceName() : "")
+ (error.lineSource() != null ? " " + error.lineSource() : "") + "\n" + error.getScriptStackTrace();
this.mListener.onError(message);
} else {
message = e.toString();
}
// Log the error message.
Log.i(TAG, "JavaScript Error: " + message);
}
public static String preprocess(String code) throws Exception {
return preprocessMultiLineStrings(extractCodeFromAppRunnerTags(code));
}
public static String extractCodeFromAppRunnerTags(String code) throws Exception {
String startDelimiter = "PROTOCODERSCRIPT_BEGIN";
String stopDelimiter = "PROTOCODERSCRIPT_END";
// Find start delimiter
int start = code.indexOf(startDelimiter, 0);
if (-1 == start) {
// No delimiter found, return code untouched
return code;
}
// Find stop delimiter
int stop = code.indexOf(stopDelimiter, start);
if (-1 == stop) {
// No delimiter found, return code untouched
return code;
}
// Extract the code between start and stop.
String result = code.substring(start + startDelimiter.length(), stop);
// Replace escaped characters with plain characters.
// TODO: Add more characters here
return result.replace("<", "<").replace(">", ">").replace(""", "\"");
}
public static String preprocessMultiLineStrings(String code) throws Exception {
StringBuilder result = new StringBuilder(code.length() + 1000);
String delimiter = "\"\"\"";
int lastStop = 0;
while (true) {
// Find next multiline delimiter
int start = code.indexOf(delimiter, lastStop);
if (-1 == start) {
// No delimiter found, append rest of the code
// to result and break
result.append(code.substring(lastStop, code.length()));
break;
}
// Find terminating delimiter
int stop = code.indexOf(delimiter, start + delimiter.length());
if (-1 == stop) {
// This is an error, throw an exception with error message
throw new Exception("Multiline string not terminated");
}
// Append the code from last stop up to the start delimiter
result.append(code.substring(lastStop, start));
// Set new lastStop
lastStop = stop + delimiter.length();
// Append multiline string converted to JavaScript code
result.append(convertMultiLineStringToJavaScript(code.substring(start + delimiter.length(), stop)));
}
return result.toString();
}
public static String convertMultiLineStringToJavaScript(String s) {
StringBuilder result = new StringBuilder(s.length() + 1000);
char quote = '\"';
char newline = '\n';
String backslashquote = "\\\"";
String concat = "\\n\" + \n\"";
result.append(quote);
for (int i = 0; i < s.length(); ++i) {
char c = s.charAt(i);
if (c == quote) {
result.append(backslashquote);
} else if (c == newline) {
result.append(concat);
} else {
result.append(c);
}
// Log.i("Multiline", result.toString());
}
result.append(quote);
return result.toString();
}
public String addInterface(Class c) {
String pkg = "Packages." + c.getName().toString();
String clsName = c.getSimpleName();
String c1 = "var " + clsName + " = " + pkg + "; \n";
String c2 = "var " + clsName.substring(1).toLowerCase() + "=" + clsName + "(Activity); \n";
String prefix = c1 + c2;
scriptPrefix += prefix;
return prefix;
}
public class Interpreter {
public Context mainScriptContext;
public Scriptable scope;
Require require;
public Interpreter() {
// Creates and enters a Context. The Context stores information
// about the execution environment of a script.
mainScriptContext = Context.enter();
mainScriptContext.getWrapFactory().setJavaPrimitiveWrap(false);
mainScriptContext.setOptimizationLevel(-1);
// Initialize the standard objects (Object, Function, etc.)
// This must be done before scripts can be executed. Returns
// a scope object that we use in later calls.
scope = mainScriptContext.initStandardObjects();
}
public Interpreter setActivity(android.content.Context a) {
// Set the global JavaScript variable Activity.
ScriptableObject.putProperty(scope, "Activity", Context.javaToJS(a, scope));
return this;
}
public Interpreter setErrorReporter(ErrorReporter reporter) {
mainScriptContext.setErrorReporter(reporter);
return this;
}
public void exit() {
Context.exit();
}
public Object eval(String code, String sourceName) throws Throwable {
String processedCode = preprocess(code);
return mainScriptContext.evaluateString(scope, processedCode, sourceName, 1, null);
}
public Object callJsFunction(String funName, Object... args) throws Throwable {
Object fun = scope.get(funName, scope);
if (fun instanceof Function) {
Log.i(TAG, "Calling JsFun " + funName);
Function f = (Function) fun;
Object result = f.call(mainScriptContext, scope, scope, args);
return Context.toString(result); // Why did I use this?
} else {
return null;
}
}
public void addObjectToInterface(String name, Object obj) {
ScriptableObject.putProperty(scope, name, Context.javaToJS(obj, scope));
}
}
public static class ScriptContextFactory extends ContextFactory {
android.content.Context c;
private final AppRunnerInterpreter appRunnerInterpreter;
ScriptContextFactory(AppRunnerInterpreter appRunnerInterpreter) {
this.appRunnerInterpreter = appRunnerInterpreter;
}
public ScriptContextFactory setActivity(android.content.Context c) {
this.c = c;
return this;
}
@Override
protected Object doTopCall(Callable callable, Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
try {
return super.doTopCall(callable, cx, scope, thisObj, args);
} catch (Throwable e) {
Log.i(TAG, "ContextFactory catched error: " + e);
if (null != c) {
appRunnerInterpreter.reportError(e);
}
return e;
}
}
}
}