callFunctionMap = new HashMap<>();
+ private boolean destroyed = false;
+ private ModuleLoader moduleLoader;
+
+ private QuickJSContext() {
+ try {
+ runtime = createRuntime();
+ context = createContext(runtime);
+ } catch (UnsatisfiedLinkError e) {
+ throw new QuickJSException("The so library must be initialized before createContext! QuickJSLoader.init should be called on the Android platform. In the JVM, you need to manually call System.loadLibrary");
+ }
+ currentThreadId = Thread.currentThread().getId();
+ }
+
+ public static QuickJSContext create() {
+ return new QuickJSContext();
+ }
+
+ public void destroyContext() {
+ checkSameThread();
+
+ nativeCleaner.forceClean();
+ destroyContext(context);
+ }
+
+ public void setProperty(JSObject jsObj, String name, Object value) {
+ checkSameThread();
+
+ setProperty(context, jsObj.getPointer(), name, value);
+ }
+
+ public JSArray createNewJSArray() {
+ return (JSArray) parseJSON("[]");
+ }
+
+ public Object parseJSON(String s) {
+ return null;
+ }
+
+ public Object getProperty(JSObject jsObj, String name) {
+ checkSameThread();
+ return getProperty(context, jsObj.getPointer(), name);
+ }
+
+ public JSObject createNewJSObject() {
+ return (JSObject) parseJSON("{}");
+ }
+
+ public boolean isLiveObject(JSObject jsObj) {
+ return isLiveObject(runtime, jsObj.getPointer());
+ }
+
+ public void setConsole(final Console console) {
+ if (console == null) {
+ return;
+ }
+ JSObject consoleObject = createJSObject();
+ getGlobalObject().set("console", consoleObject);
+ consoleObject.set("log", new JSCallFunction() {
+ @Override
+ public Object call(Object... args) {
+ StringBuilder value = new StringBuilder();
+ for (int i = 0; i < args.length; i++) {
+ value.append(args[i]);
+ if (i < args.length - 1) {
+ value.append(" ");
+ }
+ }
+ console.log(value.toString());
+ return null;
+ }
+ });
+ }
+
+ public void setMaxStackSize(int maxStackSize) {
+ setMaxStackSize(runtime, maxStackSize);
+ }
+
+ public void runGC() {
+ runGC(runtime);
+ }
+
+ public void setMemoryLimit(int memoryLimitSize) {
+ setMemoryLimit(runtime, memoryLimitSize);
+ }
+
+ public void dumpMemoryUsage(File target) {
+ if (target == null || !target.exists()) {
+ return;
+ }
+
+ dumpMemoryUsage(runtime, target.getAbsolutePath());
+ }
+
+ // will use stdout to print.
+ public void dumpMemoryUsage() {
+ dumpMemoryUsage(runtime, null);
+ }
+
+ public void dumpObjects(File target) {
+ if (target == null || !target.exists()) {
+ return;
+ }
+
+ dumpObjects(runtime, target.getAbsolutePath());
+ }
+
+ // will use stdout to print.
+ public void dumpObjects() {
+ dumpObjects(runtime, null);
+ }
+
+ private void checkSameThread() {
+ boolean isSameThread = currentThreadId == Thread.currentThread().getId();
+ if (!isSameThread) {
+ throw new QuickJSException("Must be call same thread in QuickJSContext.create!");
+ }
+ }
+
+ public long getCurrentThreadId() {
+ return currentThreadId;
+ }
+
+ public ModuleLoader getModuleLoader() {
+ return moduleLoader;
+ }
+
+ public void setModuleLoader(ModuleLoader moduleLoader) {
+ checkSameThread();
+ checkDestroyed();
+
+ if (moduleLoader == null) {
+ throw new NullPointerException("The moduleLoader can not be null!");
+ }
+
+ this.moduleLoader = moduleLoader;
+ }
+
+ private void checkDestroyed() {
+ if (destroyed) {
+ throw new QuickJSException("Can not called this after QuickJSContext was destroyed!");
+ }
+ }
+
+ public JSObject getGlobalObject() {
+ checkSameThread();
+ checkDestroyed();
+ return getGlobalObject(context);
+ }
+
+ public void destroy() {
+ checkSameThread();
+ checkDestroyed();
+
+ nativeCleaner.forceClean();
+ callFunctionMap.clear();
+ destroyContext(context);
+ destroyed = true;
+ }
+
+ public String stringify(JSObject jsObj) {
+ checkSameThread();
+ checkDestroyed();
+ return stringify(context, jsObj.getPointer());
+ }
+
+ public boolean contains(JSObject jsObj, String key) {
+ checkSameThread();
+ checkDestroyed();
+ return contains(context, jsObj.getPointer(), key);
+ }
+
+ public String[] getKeys(JSObject jsObj) {
+ checkSameThread();
+ checkDestroyed();
+ return getKeys(context, jsObj.getPointer());
+ }
+
+ public void arrayAdd(JSObject jsObj, Object value) {
+ checkSameThread();
+ checkDestroyed();
+ arrayAdd(context, jsObj.getPointer(), value);
+ }
+
+ public Object get(JSObject jsObj, String name) {
+ checkSameThread();
+ checkDestroyed();
+ return getProperty(context, jsObj.getPointer(), name);
+ }
+
+ public void set(JSObject jsObj, String name, Object value) {
+ checkSameThread();
+ checkDestroyed();
+
+ if (value instanceof JSCallFunction) {
+ // Todo 优化:可以只传 callFunctionId 给到 JNI.
+ putCallFunction((JSCallFunction) value);
+ }
+
+ setProperty(context, jsObj.getPointer(), name, value);
+ }
+
+ private void putCallFunction(JSCallFunction callFunction) {
+ int callFunctionId = callFunction.hashCode();
+ callFunctionMap.put(callFunctionId, callFunction);
+ }
+
+ /**
+ * 该方法只提供给 Native 层回调.
+ *
+ * @param callFunctionId JSCallFunction 对象标识
+ */
+ public void removeCallFunction(int callFunctionId) {
+ callFunctionMap.remove(callFunctionId);
+ }
+
+ /**
+ * 该方法只提供给 Native 层回调.
+ *
+ * @param callFunctionId JSCallFunction 对象标识
+ * @param args JS 到 Java 的参数映射
+ */
+ public Object callFunctionBack(int callFunctionId, Object... args) {
+ checkSameThread();
+ checkDestroyed();
+
+ JSCallFunction callFunction = callFunctionMap.get(callFunctionId);
+ Object ret = callFunction.call(args);
+ if (ret instanceof JSCallFunction) {
+ putCallFunction((JSCallFunction) ret);
+ }
+
+ return ret;
+ }
+
+ public void freeValue(JSObject jsObj) {
+ checkSameThread();
+ checkDestroyed();
+
+ freeValue(context, jsObj.getPointer());
+ }
+
+ /**
+ * @VisibleForTesting 该方法仅供单元测试使用
+ */
+ int getCallFunctionMapSize() {
+ return callFunctionMap.size();
+ }
+
+ /**
+ * Native 层注册的 JS 方法里的对象需要在其他地方使用,
+ * 调用该方法进行计数加一增加引用,不然 JS 方法执行完会被回收掉。
+ * 注意:不再使用的时候,调用对应的 {@link #freeDupValue(JSObject)} 方法进行计数减一。
+ */
+ private void dupValue(JSObject jsObj) {
+ checkSameThread();
+ checkDestroyed();
+
+ dupValue(context, jsObj.getPointer());
+ }
+
+ /**
+ * 引用计数减一,对应 {@link #dupValue(JSObject)}
+ */
+ private void freeDupValue(JSObject jsObj) {
+ checkSameThread();
+ checkDestroyed();
+
+ freeDupValue(context, jsObj.getPointer());
+ }
+
+ public int length(JSArray jsArray) {
+ checkSameThread();
+ checkDestroyed();
+
+ return length(context, jsArray.getPointer());
+ }
+
+ public Object get(JSArray jsArray, int index) {
+ checkSameThread();
+ checkDestroyed();
+
+ return get(context, jsArray.getPointer(), index);
+ }
+
+ public void set(JSArray jsArray, Object value, int index) {
+ checkSameThread();
+ checkDestroyed();
+
+ set(context, jsArray.getPointer(), value, index);
+ }
+
+ Object call(JSObject func, long objPointer, Object... args) {
+ checkSameThread();
+ checkDestroyed();
+
+ for (Object arg : args) {
+ if (arg instanceof JSCallFunction) {
+ putCallFunction((JSCallFunction) arg);
+ }
+ }
+
+ return call(context, func.getPointer(), objPointer, args);
+ }
+
+ /**
+ * Automatically manage the release of objects,
+ * the hold method is equivalent to call the
+ * dupValue and freeDupValue methods with NativeCleaner.
+ */
+ public void hold(JSObject jsObj) {
+ checkSameThread();
+ checkDestroyed();
+
+ dupValue(jsObj);
+ nativeCleaner.register(jsObj, jsObj.getPointer());
+ }
+
+ public JSObject createJSObject() {
+ return (JSObject) parse("{}");
+ }
+
+ public JSArray createJSArray() {
+ return (JSArray) parse("[]");
+ }
+
+ public Object parse(String json) {
+ checkSameThread();
+ checkDestroyed();
+
+ return parseJSON(context, json);
+ }
+
+ public byte[] compile(String source) {
+ return compile(source, UNKNOWN_FILE);
+ }
+
+ public byte[] compile(String source, String fileName) {
+ checkSameThread();
+ checkDestroyed();
+
+ return compile(context, source, fileName, false);
+ }
+
+ public byte[] compileModule(String source) {
+ return compileModule(source, UNKNOWN_FILE);
+ }
+
+ public byte[] compileModule(String source, String fileName) {
+ checkSameThread();
+ checkDestroyed();
+
+ return compile(context, source, fileName, true);
+ }
+
+ public Object execute(byte[] code) {
+ return execute(code, UNKNOWN_FILE);
+ }
+
+ public Object execute(byte[] code, String fileName) {
+ return execute(context, code, fileName, "default");
+ }
+
+ public Object gexecute(byte[] code, String moduleExtName) {
+ return execute(context, code, UNKNOWN_FILE, moduleExtName);
+ }
+
+ public Object execute(byte[] code, String fileName, String moduleExtName) {
+ checkSameThread();
+ checkDestroyed();
+
+ if (TextUtils.isEmpty(moduleExtName)) {
+ moduleExtName = "default";
+ }
+ return execute(context, code, fileName, moduleExtName);
+ }
+
+ public Object evaluate(String script) {
+ return evaluate(script, UNKNOWN_FILE);
+ }
+
+ public Object evaluate(String script, String fileName) {
+ checkSameThread();
+ checkDestroyed();
+ return evaluate(context, script, fileName, "default", false);
+ }
+
+ public Object evaluateModule(String script, String fileName, String moduleExtName) {
+ checkSameThread();
+ checkDestroyed();
+
+ if (TextUtils.isEmpty(moduleExtName)) {
+ moduleExtName = "default";
+ }
+ return evaluate(context, script, fileName, moduleExtName, true);
+ }
+
+ public Object evaluateModule(String script, String fileName) {
+ return evaluate(context, script, fileName, "default", true);
+ }
+
+ public Object evaluateModule(String script) {
+ return evaluateModule(script, UNKNOWN_FILE);
+ }
+
+ public void throwJSException(String error) {
+ // throw $error;
+ String errorScript = "throw " + "\"" + error + "\"" + ";";
+ evaluate(errorScript);
+ }
+
+ // runtime
+ private native long createRuntime();
+
+ private native void setMaxStackSize(long runtime, int size); // The default is 1024 * 256, and 0 means unlimited.
+
+ private native boolean isLiveObject(long runtime, long objValue);
+
+ private native void runGC(long runtime);
+
+ private native void setMemoryLimit(long runtime, int size);
+
+ private native void dumpMemoryUsage(long runtime, String fileName);
+
+ private native void dumpObjects(long runtime, String fileName);
+
+ // context
+ private native long createContext(long runtime);
+
+ private native Object evaluate(long context, String script, String fileName, String moduleext_name, boolean isModule);
+
+ private native JSObject getGlobalObject(long context);
+
+ private native Object call(long context, long func, long thisObj, Object[] args);
+
+ private native Object getProperty(long context, long objValue, String name);
+
+ private native void setProperty(long context, long objValue, String name, Object value);
+
+ private native String stringify(long context, long objValue);
+
+ private native int length(long context, long objValue);
+
+ private native Object get(long context, long objValue, int index);
+
+ private native void set(long context, long objValue, Object value, int index);
+
+ private native void freeValue(long context, long objValue);
+
+ private native void dupValue(long context, long objValue);
+
+ private native void freeDupValue(long context, long objValue);
+
+ private native Object parseJSON(long context, String json);
+
+ private native byte[] compile(long context, String sourceCode, String fileName, boolean isModule); // Bytecode compile
+
+ private native Object execute(long context, byte[] bytecode, String fileName, String moduleext_name); // Bytecode execute
+
+ private native boolean contains(long context, long objValue, String key);
+
+ private native String[] getKeys(long context, long objValue);
+
+ private native void arrayAdd(long context, long objValue, Object value);
+
+ // destroy context and runtime
+ private native void destroyContext(long context);
+
+ public interface Console {
+ void log(String info);
+ }
+
+ public static abstract class DefaultModuleLoader extends ModuleLoader {
+ @Override
+ public boolean isBytecodeMode() {
+ return false;
+ }
+
+ @Override
+ public byte[] getModuleBytecode(String moduleName) {
+ return null;
+ }
+
+ @Override
+ public String moduleNormalizeName(String moduleBaseName, String moduleName) {
+ return UriUtil.resolve(moduleBaseName, moduleName);
+ }
+ }
+
+ public static abstract class BytecodeModuleLoader extends ModuleLoader {
+ @Override
+ public boolean isBytecodeMode() {
+ return true;
+ }
+
+ @Override
+ public String getModuleStringCode(String moduleName) {
+ return null;
+ }
+
+ @Override
+ public String moduleNormalizeName(String baseModuLeName, String moduleName) {
+ return UriUtil.resolve(baseModuLeName, moduleName);
+ }
+ }
+}
diff --git a/quickjs/src/main/java/com/whl/quickjs/wrapper/QuickJSException.java b/quickjs/src/main/java/com/whl/quickjs/wrapper/QuickJSException.java
new file mode 100644
index 0000000000..340eae4669
--- /dev/null
+++ b/quickjs/src/main/java/com/whl/quickjs/wrapper/QuickJSException.java
@@ -0,0 +1,22 @@
+package com.whl.quickjs.wrapper;
+
+/**
+ * Created by Harlon Wang on 2022/2/8.
+ */
+public class QuickJSException extends RuntimeException {
+
+ private final boolean jsError;
+
+ public QuickJSException(String message) {
+ this(message, false);
+ }
+
+ public QuickJSException(String message, boolean jsError) {
+ super(message);
+ this.jsError = jsError;
+ }
+
+ public boolean isJSError() {
+ return jsError;
+ }
+}
diff --git a/quickjs/src/main/java/com/whl/quickjs/wrapper/UriUtil.java b/quickjs/src/main/java/com/whl/quickjs/wrapper/UriUtil.java
new file mode 100644
index 0000000000..dd56109633
--- /dev/null
+++ b/quickjs/src/main/java/com/whl/quickjs/wrapper/UriUtil.java
@@ -0,0 +1,287 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.whl.quickjs.wrapper;
+
+import android.net.Uri;
+import android.text.TextUtils;
+
+/**
+ * Utility methods for manipulating URIs.
+ */
+public final class UriUtil {
+
+ /**
+ * The length of arrays returned by {@link #getUriIndices(String)}.
+ */
+ private static final int INDEX_COUNT = 4;
+ /**
+ * An index into an array returned by {@link #getUriIndices(String)}.
+ *
+ * The value at this position in the array is the index of the ':' after the scheme. Equals -1
+ * if the URI is a relative reference (no scheme). The hier-part starts at (schemeColon + 1),
+ * including when the URI has no scheme.
+ */
+ private static final int SCHEME_COLON = 0;
+ /**
+ * An index into an array returned by {@link #getUriIndices(String)}.
+ *
+ *
The value at this position in the array is the index of the path part. Equals (schemeColon +
+ * 1) if no authority part, (schemeColon + 3) if the authority part consists of just "//", and
+ * (query) if no path part. The characters starting at this index can be "//" only if the
+ * authority part is non-empty (in this case the double-slash means the first segment is empty).
+ */
+ private static final int PATH = 1;
+ /**
+ * An index into an array returned by {@link #getUriIndices(String)}.
+ *
+ *
The value at this position in the array is the index of the query part, including the '?'
+ * before the query. Equals fragment if no query part, and (fragment - 1) if the query part is a
+ * single '?' with no data.
+ */
+ private static final int QUERY = 2;
+ /**
+ * An index into an array returned by {@link #getUriIndices(String)}.
+ *
+ *
The value at this position in the array is the index of the fragment part, including the '#'
+ * before the fragment. Equal to the length of the URI if no fragment part, and (length - 1) if
+ * the fragment part is a single '#' with no data.
+ */
+ private static final int FRAGMENT = 3;
+
+ private UriUtil() {
+ }
+
+ /**
+ * Like {@link #resolve(String, String)}, but returns a {@link Uri} instead of a {@link String}.
+ *
+ * @param baseUri The base URI.
+ * @param referenceUri The reference URI to resolve.
+ */
+ public static Uri resolveToUri(String baseUri, String referenceUri) {
+ return Uri.parse(resolve(baseUri, referenceUri));
+ }
+
+ /**
+ * Performs relative resolution of a {@code referenceUri} with respect to a {@code baseUri}.
+ *
+ *
The resolution is performed as specified by RFC-3986.
+ *
+ * @param baseUri The base URI.
+ * @param referenceUri The reference URI to resolve.
+ */
+ public static String resolve(String baseUri, String referenceUri) {
+ StringBuilder uri = new StringBuilder();
+
+ // Map null onto empty string, to make the following logic simpler.
+ baseUri = baseUri == null ? "" : baseUri;
+ referenceUri = referenceUri == null ? "" : referenceUri;
+
+ int[] refIndices = getUriIndices(referenceUri);
+ if (refIndices[SCHEME_COLON] != -1) {
+ // The reference is absolute. The target Uri is the reference.
+ uri.append(referenceUri);
+ removeDotSegments(uri, refIndices[PATH], refIndices[QUERY]);
+ return uri.toString();
+ }
+
+ int[] baseIndices = getUriIndices(baseUri);
+ if (refIndices[FRAGMENT] == 0) {
+ // The reference is empty or contains just the fragment part, then the target Uri is the
+ // concatenation of the base Uri without its fragment, and the reference.
+ return uri.append(baseUri, 0, baseIndices[FRAGMENT]).append(referenceUri).toString();
+ }
+
+ if (refIndices[QUERY] == 0) {
+ // The reference starts with the query part. The target is the base up to (but excluding) the
+ // query, plus the reference.
+ return uri.append(baseUri, 0, baseIndices[QUERY]).append(referenceUri).toString();
+ }
+
+ if (refIndices[PATH] != 0) {
+ // The reference has authority. The target is the base scheme plus the reference.
+ int baseLimit = baseIndices[SCHEME_COLON] + 1;
+ uri.append(baseUri, 0, baseLimit).append(referenceUri);
+ return removeDotSegments(uri, baseLimit + refIndices[PATH], baseLimit + refIndices[QUERY]);
+ }
+
+ if (referenceUri.charAt(refIndices[PATH]) == '/') {
+ // The reference path is rooted. The target is the base scheme and authority (if any), plus
+ // the reference.
+ uri.append(baseUri, 0, baseIndices[PATH]).append(referenceUri);
+ return removeDotSegments(uri, baseIndices[PATH], baseIndices[PATH] + refIndices[QUERY]);
+ }
+
+ // The target Uri is the concatenation of the base Uri up to (but excluding) the last segment,
+ // and the reference. This can be split into 2 cases:
+ if (baseIndices[SCHEME_COLON] + 2 < baseIndices[PATH]
+ && baseIndices[PATH] == baseIndices[QUERY]) {
+ // Case 1: The base hier-part is just the authority, with an empty path. An additional '/' is
+ // needed after the authority, before appending the reference.
+ uri.append(baseUri, 0, baseIndices[PATH]).append('/').append(referenceUri);
+ return removeDotSegments(uri, baseIndices[PATH], baseIndices[PATH] + refIndices[QUERY] + 1);
+ } else {
+ // Case 2: Otherwise, find the last '/' in the base hier-part and append the reference after
+ // it. If base hier-part has no '/', it could only mean that it is completely empty or
+ // contains only one segment, in which case the whole hier-part is excluded and the reference
+ // is appended right after the base scheme colon without an added '/'.
+ int lastSlashIndex = baseUri.lastIndexOf('/', baseIndices[QUERY] - 1);
+ int baseLimit = lastSlashIndex == -1 ? baseIndices[PATH] : lastSlashIndex + 1;
+ uri.append(baseUri, 0, baseLimit).append(referenceUri);
+ return removeDotSegments(uri, baseIndices[PATH], baseLimit + refIndices[QUERY]);
+ }
+ }
+
+ /**
+ * Returns true if the URI is starting with a scheme component, false otherwise.
+ */
+ public static boolean isAbsolute(String uri) {
+ return uri != null && getUriIndices(uri)[SCHEME_COLON] != -1;
+ }
+
+ /**
+ * Removes query parameter from a URI, if present.
+ *
+ * @param uri The URI.
+ * @param queryParameterName The name of the query parameter.
+ * @return The URI without the query parameter.
+ */
+ public static Uri removeQueryParameter(Uri uri, String queryParameterName) {
+ Uri.Builder builder = uri.buildUpon();
+ builder.clearQuery();
+ for (String key : uri.getQueryParameterNames()) {
+ if (!key.equals(queryParameterName)) {
+ for (String value : uri.getQueryParameters(key)) {
+ builder.appendQueryParameter(key, value);
+ }
+ }
+ }
+ return builder.build();
+ }
+
+ /**
+ * Removes dot segments from the path of a URI.
+ *
+ * @param uri A {@link StringBuilder} containing the URI.
+ * @param offset The index of the start of the path in {@code uri}.
+ * @param limit The limit (exclusive) of the path in {@code uri}.
+ */
+ private static String removeDotSegments(StringBuilder uri, int offset, int limit) {
+ if (offset >= limit) {
+ // Nothing to do.
+ return uri.toString();
+ }
+ if (uri.charAt(offset) == '/') {
+ // If the path starts with a /, always retain it.
+ offset++;
+ }
+ // The first character of the current path segment.
+ int segmentStart = offset;
+ int i = offset;
+ while (i <= limit) {
+ int nextSegmentStart;
+ if (i == limit) {
+ nextSegmentStart = i;
+ } else if (uri.charAt(i) == '/') {
+ nextSegmentStart = i + 1;
+ } else {
+ i++;
+ continue;
+ }
+ // We've encountered the end of a segment or the end of the path. If the final segment was
+ // "." or "..", remove the appropriate segments of the path.
+ if (i == segmentStart + 1 && uri.charAt(segmentStart) == '.') {
+ // Given "abc/def/./ghi", remove "./" to get "abc/def/ghi".
+ uri.delete(segmentStart, nextSegmentStart);
+ limit -= nextSegmentStart - segmentStart;
+ i = segmentStart;
+ } else if (i == segmentStart + 2
+ && uri.charAt(segmentStart) == '.'
+ && uri.charAt(segmentStart + 1) == '.') {
+ // Given "abc/def/../ghi", remove "def/../" to get "abc/ghi".
+ int prevSegmentStart = uri.lastIndexOf("/", segmentStart - 2) + 1;
+ int removeFrom = prevSegmentStart > offset ? prevSegmentStart : offset;
+ uri.delete(removeFrom, nextSegmentStart);
+ limit -= nextSegmentStart - removeFrom;
+ segmentStart = prevSegmentStart;
+ i = prevSegmentStart;
+ } else {
+ i++;
+ segmentStart = i;
+ }
+ }
+ return uri.toString();
+ }
+
+ /**
+ * Calculates indices of the constituent components of a URI.
+ *
+ * @param uriString The URI as a string.
+ * @return The corresponding indices.
+ */
+ private static int[] getUriIndices(String uriString) {
+ int[] indices = new int[INDEX_COUNT];
+ if (TextUtils.isEmpty(uriString)) {
+ indices[SCHEME_COLON] = -1;
+ return indices;
+ }
+
+ // Determine outer structure from right to left.
+ // Uri = scheme ":" hier-part [ "?" query ] [ "#" fragment ]
+ int length = uriString.length();
+ int fragmentIndex = uriString.indexOf('#');
+ if (fragmentIndex == -1) {
+ fragmentIndex = length;
+ }
+ int queryIndex = uriString.indexOf('?');
+ if (queryIndex == -1 || queryIndex > fragmentIndex) {
+ // '#' before '?': '?' is within the fragment.
+ queryIndex = fragmentIndex;
+ }
+ // Slashes are allowed only in hier-part so any colon after the first slash is part of the
+ // hier-part, not the scheme colon separator.
+ int schemeIndexLimit = uriString.indexOf('/');
+ if (schemeIndexLimit == -1 || schemeIndexLimit > queryIndex) {
+ schemeIndexLimit = queryIndex;
+ }
+ int schemeIndex = uriString.indexOf(':');
+ if (schemeIndex > schemeIndexLimit) {
+ // '/' before ':'
+ schemeIndex = -1;
+ }
+
+ // Determine hier-part structure: hier-part = "//" authority path / path
+ // This block can also cope with schemeIndex == -1.
+ boolean hasAuthority =
+ schemeIndex + 2 < queryIndex
+ && uriString.charAt(schemeIndex + 1) == '/'
+ && uriString.charAt(schemeIndex + 2) == '/';
+ int pathIndex;
+ if (hasAuthority) {
+ pathIndex = uriString.indexOf('/', schemeIndex + 3); // find first '/' after "://"
+ if (pathIndex == -1 || pathIndex > queryIndex) {
+ pathIndex = queryIndex;
+ }
+ } else {
+ pathIndex = schemeIndex + 1;
+ }
+
+ indices[SCHEME_COLON] = schemeIndex;
+ indices[PATH] = pathIndex;
+ indices[QUERY] = queryIndex;
+ indices[FRAGMENT] = fragmentIndex;
+ return indices;
+ }
+}
diff --git a/quickjs/src/main/jniLibs/armeabi-v7a/libquickjs-android-wrapper.so b/quickjs/src/main/jniLibs/armeabi-v7a/libquickjs-android-wrapper.so
new file mode 100644
index 0000000000..08b9bbdb6d
Binary files /dev/null and b/quickjs/src/main/jniLibs/armeabi-v7a/libquickjs-android-wrapper.so differ
diff --git a/settings.gradle b/settings.gradle
index 197eb2a2cc..63bed6c56f 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -1,3 +1,5 @@
rootProject.name = "TVBox"
include ':app'
-include ':player'
\ No newline at end of file
+include ':player'
+include ':quickjs'
+include ':pyramid'
\ No newline at end of file