Skip to content

Instantly share code, notes, and snippets.

@DerGoogler
Last active July 12, 2025 16:39
Show Gist options
  • Save DerGoogler/a67a57f4cbd7cc3a8fa2dea99c9d6736 to your computer and use it in GitHub Desktop.
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
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