From b6df1acf928103ac42893193986c8682d5d5d8ca Mon Sep 17 00:00:00 2001 From: SuperMonster003 Date: Fri, 16 Jan 2026 23:21:49 +0800 Subject: [PATCH] =?UTF-8?q?6.7.0=20-=20Alpha14=20-=20=E5=B0=86=20Paint=20?= =?UTF-8?q?=E7=B1=BB=E7=94=B1=20"=E5=A4=8D=E5=88=B6=E5=BC=8F=E5=8C=85?= =?UTF-8?q?=E8=A3=85"=20=E6=9B=BF=E6=8D=A2=E4=B8=BA=20"=E4=BB=A3=E7=90=86?= =?UTF-8?q?=E5=AF=B9=E8=B1=A1",=20=E5=B0=BD=E5=8F=AF=E8=83=BD=E4=BF=9D?= =?UTF-8?q?=E7=95=99=20Paint=20=E5=AF=B9=E8=B1=A1=E8=BA=AB=E4=BB=BD?= =?UTF-8?q?=E4=B8=80=E8=87=B4=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../autojs/autojs/core/image/RhinoPaint.kt | 138 +++++++++++++++++- .../autojs/engine/RhinoJavaScriptEngine.kt | 12 +- .../api/augment/proxy/JavaProxyObject.kt | 136 +++++++++++++++++ .../api/augment/proxy/PaintProxyObject.kt | 107 ++++++++++++++ version.properties | 4 +- 5 files changed, 390 insertions(+), 7 deletions(-) create mode 100644 app/src/main/java/org/autojs/autojs/runtime/api/augment/proxy/JavaProxyObject.kt create mode 100644 app/src/main/java/org/autojs/autojs/runtime/api/augment/proxy/PaintProxyObject.kt diff --git a/app/src/main/java/org/autojs/autojs/core/image/RhinoPaint.kt b/app/src/main/java/org/autojs/autojs/core/image/RhinoPaint.kt index 8cba37db..6dd61f7b 100644 --- a/app/src/main/java/org/autojs/autojs/core/image/RhinoPaint.kt +++ b/app/src/main/java/org/autojs/autojs/core/image/RhinoPaint.kt @@ -1,12 +1,146 @@ package org.autojs.autojs.core.image import android.graphics.Paint +import android.util.Log import org.autojs.autojs.runtime.api.augment.colors.Colors +import org.autojs.autojs6.BuildConfig +import java.util.concurrent.ConcurrentHashMap +// @Hint by JetBrains AI Assistant (GPT-5.2) on Jan 16, 2026. +// ! This class is no longer used (replaced by org.autojs.autojs.runtime.api.augment.proxy.PaintProxyObject). +// ! +// ! Reason 1: RhinoPaint is a copy-based wrapper (class RhinoPaint(paint) : Paint(paint)). +// ! Copy-based wrapping breaks identity semantics between Java and JavaScript. +// ! Specifically, the JS side may see and mutate a copied Paint instance +// ! while native/Java code still holds the original instance, or vice versa. +// ! +// ! Reason 2: OEM Canvas implementations may mutate caller-provided Paint objects +// ! during drawing (e.g. MIUI's MiuiCanvas calling Paint#setColor(long) internally). +// ! With RhinoPaint, such OEM mutations can be mixed with our overload-fix logic +// ! and make behavior harder to reason about and debug. +// ! +// ! Reason 3: The original goal of RhinoPaint was to mitigate Android Q+ Paint#setColor +// ! overload ambiguity for JS numbers. +// ! However, a subclass-based approach cannot fully preserve ColorLong semantics +// ! in all cases without additional heuristics, and still does not solve identity issues. +// ! +// ! Reason 4: PaintProxyObject uses a composition-based proxy with Wrapper support. +// ! This preserves the original Paint instance identity, so method overload resolution +// ! and `instanceof Paint` behave naturally in Rhino. +// ! The proxy intercepts only the minimal surface area (setColor / color assignment) +// ! and forwards everything else to the underlying Java wrapper, reducing side effects. +// ! Note: For OEM paint-mutation issues (e.g. MIUI), the draw-time paint copy workaround +// ! is implemented in ScriptCanvas.safePaint(...). +// ! +// ! zh-CN: +// ! +// ! 当前类不再使用 (由 org.autojs.autojs.runtime.api.augment.proxy.PaintProxyObject 替代). +// ! +// ! 原因 1: RhinoPaint 属于 "复制式包装" (class RhinoPaint(paint) : Paint(paint)). +// ! 复制式包装会破坏 Java 与 JavaScript 之间的对象身份一致性语义. +// ! 具体表现为: JS 侧可能看到并修改的是 Paint 的副本, 而 Java/系统侧仍持有原始实例 (反之亦然). +// ! +// ! 原因 2: 某些厂商的 Canvas 实现可能在绘制过程中修改调用方传入的 Paint 对象 (例如 MIUI 的 MiuiCanvas 在内部调用 Paint#setColor(long)). +// ! 使用 RhinoPaint 时, 这类 OEM 的 "写回" 行为会与我们的重载修复逻辑交织, 使行为更难推理/更难调试. +// ! +// ! 原因 3: RhinoPaint 的初衷是缓解 Android Q+ 上 Paint#setColor 的重载歧义问题 (针对 JS number). +// ! 但基于子类的方案即使加入判定逻辑, 也难以在所有场景中完整保留 ColorLong 语义, 并且仍无法解决对象身份不一致的问题. +// ! +// ! 原因 4: PaintProxyObject 使用 "组合式代理" (composition) 并实现 Wrapper. +// ! 这样可以保留原始 Paint 实例身份, 并使 Rhino 的方法重载匹配与 `instanceof Paint` 表现符合直觉. +// ! 代理仅拦截最小必要面 (setColor / color 属性赋值), 其它成员全部转发到底层 Java wrapper, 从而减少副作用. +// ! 注意: 针对 OEM 绘制时修改 Paint 的问题 (例如 MIUI), 已在 ScriptCanvas.safePaint(...) 中实现 "绘制时复制 paint" 的规避策略. +/** + * Created by SuperMonster003 on Jun 15, 2025. + * Modified by JetBrains AI Assistant (GPT-5.2) as of Jan 16, 2026. + * Modified by SuperMonster003 as of Jan 16, 2026. + */ class RhinoPaint(paint: Paint) : Paint(paint) { - override fun setColor(color: Long) = setColor(Colors.toIntRhino(color)) + companion object { + // Deduplicate stack traces to avoid log flooding. + // zh-CN: 对调用栈进行去重, 避免日志洪泛. + private val sLogged = ConcurrentHashMap.newKeySet() + } - fun setColor(color: Any?) = setColor(Colors.toIntRhino(color)) + private fun logOnce(tag: String) { + if (!BuildConfig.DEBUG) return + + val stack = Log.getStackTraceString(Throwable()) + + // Use tag + first part of stack as a dedupe key. + // zh-CN: 使用 tag + 调用栈前半段作为去重键. + val key = buildString { + append(tag) + append("|") + append(stack.take(600)) + } + + if (!sLogged.add(key)) return + Log.d("RhinoPaint", "Suspicious Paint mutation: $tag\n$stack") + } + + // Fix ColorLong overload ambiguity on Android Q+ while preserving real ColorLong. + // zh-CN: 修复 Android Q+ 上 ColorLong 重载歧义, 同时保留真正的 ColorLong 语义. + override fun setColor(color: Long) { + // Detect whether "color" is just a sign-extended 32-bit ColorInt. + // zh-CN: 检测 color 是否仅为 32-bit ColorInt 的符号扩展. + val asInt = color.toInt() + val looksLikeColorInt = (color == asInt.toLong()) + + when { + looksLikeColorInt -> { + // JS number commonly lands here and should be treated as ColorInt. + // zh-CN: JS number 常命中该重载, 应按 ColorInt 处理. + if (color == 0L) { + logOnce("setColor(Long=0 as ColorInt)") + } + super.setColor(Colors.toIntRhino(asInt)) + } + else -> { + // Preserve real ColorLong (contains color space / wide gamut info). + // zh-CN: 保留真正的 ColorLong (包含颜色空间/广色域信息). + if (color == 0L) { + logOnce("setColor(Long=0 as ColorLong)") + } + super.setColor(color) + } + } + } + + override fun setColor(color: Int) { + // Only log when the incoming value is 0 (transparent). + // zh-CN: 仅当传入值为 0 (透明) 时记录. + if (color == 0) { + logOnce("setColor(Int=0)") + } + super.setColor(color) + } + + fun setColor(color: Any?) { + val c = Colors.toIntRhino(color) + // Only log when the computed int value is 0 (transparent). + // zh-CN: 仅当计算出的 int 值为 0 (透明) 时记录. + if (c == 0) { + logOnce("setColor(Any?->Int=0)") + } + super.setColor(c) + } + + override fun setAlpha(a: Int) { + // Log only when alpha is explicitly cleared to 0. + // zh-CN: 仅当 alpha 被显式清为 0 时记录. + if (a == 0) { + logOnce("setAlpha(0)") + } + super.setAlpha(a) + } + + override fun reset() { + // reset() wipes out color/alpha and more, always suspicious for user Paint. + // zh-CN: reset() 会清空 color/alpha 等多项状态, 对用户 Paint 来说总是可疑. + logOnce("reset()") + super.reset() + } } diff --git a/app/src/main/java/org/autojs/autojs/engine/RhinoJavaScriptEngine.kt b/app/src/main/java/org/autojs/autojs/engine/RhinoJavaScriptEngine.kt index a3f4667f..0216bef1 100644 --- a/app/src/main/java/org/autojs/autojs/engine/RhinoJavaScriptEngine.kt +++ b/app/src/main/java/org/autojs/autojs/engine/RhinoJavaScriptEngine.kt @@ -2,9 +2,9 @@ package org.autojs.autojs.engine import android.annotation.SuppressLint import android.graphics.Paint +import android.os.Build import android.util.Log import android.view.View -import org.autojs.autojs.core.image.RhinoPaint import org.autojs.autojs.core.ui.ViewExtras import org.autojs.autojs.engine.module.AssetAndUrlModuleSourceProvider import org.autojs.autojs.extension.AnyExtensions.isJsNullish @@ -19,6 +19,7 @@ import org.autojs.autojs.rhino.AutoJsContext import org.autojs.autojs.rhino.RhinoAndroidHelper import org.autojs.autojs.rhino.TopLevelScope import org.autojs.autojs.runtime.ScriptRuntime +import org.autojs.autojs.runtime.api.augment.proxy.PaintProxyObject import org.autojs.autojs.script.JavaScriptSource import org.autojs.autojs.util.RhinoUtils.coerceString import org.autojs.autojs.util.RhinoUtils.js_object_assign @@ -45,7 +46,7 @@ import java.util.* /** * Created by Stardust on Apr 2, 2017. - * Modified by SuperMonster003 as of May 26, 2022. + * Modified by SuperMonster003 as of Jan 16, 2026. */ open class RhinoJavaScriptEngine(private val androidContext: android.content.Context) : JavaScriptEngine() { @@ -216,7 +217,12 @@ open class RhinoJavaScriptEngine(private val androidContext: android.content.Con override fun wrapAsJavaObject(cx: Context?, scope: Scriptable, javaObject: Any?, staticType: TypeInfo): Scriptable? { return when (javaObject) { is View -> ViewExtras.getNativeView(scope, /* view = */ javaObject, staticType.asClass(), runtime) - is Paint -> super.wrapAsJavaObject(cx, scope, RhinoPaint(javaObject), staticType) + is Paint if Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q -> { + // Use composition-based proxy to preserve Paint identity and intercept setColor only. + // zh-CN: 使用组合式代理以保持 Paint 身份一致性, 且仅拦截 setColor. + val base = super.wrapAsJavaObject(cx, scope, javaObject, staticType) ?: return null + PaintProxyObject(scope, base, javaObject) + } else -> super.wrapAsJavaObject(cx, scope, javaObject, staticType) } } diff --git a/app/src/main/java/org/autojs/autojs/runtime/api/augment/proxy/JavaProxyObject.kt b/app/src/main/java/org/autojs/autojs/runtime/api/augment/proxy/JavaProxyObject.kt new file mode 100644 index 00000000..8a2337f1 --- /dev/null +++ b/app/src/main/java/org/autojs/autojs/runtime/api/augment/proxy/JavaProxyObject.kt @@ -0,0 +1,136 @@ +package org.autojs.autojs.runtime.api.augment.proxy + +import org.mozilla.javascript.BaseFunction +import org.mozilla.javascript.Context +import org.mozilla.javascript.Function +import org.mozilla.javascript.Scriptable +import org.mozilla.javascript.ScriptableObject +import org.mozilla.javascript.Wrapper +import java.util.concurrent.ConcurrentHashMap + +/** + * A generic composition-based Java proxy for Rhino. + * Key rules for a correct proxy: + * 1) Implement Wrapper so Rhino can unwrap for instanceof and overload resolution. + * 2) Bind functions so that "this" is the underlying Java wrapper, not the proxy. + * 3) Intercept only what you must, forward everything else. + * + * zh-CN: + * + * 一个通用的组合式 Java 代理对象, 用于 Rhino. + * 正确代理对象的关键要素: + * 1. 实现 Wrapper, 让 Rhino 可用于 instanceof 与方法重载匹配. + * 2. 绑定函数, 确保调用时 this 为底层 Java wrapper, 而非代理对象. + * 3. 只拦截必要行为, 其它全部转发. + * + * Created by JetBrains AI Assistant (GPT-5.2) on Jan 16, 2026. + */ +open class JavaProxyObject( + scope: Scriptable, + protected val base: Scriptable, + protected val target: T, +) : ScriptableObject(), Wrapper { + + // Cache bound functions to reduce allocations on hot paths. + // zh-CN: 缓存绑定后的函数, 以减少热路径的对象分配. + private val functionMap: ConcurrentHashMap = ConcurrentHashMap() + + init { + parentScope = scope + } + + override fun getClassName(): String = "JavaProxyObject" + + // Expose underlying Java object to Rhino for instanceof and method overload resolution. + // zh-CN: 向 Rhino 暴露底层 Java 对象, 用于 instanceof 与方法重载匹配. + override fun unwrap(): Any = target + + override fun get(name: String, start: Scriptable): Any { + // Allow subclasses to intercept member access. + // zh-CN: 允许子类拦截成员访问. + getIntercepted(name)?.let { return it } + + // Try to fetch from the underlying Java wrapper first. + // zh-CN: 优先从底层 Java wrapper 获取成员. + val v = base.get(name, base) + if (v != Scriptable.NOT_FOUND) { + // Fast path: cached function. + // zh-CN: 快速路径: 命中函数缓存. + if (v is Function) { + functionMap[name]?.let { return it } + } + + val bound = bindIfFunction(v) + if (bound is Function) { + functionMap.putIfAbsent(name, bound) + } + return bound + } + + // Fallback to ScriptableObject default. + // zh-CN: 兜底回退到 ScriptableObject 默认行为. + return super.get(name, start) + } + + override fun put(name: String, start: Scriptable, value: Any?) { + // Allow subclasses to intercept member writes. + // zh-CN: 允许子类拦截成员写入. + if (putIntercepted(name, value)) return + + // Forward property sets to the underlying Java wrapper. + // zh-CN: 将属性写入转发到底层 Java wrapper. + base.put(name, base, value) + } + + override fun has(name: String, start: Scriptable): Boolean { + // Ensure intercepted members are visible to JS (e.g. setColor). + // zh-CN: 确保被拦截的成员对 JS 可见 (例如 setColor). + if (hasIntercepted(name)) return true + return base.has(name, base) || super.has(name, start) + } + + override fun getDefaultValue(typeHint: Class<*>?): Any { + // Delegate to underlying wrapper for meaningful stringification. + // zh-CN: 委托到底层 wrapper, 以获得更有意义的字符串化结果. + return base.getDefaultValue(typeHint) + } + + override fun toString(): String { + // Make log(proxy) look like a Java object. + // zh-CN: 让 log(proxy) 更像 JavaObject 的输出. + return target.toString() + } + + /** + * Subclass hook: intercept read access for a property/method name. + * zh-CN: 子类钩子: 拦截某个属性/方法名的读取访问. + */ + protected open fun getIntercepted(name: String): Any? = null + + /** + * Subclass hook: intercept write access for a property name. + * Return true if handled. + * zh-CN: + * 子类钩子: 拦截某个属性名的写入访问. + * 若已处理则返回 true. + */ + protected open fun putIntercepted(name: String, value: Any?): Boolean = false + + /** + * Subclass hook: declare intercepted members as existing. + * zh-CN: 子类钩子: 声明被拦截成员存在. + */ + protected open fun hasIntercepted(name: String): Boolean = false + + private fun bindIfFunction(v: Any): Any { + // Bind functions so that "this" is always the underlying Java wrapper. + // zh-CN: 绑定函数, 让 this 始终为底层 Java wrapper. + if (v !is Function) return v + return object : BaseFunction() { + override fun call(cx: Context, scope: Scriptable, thisObj: Scriptable, args: Array): Any? { + return v.call(cx, scope, base, args) + } + } + } + +} diff --git a/app/src/main/java/org/autojs/autojs/runtime/api/augment/proxy/PaintProxyObject.kt b/app/src/main/java/org/autojs/autojs/runtime/api/augment/proxy/PaintProxyObject.kt new file mode 100644 index 00000000..a0e69237 --- /dev/null +++ b/app/src/main/java/org/autojs/autojs/runtime/api/augment/proxy/PaintProxyObject.kt @@ -0,0 +1,107 @@ +package org.autojs.autojs.runtime.api.augment.proxy + +import android.graphics.Paint +import android.os.Build +import org.autojs.autojs.runtime.api.augment.colors.Colors +import org.autojs.autojs.util.RhinoUtils.NOT_CONSTRUCTABLE +import org.autojs.autojs.util.RhinoUtils.newBaseFunction +import org.autojs.autojs.util.RhinoUtils.withRhinoContext +import org.mozilla.javascript.BaseFunction +import org.mozilla.javascript.Scriptable +import org.mozilla.javascript.Undefined + +/** + * Paint proxy object which preserves identity and only intercepts setColor overload ambiguity. + * zh-CN: Paint 代理对象, 保持对象身份一致性, 且仅拦截 setColor 重载歧义问题. + * + * Created by JetBrains AI Assistant (GPT-5.2) on Jan 16, 2026. + * Modified by SuperMonster003 as of Jan 16, 2026. + */ +class PaintProxyObject( + scope: Scriptable, + base: Scriptable, + paint: Paint, +) : JavaProxyObject(scope, base, paint) { + + override fun getClassName(): String = "Paint" + + // Rhino JS uses only "number", and Android Q+ has both setColor(int) and setColor(long). + // In many JS->Java bridges, a JS number is represented as a Java Number and may match the long overload first. + // A plain ColorInt is 32-bit; when widened to long, it is typically just a sign-extension of low 32 bits. + // A real ColorLong (from Color.pack) uses high bits for metadata and is not equal to sign-extended int. + // + // zh-CN: + // + // Rhino JS 只有 number, 而 Android Q+ 同时存在 setColor(int) 与 setColor(long). + // 在许多 JS->Java 桥接中, JS number 会被表示为 Java Number, 并可能优先命中 long 重载. + // 普通 ColorInt 是 32-bit 值; 扩展为 long 时通常只是低 32 位的符号扩展. + // 真正的 ColorLong (由 Color.pack 得到) 高位包含元数据, 通常不等于 int 的符号扩展. + private fun isLikelyColorIntFromJsNumber(value: Long): Boolean { + val asInt = value.toInt() + return value == asInt.toLong() + } + + private val setColorFn: BaseFunction = newBaseFunction("setColor", { args -> + + // Intercept Paint#setColor overload ambiguity on Android Q+. + // zh-CN: 在 Android Q+ 上拦截 Paint#setColor 的重载歧义问题. + val arg = args.getOrNull(0) + + when { + Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q -> { + when (arg) { + is Number -> { + val l = arg.toLong() + when { + isLikelyColorIntFromJsNumber(l) -> { + // Treat as ColorInt and use safe implementation. + // zh-CN: 按 ColorInt 处理并使用安全实现. + Colors.setPaintColorRhino(target, l.toInt()) + } + else -> { + // Preserve real ColorLong semantics. + // zh-CN: 保留真正的 ColorLong 语义. + target.setColor(l) + } + } + } + else -> { + // Non-number inputs: ColorHex/ColorName/etc. + // zh-CN: 非数字输入: ColorHex/ColorName 等. + Colors.setPaintColorRhino(target, arg) + } + } + } + else -> { + // On pre-Q devices, setColor(int) works normally. + // zh-CN: 在 Q 之前, setColor(int) 正常工作. + Colors.setPaintColorRhino(target, arg) + } + } + + // Return undefined like Java void. + // zh-CN: 与 Java void 一致返回 undefined. + Undefined.instance + }, NOT_CONSTRUCTABLE) + + override fun getIntercepted(name: String): Any? = + when (name) { + "setColor" -> setColorFn + else -> null + } + + override fun putIntercepted(name: String, value: Any?): Boolean = + when (name) { + // Support property form: paint.color = ... + // zh-CN: 支持属性形式: paint.color = ... + "color" -> withRhinoContext { cx -> + setColorFn.call(cx, this, this, arrayOf(value)) + true + } + else -> false + } + + override fun hasIntercepted(name: String): Boolean = + name == "setColor" || name == "color" + +} diff --git a/version.properties b/version.properties index 1d3ab212..5e54c667 100644 --- a/version.properties +++ b/version.properties @@ -1,5 +1,5 @@ -#Fri Jan 16 20:18:26 CST 2026 -BUILD_TIME=1768565906722 +#Fri Jan 16 23:14:30 CST 2026 +BUILD_TIME=1768576470633 COMPILE_SDK_VERSION=36 IMAGE_QUANT_CMAKE_VERSION=3.22.1 IMAGE_QUANT_NDK_VERSION=26.1.10909125