Last active
July 12, 2025 16:39
-
-
Save DerGoogler/a67a57f4cbd7cc3a8fa2dea99c9d6736 to your computer and use it in GitHub Desktop.
Proof of concept for JavaScript-to-Java/Kotlin reflection using the WXU Plugin for WebUI X
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class JavaObject { | |
static { | |
this.reflect = global.require("wx:reflect"); | |
this.proxyHandlers = new Map(); | |
this.nextProxyId = 0; | |
} | |
constructor(className, args = []) { | |
if (className) { | |
this.classId = JavaObject.getClass(className); | |
this.objId = JavaObject.newInstance(this.classId, args); | |
} | |
} | |
call(method, args = []) { | |
return JavaObject.callMethod(this.objId, method, args); | |
} | |
get(field) { | |
return JavaObject.getField(this.objId, field); | |
} | |
set(field, value) { | |
JavaObject.setField(this.objId, field, value); | |
} | |
release() { | |
JavaObject.release(this.objId); | |
} | |
static getClass(name) { | |
return this.reflect.getClass(name); | |
} | |
static newInstance(classId, args) { | |
const jsonArgs = (args == null) ? "null" : JSON.stringify(args); | |
return this.reflect.newInstance(classId, jsonArgs); | |
} | |
static callMethod(objId, method, args) { | |
const jsonArgs = (args == null) ? "[]" : JSON.stringify(args); | |
return this.reflect.callMethod(objId, method, jsonArgs); | |
} | |
static getField(objId, field) { | |
return this.reflect.getField(objId, field); | |
} | |
static setField(objId, field, value) { | |
this.reflect.setField(objId, field, value); | |
} | |
static release(objId) { | |
this.reflect.releaseObject(objId); | |
} | |
static fromObjId(objId) { | |
const obj = new JavaObject(null); | |
obj.objId = objId; | |
return obj; | |
} | |
static createProxy(interfaceName, handler) { | |
const methodsMap = {}; | |
const handlerIds = []; | |
const allMethods = { | |
hashCode: () => 0, | |
equals: () => false, | |
toString: () => '[JavaScript Proxy]', | |
...handler | |
}; | |
for (const methodName in allMethods) { | |
if (typeof allMethods[methodName] === 'function') { | |
const methodId = `proxy_handler_${this.nextProxyId++}`; | |
methodsMap[methodName] = methodId; | |
handlerIds.push(methodId); | |
this.proxyHandlers.set(methodId, (...args) => { | |
try { | |
return allMethods[methodName](...args); | |
} catch (e) { | |
console.error(`Error in proxy method ${methodName}:`, e); | |
if (methodName === 'equals') return false; | |
return null; | |
} | |
}); | |
} | |
} | |
const proxyId = this.reflect.createProxy(interfaceName, JSON.stringify(methodsMap)); | |
const proxy = JavaObject.fromObjId(proxyId); | |
proxy.releaseProxy = () => { | |
handlerIds.forEach(id => this.proxyHandlers.delete(id)); | |
proxy.release(); | |
}; | |
return proxy; | |
} | |
} | |
class SharedPreferences { | |
#prefsId; | |
#editorId = null; | |
#listeners = new Map(); | |
#listenerProxy = null; | |
constructor(context, name, mode = 0) { | |
const contextObj = (context instanceof JavaObject) ? context : JavaObject.fromObjId(context); | |
this.#prefsId = contextObj.call("getSharedPreferences", [name, mode]); | |
} | |
#getEditor() { | |
if (!this.#editorId) { | |
this.#editorId = JavaObject.callMethod(this.#prefsId, "edit", []); | |
} | |
return this.#editorId; | |
} | |
get(key, defaultValue = null) { | |
const type = typeof defaultValue; | |
switch (type) { | |
case 'boolean': | |
return JavaObject.callMethod(this.#prefsId, "getBoolean", [key, defaultValue]) == "true"; | |
case 'number': | |
if (Number.isInteger(defaultValue)) { | |
const int = JavaObject.callMethod(this.#prefsId, "getInt", [key, defaultValue]) | |
const parsedInt = Number.parseInt(int) | |
if (Number.isNaN(parsedInt)) { | |
return defaultValue | |
} | |
return parsedInt | |
} | |
const float = JavaObject.callMethod(this.#prefsId, "getFloat", [key, defaultValue]) | |
const parsedFloat = Number.parseFloat(float) | |
if (Number.isNaN(parsedFloat)) { | |
return defaultValue | |
} | |
return parsedFloat | |
case 'string': | |
return JavaObject.callMethod(this.#prefsId, "getString", [key, defaultValue]); | |
default: | |
return JavaObject.callMethod(this.#prefsId, "getString", [key, String(defaultValue)]); | |
} | |
} | |
put(key, value) { | |
const editor = this.#getEditor(); | |
const type = typeof value; | |
switch (type) { | |
case 'boolean': | |
JavaObject.callMethod(editor, "putBoolean", [key, value]); | |
break; | |
case 'number': | |
Number.isInteger(value) ? | |
JavaObject.callMethod(editor, "putInt", [key, value]) : | |
JavaObject.callMethod(editor, "putFloat", [key, value]); | |
break; | |
case 'string': | |
JavaObject.callMethod(editor, "putString", [key, value]); | |
break; | |
default: | |
JavaObject.callMethod(editor, "putString", [key, String(value)]); | |
} | |
return this; | |
} | |
remove(key) { | |
JavaObject.callMethod(this.#getEditor(), "remove", [key]); | |
return this; | |
} | |
clear() { | |
JavaObject.callMethod(this.#getEditor(), "clear", []); | |
return this; | |
} | |
commit() { | |
if (!this.#editorId) return false; | |
const result = JavaObject.callMethod(this.#editorId, "commit", []); | |
this.#editorId = null; | |
return result; | |
} | |
apply() { | |
if (!this.#editorId) return; | |
JavaObject.callMethod(this.#editorId, "apply", []); | |
this.#editorId = null; | |
} | |
contains(key) { | |
return JavaObject.callMethod(this.#prefsId, "contains", [key]); | |
} | |
getAll() { | |
return JavaObject.callMethod(this.#prefsId, "getAll", []); | |
} | |
registerOnChangeListener(callback) { | |
if (typeof callback !== 'function') { | |
throw new Error('Callback must be a function'); | |
} | |
const listenerId = `listener_${Math.random().toString(36).substring(2, 9)}`; | |
this.#listeners.set(listenerId, callback); | |
if (!this.#listenerProxy) { | |
const interfaceName = "android.content.SharedPreferences$OnSharedPreferenceChangeListener"; | |
this.#listenerProxy = JavaObject.createProxy(interfaceName, { | |
onSharedPreferenceChanged: (prefsId, key) => { | |
this.#listeners.forEach(listener => { | |
try { | |
listener(key, this); | |
} catch (e) { | |
console.error('Error in preference change listener:', e); | |
} | |
}); | |
}, | |
}); | |
JavaObject.callMethod(this.#prefsId, "registerOnSharedPreferenceChangeListener", [this.#listenerProxy.objId]); | |
} | |
return () => this.unregisterOnChangeListener(listenerId); | |
} | |
unregisterOnChangeListener(listenerId) { | |
if (this.#listeners.delete(listenerId) && this.#listeners.size === 0 && this.#listenerProxy) { | |
JavaObject.callMethod(this.#prefsId, "unregisterOnSharedPreferenceChangeListener", [this.#listenerProxy.objId]); | |
this.#listenerProxy.releaseProxy(); | |
this.#listenerProxy = null; | |
return true; | |
} | |
return false; | |
} | |
} | |
window.context = (() => { | |
const ActivityThread = new JavaObject("android.app.ActivityThread"); | |
const thread = ActivityThread.call("currentActivityThread"); | |
const context = JavaObject.callMethod(thread, "getApplication", []); | |
return JavaObject.callMethod(context, "getBaseContext", []); | |
})() | |
window.prefs = new SharedPreferences(window.context, "my_prefs"); | |
const unregister = prefs.registerOnChangeListener((key) => { | |
console.log(`Preference changed: ${key}`); | |
}); | |
prefs.put("username", "john_doe") | |
.put("user_id", 12345) | |
.put("premium_user", false) | |
.apply(); | |
const username = prefs.get("username", ""); | |
const userId = prefs.get("user_id", 0); | |
const isPremium = prefs.get("premium_user", false); | |
console.log(`User:`, username, `, ID:`, userId, `, Premium:`, isPremium); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment