6.7.0 - Alpha14 - 将 Paint 类由 "复制式包装" 替换为 "代理对象", 尽可能保留 Paint 对象身份一致性
This commit is contained in:
@@ -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<String>()
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<T : Any>(
|
||||
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<String, Function> = 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<out Any?>): Any? {
|
||||
return v.call(cx, scope, base, args)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<Paint>(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"
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user