package com.tns; import java.io.File; import java.io.IOException; import java.nio.ByteBuffer; import java.lang.ref.WeakReference; import java.lang.reflect.Array; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Queue; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.atomic.AtomicInteger; import android.os.Handler; import android.os.HandlerThread; import android.os.Looper; import android.os.Message; import android.os.Process; import android.util.SparseArray; import com.tns.bindings.ProxyGenerator; public class Runtime { private native void initNativeScript(int runtimeId, String filesPath, String nativeLibDir, boolean verboseLoggingEnabled, boolean isDebuggable, String packageName, Object[] v8Options, String callingDir); private native void runModule(int runtimeId, String filePath) throws NativeScriptException; private native void runWorker(int runtimeId, String filePath) throws NativeScriptException; private native Object runScript(int runtimeId, String filePath) throws NativeScriptException; private native Object callJSMethodNative(int runtimeId, int javaObjectID, String methodName, int retType, boolean isConstructor, Object... packagedArgs) throws NativeScriptException; private native void createJSInstanceNative(int runtimeId, Object javaObject, int javaObjectID, String canonicalName); private native int generateNewObjectId(int runtimeId); private native boolean notifyGc(int runtimeId); private native void passUncaughtExceptionToJsNative(int runtimeId, Throwable ex, String stackTrace); private native void clearStartupData(int runtimeId); public static native int getPointerSize(); private static native void WorkerGlobalOnMessageCallback(int runtimeId, String message); private static native void WorkerObjectOnMessageCallback(int runtimeId, int workerId, String message); private static native void TerminateWorkerCallback(int runtimeId); private static native void ClearWorkerPersistent(int runtimeId, int workerId); private static native void CallWorkerObjectOnErrorHandleMain(int runtimeId, int workerId, String message, String stackTrace, String filename, int lineno, String threadName) throws NativeScriptException; void passUncaughtExceptionToJs(Throwable ex, String stackTrace) { passUncaughtExceptionToJsNative(getRuntimeId(), ex, stackTrace); } private boolean initialized; private final static HashMap<String, Class<?>> classCache = new HashMap<String, Class<?>>(); private final static HashSet<ClassLoader> classLoaderCache = new HashSet<ClassLoader>(); private final static String FAILED_CTOR_RESOLUTION_MSG = "Check the number and type of arguments.\n" + "Primitive types need to be manually wrapped in their respective Object wrappers.\n" + "If you are creating an instance of an inner class, make sure to always provide reference to the outer `this` as the first argument."; private final SparseArray<Object> strongInstances = new SparseArray<Object>(); private final SparseArray<WeakReference<Object>> weakInstances = new SparseArray<WeakReference<Object>>(); private final NativeScriptHashMap<Object, Integer> strongJavaObjectToID = new NativeScriptHashMap<Object, Integer>(); private final NativeScriptWeakHashMap<Object, Integer> weakJavaObjectToID = new NativeScriptWeakHashMap<Object, Integer>(); private final Map<Class<?>, JavaScriptImplementation> loadedJavaScriptExtends = new HashMap<Class<?>, JavaScriptImplementation>(); private final java.lang.Runtime dalvikRuntime = java.lang.Runtime.getRuntime(); private final Object keyNotFoundObject = new Object(); private int currentObjectId = -1; private ExtractPolicy extractPolicy; private ArrayList<Constructor<?>> ctorCache = new ArrayList<Constructor<?>>(); private Logger logger; private ThreadScheduler threadScheduler; private DexFactory dexFactory; private final ClassResolver classResolver; private final GcListener gcListener; private final static Comparator<Method> methodComparator = new Comparator<Method>() { public int compare(Method lhs, Method rhs) { return lhs.getName().compareTo(rhs.getName()); } }; private final StaticConfiguration config; private static StaticConfiguration staticConfiguration; private final DynamicConfiguration dynamicConfig; private final int runtimeId; private boolean isTerminating; /* Used to map to Handler, for messaging between Main and the other Workers */ private final int workerId; /* Used by all Worker threads to communicate with the Main thread */ private Handler mainThreadHandler; private static AtomicInteger nextRuntimeId = new AtomicInteger(0); private final static ThreadLocal<Runtime> currentRuntime = new ThreadLocal<Runtime>(); private final static Map<Integer, Runtime> runtimeCache = new ConcurrentHashMap<>(); public static Map<Integer, ConcurrentLinkedQueue<Message>> pendingWorkerMessages = new ConcurrentHashMap<>(); /* Holds reference to all Worker Threads' handlers Note: Should only be used on the main thread */ private Map<Integer, Handler> workerIdToHandler = new HashMap<>(); public Runtime(StaticConfiguration config, DynamicConfiguration dynamicConfiguration) { synchronized (Runtime.currentRuntime) { Runtime existingRuntime = currentRuntime.get(); if (existingRuntime != null) { throw new NativeScriptException("There is an existing runtime on this thread with id=" + existingRuntime.getRuntimeId()); } this.runtimeId = nextRuntimeId.getAndIncrement(); this.config = config; this.dynamicConfig = dynamicConfiguration; this.threadScheduler = dynamicConfiguration.myThreadScheduler; this.workerId = dynamicConfiguration.workerId; if (dynamicConfiguration.mainThreadScheduler != null) { this.mainThreadHandler = dynamicConfiguration.mainThreadScheduler.getHandler(); } classResolver = new ClassResolver(this); currentRuntime.set(this); runtimeCache.put(this.runtimeId, this); gcListener = GcListener.getInstance(config.appConfig.getGcThrottleTime(), config.appConfig.getMemoryCheckInterval(), config.appConfig.getFreeMemoryRatio()); } } public int getRuntimeId() { return this.runtimeId; } public static Runtime getCurrentRuntime() { Runtime runtime = currentRuntime.get(); return runtime; } private static Runtime getObjectRuntime(Object object) { Runtime runtime = null; for (Runtime r : runtimeCache.values()) { if (r.getJavaObjectID(object) != null) { runtime = r; break; } } return runtime; } public DynamicConfiguration getDynamicConfig() { return dynamicConfig; } public static boolean isInitialized() { Runtime runtime = Runtime.getCurrentRuntime(); return (runtime != null) ? runtime.isInitializedImpl() : false; } public int getWorkerId() { return workerId; } public Handler getHandler() { return this.threadScheduler.getHandler(); } private static class WorkerThreadHandler extends Handler { @Override public void handleMessage(Message msg) { Runtime currentRuntime = Runtime.getCurrentRuntime(); if (currentRuntime.isTerminating) { if (currentRuntime.logger.isEnabled()) { currentRuntime.logger.write("Worker(id=" + currentRuntime.workerId + ") is terminating, it will not process the message."); } return; } /* Handle messages coming from the Main thread */ if (msg.arg1 == MessageType.MainToWorker) { /* Calls the Worker script's onmessage implementation with arg -> msg.obj.toString() */ WorkerGlobalOnMessageCallback(currentRuntime.runtimeId, msg.obj.toString()); } else if (msg.arg1 == MessageType.TerminateThread) { currentRuntime.isTerminating = true; currentRuntime.gcListener.unsubscribe(currentRuntime); runtimeCache.remove(currentRuntime.runtimeId); TerminateWorkerCallback(currentRuntime.runtimeId); if (currentRuntime.logger.isEnabled()) { currentRuntime.logger.write("Worker(id=" + currentRuntime.workerId + ", name=\"" + Thread.currentThread().getName() + "\") has terminated execution. Don't make further function calls to it."); } this.getLooper().quit(); } else if (msg.arg1 == MessageType.TerminateAndCloseThread) { Message msgToMain = Message.obtain(); msgToMain.arg1 = MessageType.CloseWorker; msgToMain.arg2 = currentRuntime.workerId; currentRuntime.mainThreadHandler.sendMessage(msgToMain); currentRuntime.isTerminating = true; currentRuntime.gcListener.unsubscribe(currentRuntime); runtimeCache.remove(currentRuntime.runtimeId); TerminateWorkerCallback(currentRuntime.runtimeId); if (currentRuntime.logger.isEnabled()) { currentRuntime.logger.write("Worker(id=" + currentRuntime.workerId + ", name=\"" + Thread.currentThread().getName() + "\") has terminated execution. Don't make further function calls to it."); } this.getLooper().quit(); } } } private static class WorkerThread extends HandlerThread { private Integer workerId; private ThreadScheduler mainThreadScheduler; private String filePath; private String callingJsDir; public WorkerThread(String name, Integer workerId, ThreadScheduler mainThreadScheduler, String callingJsDir) { super("W" + workerId + ": " + name); this.filePath = name; this.workerId = workerId; this.mainThreadScheduler = mainThreadScheduler; this.callingJsDir = callingJsDir; } public void startRuntime() { Handler handler = new Handler(this.getLooper()); handler.post((new Runnable() { @Override public void run() { Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); WorkThreadScheduler workThreadScheduler = new WorkThreadScheduler(new WorkerThreadHandler()); DynamicConfiguration dynamicConfiguration = new DynamicConfiguration(workerId, workThreadScheduler, mainThreadScheduler, callingJsDir); if (staticConfiguration.logger.isEnabled()) { staticConfiguration.logger.write("Worker (id=" + workerId + ")'s Runtime is initializing!"); } Runtime runtime = initRuntime(dynamicConfiguration); if (staticConfiguration.logger.isEnabled()) { staticConfiguration.logger.write("Worker (id=" + workerId + ")'s Runtime initialized!"); } /* Send a message to the Main Thread to `shake hands`, Main Thread will cache the Worker Handler for later use */ Message msg = Message.obtain(); msg.arg1 = MessageType.Handshake; msg.arg2 = runtime.runtimeId; runtime.mainThreadHandler.sendMessage(msg); runtime.runWorker(runtime.runtimeId, filePath); runtime.processPendingMessages(); } })); } } private void processPendingMessages() { Queue<Message> messages = Runtime.pendingWorkerMessages.get(this.getWorkerId()); if (messages == null) { return; } Handler handler = this.getHandler(); while (!messages.isEmpty()) { handler.sendMessage(messages.poll()); } } private static class MainThreadHandler extends Handler { public MainThreadHandler(Looper looper) { super(looper); } @Override public void handleMessage(Message msg) { /* Handle messages coming from a Worker thread */ if (msg.arg1 == MessageType.WorkerToMain) { /* Calls the Worker (with id - workerId) object's onmessage implementation with arg -> msg.obj.toString() */ WorkerObjectOnMessageCallback(Runtime.getCurrentRuntime().runtimeId, msg.arg2, msg.obj.toString()); } /* Handle a 'Handshake' message sent from a new Worker, so that the Main may cache it and send messages to it later */ else if (msg.arg1 == MessageType.Handshake) { int senderRuntimeId = msg.arg2; Runtime workerRuntime = runtimeCache.get(senderRuntimeId); Runtime mainRuntime = Runtime.getCurrentRuntime(); // If worker has had its close/terminate called before the threads could shake hands if (workerRuntime == null) { if (mainRuntime.logger.isEnabled()) { mainRuntime.logger.write("Main thread couldn't shake hands with worker (runtimeId: " + workerRuntime + ") because it has been terminated!"); } return; } /* Main thread now has a reference to the Worker's handler, so messaging between the two threads can begin */ mainRuntime.workerIdToHandler.put(workerRuntime.getWorkerId(), workerRuntime.getHandler()); if (mainRuntime.logger.isEnabled()) { mainRuntime.logger.write("Worker thread (workerId:" + workerRuntime.getWorkerId() + ") shook hands with the main thread!"); } } else if (msg.arg1 == MessageType.CloseWorker) { Runtime currentRuntime = Runtime.getCurrentRuntime(); // remove reference to a Worker thread's handler that is in the process of closing currentRuntime.workerIdToHandler.put(msg.arg2, null); ClearWorkerPersistent(currentRuntime.runtimeId, msg.arg2); } /* Handle unhandled exceptions/errors coming from the worker thread */ else if (msg.arg1 == MessageType.BubbleUpException) { Runtime currentRuntime = Runtime.getCurrentRuntime(); int workerId = msg.arg2; JavaScriptErrorMessage errorMessage = (JavaScriptErrorMessage) msg.obj; CallWorkerObjectOnErrorHandleMain(currentRuntime.runtimeId, workerId, errorMessage.getMessage(), errorMessage.getStackTrace(), errorMessage.getFilename(), errorMessage.getLineno(), errorMessage.getThreadName()); } } } /* This method initializes the runtime and should always be called first and through the main thread in order to set static configuration that all following workers can use */ public static Runtime initializeRuntimeWithConfiguration(StaticConfiguration config) { staticConfiguration = config; WorkThreadScheduler mainThreadScheduler = new WorkThreadScheduler(new MainThreadHandler(Looper.myLooper())); DynamicConfiguration dynamicConfiguration = new DynamicConfiguration(0, mainThreadScheduler, null); Runtime runtime = initRuntime(dynamicConfiguration); return runtime; } /* This method should be called via native code only after the static configuration has been initialized. It will use the static configuration for all following calls to initialize a new runtime. */ @RuntimeCallable public static void initWorker(String jsFileName, String callingJsDir, int id) { // This method will always be called from the Main thread Runtime runtime = Runtime.getCurrentRuntime(); ThreadScheduler mainThreadScheduler = runtime.getDynamicConfig().myThreadScheduler; WorkerThread worker = new WorkerThread(jsFileName, id, mainThreadScheduler, callingJsDir); worker.start(); worker.startRuntime(); } /* This method deals with initializing the runtime with given configuration Does it for both workers and for the main thread */ private static Runtime initRuntime(DynamicConfiguration dynamicConfiguration) { Runtime runtime = new Runtime(staticConfiguration, dynamicConfiguration); runtime.init(); return runtime; } private boolean isInitializedImpl() { return initialized; } public void init() { init(config.logger, config.appName, config.nativeLibDir, config.rootDir, config.appDir, config.classLoader, config.dexDir, config.dexThumb, config.appConfig, dynamicConfig.callingJsDir, config.isDebuggable); } private void init(Logger logger, String appName, String nativeLibDir, File rootDir, File appDir, ClassLoader classLoader, File dexDir, String dexThumb, AppConfig appConfig, String callingJsDir, boolean isDebuggable) throws RuntimeException { if (initialized) { throw new RuntimeException("NativeScriptApplication already initialized"); } this.logger = logger; this.dexFactory = new DexFactory(logger, classLoader, dexDir, dexThumb); if (logger.isEnabled()) { logger.write("Initializing NativeScript JAVA"); } try { Module.init(logger, rootDir, appDir); } catch (IOException ex) { throw new RuntimeException("Fail to initialize Require class", ex); } initNativeScript(getRuntimeId(), Module.getApplicationFilesPath(), nativeLibDir, logger.isEnabled(), isDebuggable, appName, appConfig.getAsArray(), callingJsDir); clearStartupData(getRuntimeId()); // It's safe to delete the data after the V8 debugger is initialized if (logger.isEnabled()) { Date d = new Date(); int pid = android.os.Process.myPid(); File f = new File("/proc/" + pid); Date lastModDate = new Date(f.lastModified()); logger.write("init time=" + (d.getTime() - lastModDate.getTime())); } gcListener.subscribe(this); initialized = true; } @RuntimeCallable public void enableVerboseLogging() { logger.setEnabled(true); ProxyGenerator.IsLogEnabled = true; } @RuntimeCallable public void disableVerboseLogging() { logger.setEnabled(false); ProxyGenerator.IsLogEnabled = false; } public void run() throws NativeScriptException { String mainModule = Module.bootstrapApp(); runModule(new File(mainModule)); } public void runModule(File jsFile) throws NativeScriptException { String filePath = jsFile.getPath(); runModule(getRuntimeId(), filePath); } public Object runScript(File jsFile) throws NativeScriptException { Object result = null; if (jsFile.exists() && jsFile.isFile()) { final String filePath = jsFile.getAbsolutePath(); boolean isWorkThread = threadScheduler.getThread().equals(Thread.currentThread()); if (isWorkThread) { result = runScript(getRuntimeId(), filePath); } else { final Object[] arr = new Object[2]; Runnable r = new Runnable() { @Override public void run() { synchronized (this) { try { arr[0] = runScript(getRuntimeId(), filePath); } finally { this.notify(); arr[1] = Boolean.TRUE; } } } }; boolean success = threadScheduler.post(r); if (success) { synchronized (r) { try { if (arr[1] == null) { r.wait(); } } catch (InterruptedException e) { result = e; } } } } } return result; } @RuntimeCallable private Class<?> resolveClass(String fullClassName, String[] methodOverrides, String[] implementedInterfaces, boolean isInterface) throws ClassNotFoundException, IOException { Class<?> javaClass = classResolver.resolveClass(fullClassName, dexFactory, methodOverrides, implementedInterfaces, isInterface); return javaClass; } @RuntimeCallable private long getUsedMemory() { long usedMemory = dalvikRuntime.totalMemory() - dalvikRuntime.freeMemory(); return usedMemory; } public void notifyGc() { notifyGc(runtimeId); } public static void initInstance(Object instance) { Runtime runtime = Runtime.getCurrentRuntime(); int objectId = runtime.currentObjectId; if (objectId != -1) { runtime.makeInstanceStrong(instance, objectId); } else { runtime.createJSInstance(instance); } } private void createJSInstance(Object instance) { int javaObjectID = generateNewObjectId(getRuntimeId()); makeInstanceStrong(instance, javaObjectID); Class<?> clazz = instance.getClass(); if (!loadedJavaScriptExtends.containsKey(clazz)) { JavaScriptImplementation jsImpl = clazz.getAnnotation(JavaScriptImplementation.class); if (jsImpl != null) { File jsFile = new File(jsImpl.javaScriptFile()); runModule(jsFile); } loadedJavaScriptExtends.put(clazz, jsImpl); } String className = clazz.getName(); createJSInstanceNative(getRuntimeId(), instance, javaObjectID, className); if (logger.isEnabled()) { logger.write("JSInstance for " + instance.getClass().toString() + " created with overrides"); } } @RuntimeCallable private static String[] getTypeMetadata(String className, int index) throws ClassNotFoundException { Class<?> clazz = classCache.get(className); if (clazz == null) { for (ClassLoader classLoader : classLoaderCache) { try { clazz = classLoader.loadClass(className); if (clazz != null) { classCache.put(className, clazz); break; } } catch (Exception e1) { e1.printStackTrace(); } } if (clazz == null) { clazz = Class.forName(className); classCache.put(className, clazz); } } String[] result = getTypeMetadata(clazz, index); return result; } private static String[] getTypeMetadata(Class<?> clazz, int index) { Class<?> mostOuterClass = clazz.getEnclosingClass(); ArrayList<Class<?>> outerClasses = new ArrayList<Class<?>>(); while (mostOuterClass != null) { outerClasses.add(0, mostOuterClass); Class<?> nextOuterClass = mostOuterClass.getEnclosingClass(); if (nextOuterClass == null) { break; } mostOuterClass = nextOuterClass; } Package p = (mostOuterClass != null) ? mostOuterClass.getPackage() : clazz.getPackage(); int packageCount = 1; String pname = p.getName(); for (int i = 0; i < pname.length(); i++) { if (pname.charAt(i) == '.') { ++packageCount; } } String name = clazz.getName(); String[] parts = name.split("[\\.\\$]"); int endIdx = parts.length; int len = endIdx - index; String[] result = new String[len]; int endOuterTypeIdx = packageCount + outerClasses.size(); for (int i = index; i < endIdx; i++) { if (i < packageCount) { result[i - index] = "P"; } else { if (i < endOuterTypeIdx) { result[i - index] = getTypeMetadata(outerClasses.get(i - packageCount)); } else { result[i - index] = getTypeMetadata(clazz); } } } return result; } private static String getTypeMetadata(Class<?> clazz) { StringBuilder sb = new StringBuilder(); if (clazz.isInterface()) { sb.append("I "); } else { sb.append("C "); } if (Modifier.isStatic(clazz.getModifiers())) { sb.append("S\n"); } else { sb.append("I\n"); } Class<?> baseClass = clazz.getSuperclass(); sb.append("B " + ((baseClass != null) ? baseClass.getName() : "").replace('.', '/') + "\n"); Method[] methods = clazz.getDeclaredMethods(); Arrays.sort(methods, methodComparator); for (Method m : methods) { int modifiers = m.getModifiers(); if (!Modifier.isStatic(modifiers) && (Modifier.isPublic(modifiers) || Modifier.isProtected(modifiers))) { sb.append("M "); sb.append(m.getName()); Class<?>[] params = m.getParameterTypes(); String sig = MethodResolver.getMethodSignature(m.getReturnType(), params); sb.append(" "); sb.append(sig); int paramCount = params.length; sb.append(" "); sb.append(paramCount); sb.append("\n"); } } Field[] fields = clazz.getDeclaredFields(); for (Field f : fields) { int modifiers = f.getModifiers(); if (!Modifier.isStatic(modifiers) && (Modifier.isPublic(modifiers) || Modifier.isProtected(modifiers))) { sb.append("F "); sb.append(f.getName()); sb.append(" "); String sig = MethodResolver.getTypeSignature(f.getType()); sb.append(sig); sb.append(" 0\n"); } } String ret = sb.toString(); return ret; } @RuntimeCallable private void makeInstanceStrong(Object instance, int objectId) { if (instance == null) { throw new IllegalArgumentException("instance cannot be null"); } int key = objectId; strongInstances.put(key, instance); strongJavaObjectToID.put(instance, key); Class<?> clazz = instance.getClass(); String className = clazz.getName(); if (!classCache.containsKey(className)) { classCache.put(className, clazz); ClassLoader clazzloader = clazz.getClassLoader(); if (!classLoaderCache.contains(clazzloader)) { classLoaderCache.add(clazzloader); } } if (logger != null && logger.isEnabled()) { logger.write("MakeInstanceStrong (" + key + ", " + instance.getClass().toString() + ")"); } } private void makeInstanceWeak(int javaObjectID, boolean keepAsWeak) { if (logger.isEnabled()) { logger.write("makeInstanceWeak instance " + javaObjectID + " keepAsWeak=" + keepAsWeak); } Object instance = strongInstances.get(javaObjectID); if (keepAsWeak) { weakJavaObjectToID.put(instance, Integer.valueOf(javaObjectID)); weakInstances.put(javaObjectID, new WeakReference<Object>(instance)); } strongInstances.delete(javaObjectID); strongJavaObjectToID.remove(instance); } @RuntimeCallable private void makeInstanceWeak(ByteBuffer buff, int length, boolean keepAsWeak) { buff.position(0); for (int i = 0; i < length; i++) { int javaObjectId = buff.getInt(); makeInstanceWeak(javaObjectId, keepAsWeak); } } @RuntimeCallable private void checkWeakObjectAreAlive(ByteBuffer input, ByteBuffer output, int length) { input.position(0); output.position(0); for (int i = 0; i < length; i++) { int javaObjectId = input.getInt(); WeakReference<Object> weakRef = weakInstances.get(javaObjectId); int isReleased; if (weakRef != null) { Object instance = weakRef.get(); if (instance == null) { isReleased = 1; weakInstances.delete(javaObjectId); } else { isReleased = 0; } } else { isReleased = (strongInstances.get(javaObjectId) == null) ? 1 : 0; } output.putInt(isReleased); } } @RuntimeCallable private Object getJavaObjectByID(int javaObjectID) throws Exception { if (logger.isEnabled()) { logger.write("Platform.getJavaObjectByID:" + javaObjectID); } Object instance = strongInstances.get(javaObjectID, keyNotFoundObject); if (instance == keyNotFoundObject) { WeakReference<Object> wr = weakInstances.get(javaObjectID); if (wr == null) { throw new NativeScriptException("No weak reference found. Attempt to use cleared object reference id=" + javaObjectID); } instance = wr.get(); if (instance == null) { throw new NativeScriptException("Attempt to use cleared object reference id=" + javaObjectID); } } // Log.d(DEFAULT_LOG_TAG, // "Platform.getJavaObjectByID found strong object with id:" + // javaObjectID); return instance; } private Integer getJavaObjectID(Object obj) { Integer id = strongJavaObjectToID.get(obj); if (id == null) { id = weakJavaObjectToID.get(obj); } return id; } @RuntimeCallable private int getOrCreateJavaObjectID(Object obj) { Integer result = getJavaObjectID(obj); if (result == null) { int objectId = generateNewObjectId(getRuntimeId()); makeInstanceStrong(obj, objectId); result = objectId; } return result; } // sends args in pairs (typeID, value, null) except for objects where its // (typeid, javaObjectID, javaJNIClassPath) public static Object callJSMethod(Object javaObject, String methodName, Class<?> retType, Object... args) throws NativeScriptException { return callJSMethod(javaObject, methodName, retType, false /* isConstructor */, args); } public static Object callJSMethodWithDelay(Object javaObject, String methodName, Class<?> retType, long delay, Object... args) throws NativeScriptException { return callJSMethod(javaObject, methodName, retType, false /* isConstructor */, delay, args); } public static Object callJSMethod(Object javaObject, String methodName, Class<?> retType, boolean isConstructor, Object... args) throws NativeScriptException { Object ret = callJSMethod(javaObject, methodName, retType, isConstructor, 0, args); return ret; } public static Object callJSMethod(Object javaObject, String methodName, boolean isConstructor, Object... args) throws NativeScriptException { return callJSMethod(javaObject, methodName, void.class, isConstructor, 0, args); } public static Object callJSMethod(Object javaObject, String methodName, Class<?> retType, boolean isConstructor, long delay, Object... args) throws NativeScriptException { Runtime runtime = Runtime.getCurrentRuntime(); if (runtime == null) { runtime = getObjectRuntime(javaObject); } return runtime.callJSMethodImpl(javaObject, methodName, retType, isConstructor, delay, args); } private Object callJSMethodImpl(Object javaObject, String methodName, Class<?> retType, boolean isConstructor, long delay, Object... args) throws NativeScriptException { Integer javaObjectID = getJavaObjectID(javaObject); if (javaObjectID == null) { throw new NativeScriptException("Cannot find object id for instance=" + ((javaObject == null) ? "null" : javaObject)); } if (logger.isEnabled()) { logger.write("Platform.CallJSMethod: calling js method " + methodName + " with javaObjectID " + javaObjectID + " type=" + ((javaObject != null) ? javaObject.getClass().getName() : "null")); } Object result = dispatchCallJSMethodNative(javaObjectID, methodName, isConstructor, delay, retType, args); return result; } // Packages args in format (typeID, value, null) except for objects where it is // (typeid, javaObjectID, canonicalName) // if javaObject has no javaObjecID meaning javascript object does not // exists for this object we assign one. private Object[] packageArgs(Object... args) { int len = (args != null) ? (args.length * 3) : 0; Object[] packagedArgs = new Object[len]; if (len > 0) { int jsArgsIndex = 0; for (int i = 0; i < args.length; i++) { Object value = args[i]; int typeId = TypeIDs.GetObjectTypeId(value); String javaClassPath = null; if (typeId == TypeIDs.JsObject) { javaClassPath = value.getClass().getName(); value = getOrCreateJavaObjectID(value); } packagedArgs[jsArgsIndex++] = typeId; packagedArgs[jsArgsIndex++] = value; packagedArgs[jsArgsIndex++] = javaClassPath; } } return packagedArgs; } static Class<?> getClassForName(String className) throws ClassNotFoundException { Class<?> clazz = classCache.get(className); if (clazz == null) { clazz = Class.forName(className); if (clazz != null) { classCache.put(className, clazz); } } return clazz; } @RuntimeCallable private String resolveConstructorSignature(Class<?> clazz, Object[] args) throws Exception { // Pete: cache stuff here, or in the cpp part if (logger.isEnabled()) { logger.write("resolveConstructorSignature: Resolving constructor for class " + clazz.getName()); } String res = MethodResolver.resolveConstructorSignature(clazz, args); if (res == null) { throw new Exception("Failed resolving constructor for class \'" + clazz.getName() + "\' with " + (args != null ? args.length : 0) + " parameters. " + FAILED_CTOR_RESOLUTION_MSG); } return res; } @RuntimeCallable private String resolveMethodOverload(String className, String methodName, Object[] args) throws Exception { if (logger.isEnabled()) { logger.write("resolveMethodOverload: Resolving method " + methodName + " on class " + className); } Class<?> clazz = getClassForName(className); String res = MethodResolver.resolveMethodOverload(clazz, methodName, args); if (logger.isEnabled()) { logger.write("resolveMethodOverload: method found :" + res); } if (res == null) { throw new Exception("Failed resolving method " + methodName + " on class " + className); } return res; } private Object[] extendConstructorArgs(String methodName, boolean isConstructor, Object[] args) { Object[] arr = null; if (methodName.equals("init")) { if (args == null) { arr = new Object[] {isConstructor}; } else { arr = new Object[args.length + 1]; System.arraycopy(args, 0, arr, 0, args.length); arr[arr.length - 1] = isConstructor; } } else { arr = args; } return arr; } private Object dispatchCallJSMethodNative(final int javaObjectID, final String methodName, boolean isConstructor, Class<?> retType, final Object[] args) throws NativeScriptException { return dispatchCallJSMethodNative(javaObjectID, methodName, isConstructor, 0, retType, args); } private Object dispatchCallJSMethodNative(final int javaObjectID, final String methodName, boolean isConstructor, long delay, Class<?> retType, final Object[] args) throws NativeScriptException { final int returnType = TypeIDs.GetObjectTypeId(retType); Object ret = null; boolean isWorkThread = threadScheduler.getThread().equals(Thread.currentThread()); final Object[] tmpArgs = extendConstructorArgs(methodName, isConstructor, args); if (isWorkThread) { Object[] packagedArgs = packageArgs(tmpArgs); ret = callJSMethodNative(getRuntimeId(), javaObjectID, methodName, returnType, isConstructor, packagedArgs); } else { final Object[] arr = new Object[2]; final boolean isCtor = isConstructor; Runnable r = new Runnable() { @Override public void run() { synchronized (this) { try { final Object[] packagedArgs = packageArgs(tmpArgs); arr[0] = callJSMethodNative(getRuntimeId(), javaObjectID, methodName, returnType, isCtor, packagedArgs); } finally { this.notify(); arr[1] = Boolean.TRUE; } } } }; if (delay > 0) { try { Thread.sleep(delay); } catch (InterruptedException e) { } } boolean success = threadScheduler.post(r); if (success) { synchronized (r) { try { if (arr[1] == null) { r.wait(); } } catch (InterruptedException e) { ret = e; } } } ret = arr[0]; } return ret; } @RuntimeCallable private static Class<?> getCachedClass(String className) { Class<?> clazz = classCache.get(className); return clazz; } @RuntimeCallable private Class<?> findClass(String className) throws ClassNotFoundException { Class<?> clazz = dexFactory.findClass(className); return clazz; } private void purgeAllProxies() { if (dexFactory == null) { return; } dexFactory.purgeAllProxies(); } @RuntimeCallable private static Object createArrayHelper(String arrayClassName, int size) throws ClassNotFoundException { Class<?> clazz = getClassForName(arrayClassName); Object arr = Array.newInstance(clazz, size); return arr; } @RuntimeCallable private static boolean useGlobalRefs() { int JELLY_BEAN = 16; boolean useGlobalRefs = android.os.Build.VERSION.SDK_INT >= JELLY_BEAN; return useGlobalRefs; } /* ====================================================================== ====================================================================== Workers messaging callbacks ====================================================================== ====================================================================== */ @RuntimeCallable public static void sendMessageFromMainToWorker(int workerId, String message) { Runtime currentRuntime = Runtime.getCurrentRuntime(); Message msg = Message.obtain(); msg.obj = message; msg.arg1 = MessageType.MainToWorker; boolean hasKey = currentRuntime.workerIdToHandler.containsKey(workerId); Handler workerHandler = currentRuntime.workerIdToHandler.get(workerId); // TODO: Pete: Ensure that we won't end up in an endless loop. Can we get an invalid workerId? /* If workHandler is null then the new Worker Thread still hasn't completed initializing OR The workHandler is null because it has been closed; Check if its key is still in the map */ if (workerHandler == null) { // Attempt to send a message to a closed worker, throw error or just log a message if (hasKey) { if (currentRuntime.logger.isEnabled()) { currentRuntime.logger.write("Worker(id=" + msg.arg2 + ") that you are trying to send a message to has been terminated. No message will be sent."); } return; } if (currentRuntime.logger.isEnabled()) { currentRuntime.logger.write("Worker(id=" + msg.arg2 + ")'s handler still not initialized. Requeueing message for Worker(id=" + msg.arg2 + ")"); } if (pendingWorkerMessages.get(workerId) == null) { pendingWorkerMessages.put(workerId, new ConcurrentLinkedQueue<Message>()); } Queue<Message> messages = pendingWorkerMessages.get(workerId); messages.add(msg); return; } if (!workerHandler.getLooper().getThread().isAlive()) { return; } workerHandler.sendMessage(msg); } @RuntimeCallable public static void sendMessageFromWorkerToMain(String message) { Runtime currentRuntime = Runtime.getCurrentRuntime(); Message msg = Message.obtain(); msg.arg1 = MessageType.WorkerToMain; /* Send the workerId associated with the JavaScript Worker object */ msg.arg2 = currentRuntime.getWorkerId(); msg.obj = message; currentRuntime.mainThreadHandler.sendMessage(msg); } @RuntimeCallable public static void workerObjectTerminate(int workerId) { // Thread should always be main here Runtime currentRuntime = Runtime.getCurrentRuntime(); final long ResendDelay = 1000; Message msg = Message.obtain(); boolean hasKey = currentRuntime.workerIdToHandler.containsKey(workerId); Handler workerHandler = currentRuntime.workerIdToHandler.get(workerId); msg.arg1 = MessageType.TerminateThread; msg.arg2 = workerId; /* If workHandler is null then the new Worker Thread still hasn't completed initializing OR The workHandler is null because it has been closed; Check if its key is still in the map */ if (workerHandler == null) { // Attempt to send a message to a closed worker, throw error or just log a message if (hasKey) { if (currentRuntime.logger.isEnabled()) { currentRuntime.logger.write("Worker(id=" + msg.arg2 + ") is already terminated. No message will be sent."); } return; } else { if (currentRuntime.logger.isEnabled()) { currentRuntime.logger.write("Worker(id=" + msg.arg2 + ")'s handler still not initialized. Requeueing terminate() message for Worker(id=" + msg.arg2 + ")"); } if (pendingWorkerMessages.get(workerId) == null) { pendingWorkerMessages.put(workerId, new ConcurrentLinkedQueue<Message>()); } Queue<Message> messages = pendingWorkerMessages.get(workerId); messages.add(msg); return; } } // Worker was closed during this 'terminate' call, nothing to do here if (!workerHandler.getLooper().getThread().isAlive()) { return; } // 'terminate' message must be executed immediately workerHandler.sendMessageAtFrontOfQueue(msg); // Set value for workerId key to null currentRuntime.workerIdToHandler.put(workerId, null); } @RuntimeCallable public static void workerScopeClose() { // Thread should always be a worker Runtime currentRuntime = Runtime.getCurrentRuntime(); Message msgToWorker = Message.obtain(); msgToWorker.arg1 = MessageType.TerminateAndCloseThread; currentRuntime.getHandler().sendMessageAtFrontOfQueue(msgToWorker); } @RuntimeCallable public static void passUncaughtExceptionFromWorkerToMain(String message, String filename, String stackTrace, int lineno) { // Thread should always be a worker Runtime currentRuntime = Runtime.getCurrentRuntime(); Message msg = Message.obtain(); msg.arg1 = MessageType.BubbleUpException; msg.arg2 = currentRuntime.workerId; String threadName = currentRuntime.getHandler().getLooper().getThread().getName(); JavaScriptErrorMessage error = new JavaScriptErrorMessage(message, stackTrace, filename, lineno, threadName); msg.obj = error; // TODO: Pete: Should we treat the message with higher priority? currentRuntime.mainThreadHandler.sendMessage(msg); } }