diff --git a/.changelog/lang_zh-Hans.json b/.changelog/lang_zh-Hans.json index 7ce9300c..06a82814 100644 --- a/.changelog/lang_zh-Hans.json +++ b/.changelog/lang_zh-Hans.json @@ -1,19 +1,22 @@ { "$data": { "v6.6.1": { - "released_date": "2024/12/08", + "released_date": "2024/12/09", "feature": [ - "pinyin 模块, 用于汉语拼音转换 (参阅 项目文档 > [汉语拼音](https://docs.autojs6.com/#/pinyin))" + "pinyin 模块, 用于汉语拼音转换 (参阅 项目文档 > [汉语拼音](https://docs.autojs6.com/#/pinyin))", + "UiObject#isSimilar 及 UiObjectCollection#isSimilar 方法, 用于确定控件或控件集合是否相似" ], "fix": [ "部分环境因回退版本过低而无法正常编译项目的问题", "调用不存在的方法时可能出现的 \"非原始类型值\" 异常", "部分设备无法正常添加脚本快捷方式的问题 (试修) _[`issue #221`](http://issues.autojs6.com/221)_", - "automator.click/longClick 方法参数类型限制错误 _[`issue #275`](http://issues.autojs6.com/275)_" + "automator.click/longClick 方法参数类型限制错误 _[`issue #275`](http://issues.autojs6.com/275)_", + "UiObjectCollection 实例缺失自身方法及属性的问题" ], "improvement": [ "脚本项目识别在 project.json 损坏情况下尽可能还原关键信息", "打包单文件时自动生成的包名后缀支持将简体中文自动转换为拼音", + "UiSelector#findOnce 及 UiSelector#find 方法支持负数参数", "UI 元素及 className 相关选择器支持更多的包名前缀省略形式 (如 RecyclerView, Snackbar 等)" ] }, diff --git a/app/src/main/java/org/autojs/autojs/core/automator/UiObject.kt b/app/src/main/java/org/autojs/autojs/core/automator/UiObject.kt index 11f46925..4af333af 100644 --- a/app/src/main/java/org/autojs/autojs/core/automator/UiObject.kt +++ b/app/src/main/java/org/autojs/autojs/core/automator/UiObject.kt @@ -13,6 +13,7 @@ import org.autojs.autojs.core.accessibility.AccessibilityNodeInfoHelper import org.autojs.autojs.core.accessibility.UiSelector import org.autojs.autojs.extension.AnyExtensions.isJsNullish import org.autojs.autojs.extension.AnyExtensions.isJsUndefined +import org.autojs.autojs.extension.ArrayExtensions.toHashCode import org.autojs.autojs.extension.ArrayExtensions.toNativeArray import org.autojs.autojs.runtime.ScriptRuntime import org.autojs.autojs.util.DisplayUtils.toRoundIntX @@ -22,6 +23,7 @@ import org.autojs.autojs.util.StringUtils.str import org.autojs.autojs6.R import org.mozilla.javascript.BaseFunction import org.mozilla.javascript.Context +import org.mozilla.javascript.Scriptable import org.opencv.core.Point import org.opencv.core.Size import kotlin.math.roundToInt @@ -485,8 +487,109 @@ open class UiObject( override fun toString() = "[${UiObject::class.java.simpleName}] $className ${summary()}" - override fun hashCode(): Int { - return super.hashCode() + override fun hashCode() = listOf( + "${packageName()}", + "$parent", + "${id()}", + "${fullId()}", + "${idHex()}", + "${desc()}", + text(), + "${bounds()}", + "${className()}", + "${clickable()}", + "${longClickable()}", + "${scrollable()}", + "${indexInParent()}", + "${childCount()}", + "${depth()}", + "${checked()}", + "${enabled()}", + "${editable()}", + "${focusable()}", + "${checkable()}", + "${selected()}", + "$isDismissable", + "${visibleToUser()}", + "$isContextClickable", + "${focused()}", + "$isAccessibilityFocused", + "${rowCount()}", + "${columnCount()}", + "${row()}", + "${column()}", + "${rowSpan()}", + "${columnSpan()}", + "$drawingOrder", + "${actionNames()}", + "${isSingleton()}", + "${firstSibling()}", + "${lastSibling()}", + "${firstChild()}", + "${lastChild()}", + ).toHashCode() + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is UiObject) return false + + return packageName() == other.packageName() && + parent == other.parent && + id() == other.id() && + fullId() == other.fullId() && + idHex() == other.idHex() && + desc() == other.desc() && + text() == other.text() && + bounds() == other.bounds() && + className() == other.className() && + clickable() == other.clickable() && + longClickable() == other.longClickable() && + scrollable() == other.scrollable() && + indexInParent() == other.indexInParent() && + childCount() == other.childCount() && + depth() == other.depth() && + checked() == other.checked() && + enabled() == other.enabled() && + editable() == other.editable() && + focusable() == other.focusable() && + checkable() == other.checkable() && + selected() == other.selected() && + isDismissable == other.isDismissable && + visibleToUser() == other.visibleToUser() && + isContextClickable == other.isContextClickable && + focused() == other.focused() && + isAccessibilityFocused == other.isAccessibilityFocused && + rowCount() == other.rowCount() && + columnCount() == other.columnCount() && + row() == other.row() && + column() == other.column() && + rowSpan() == other.rowSpan() && + columnSpan() == other.columnSpan() && + drawingOrder == other.drawingOrder && + actionNames() == other.actionNames() && + isSingleton() == other.isSingleton() && + "${firstSibling()}" == "${other.firstSibling()}" && + "${lastSibling()}" == "${other.lastSibling()}" && + "${firstChild()}" == "${other.firstChild()}" && + "${lastChild()}" == "${other.lastChild()}" + } + + fun isSimilar(other: Any?): Boolean { + if (this === other) return true + if (other == null) return false + when (other) { + is Scriptable -> { + val thisHashCode = hashCode() + val otherHashCode = RhinoUtils.hashCodeOfScriptable(other) ?: return false + return thisHashCode == otherHashCode + } + else -> { + return when { + other.javaClass != UiObject::class.java -> false + else -> hashCode() == other.hashCode() + } + } + } } companion object { @@ -496,14 +599,7 @@ open class UiObject( internal const val RESULT_TYPE_WIDGET = "widget" private val RESULT_GROUP_WIDGET by lazy { arrayOf("#", "w", RESULT_TYPE_WIDGET) } - private val RESULT_GROUP_WIDGET_COLLECTION by lazy { - arrayOf("{}", "wc", "collection", "list").plus( - listAliases( - "#", - "w" - ) - ) - } + private val RESULT_GROUP_WIDGET_COLLECTION by lazy { arrayOf("{}", "wc", "collection", "list").plus(listAliases("#", "w")) } private val RESULT_GROUP_WIDGETS by lazy { arrayOf("[]", "ws", "widgets").plus(arrayAliases("#", "w")) } private val RESULT_GROUP_CONTENT by lazy { arrayOf("$", "txt", "content") } private val RESULT_GROUP_CONTENTS by lazy { arrayOf("contents").plus(arrayAliases("$", "txt", "content")) } @@ -520,9 +616,8 @@ open class UiObject( @JvmStatic @Throws(IllegalArgumentException::class) fun ensureCompass(s: CharSequence?) { - if (s != null && !isCompass(s)) { - throw IllegalArgumentException(str(R.string.error_invalid_compass, s)) - } + s ?: return + require(isCompass(s)) { str(R.string.error_invalid_compass, s) } } @JvmStatic diff --git a/app/src/main/java/org/autojs/autojs/core/automator/UiObjectCollection.kt b/app/src/main/java/org/autojs/autojs/core/automator/UiObjectCollection.kt index 20b8dc62..92dce544 100644 --- a/app/src/main/java/org/autojs/autojs/core/automator/UiObjectCollection.kt +++ b/app/src/main/java/org/autojs/autojs/core/automator/UiObjectCollection.kt @@ -1,7 +1,10 @@ package org.autojs.autojs.core.automator import org.autojs.autojs.core.accessibility.UiSelector +import org.autojs.autojs.extension.ArrayExtensions.toHashCode import org.autojs.autojs.tool.Consumer +import org.autojs.autojs.util.RhinoUtils +import org.mozilla.javascript.Scriptable /** * Created by Stardust on Mar 9, 2017. @@ -62,7 +65,36 @@ class UiObjectCollection private constructor(val nodes: List) : UiObj } override fun toString(): String { - return "${UiObjectCollection::class.java.name}@${hashCode()}" + return "${UiObjectCollection::class.java.name}@${super.hashCode()}" + } + + override fun hashCode(): Int = when { + nodes.isEmpty() -> super.hashCode() + else -> nodes.map { it.hashCode() }.toHashCode() + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is UiObjectCollection) return false + return nodes == other.nodes + } + + fun isSimilar(other: Any?): Boolean { + if (this === other) return true + if (other == null) return false + when (other) { + is Scriptable -> { + val thisHashCode = hashCode() + val otherHashCode = RhinoUtils.hashCodeOfScriptable(other) ?: return false + return thisHashCode == otherHashCode + } + else -> { + return when { + other.javaClass != UiObjectCollection::class.java -> false + else -> hashCode() == other.hashCode() + } + } + } } companion object { diff --git a/app/src/main/java/org/autojs/autojs/util/RhinoUtils.kt b/app/src/main/java/org/autojs/autojs/util/RhinoUtils.kt index f7ae205b..908d51b9 100644 --- a/app/src/main/java/org/autojs/autojs/util/RhinoUtils.kt +++ b/app/src/main/java/org/autojs/autojs/util/RhinoUtils.kt @@ -7,9 +7,11 @@ import android.os.Looper import android.os.Parcelable import android.util.Log import org.autojs.autojs.AutoJs +import org.autojs.autojs.core.automator.UiObjectCollection import org.autojs.autojs.extension.AnyExtensions.isJsNullish import org.autojs.autojs.extension.AnyExtensions.jsBrief import org.autojs.autojs.extension.AnyExtensions.jsSpecies +import org.autojs.autojs.extension.AnyExtensions.jsUnwrapped import org.autojs.autojs.extension.ScriptableExtensions.defineProp import org.autojs.autojs.extension.ScriptableExtensions.prop import org.autojs.autojs.rhino.TopLevelScope @@ -813,6 +815,22 @@ object RhinoUtils { @JvmStatic fun newNativeArray(array: Array) = NativeArray(array).also { it.exportAsJSClass(NativeArray.MAX_PROTOTYPE_ID, it, false) } + @JvmStatic + fun hashCodeOfScriptable(other: Any?): Int? { + if (other !is Scriptable) return null + val getClassFunc = other["getClass"] + if (getClassFunc is BaseFunction) { + val clazz = callFunction(getClassFunc, emptyArray()).jsUnwrapped() + if (clazz != UiObjectCollection::class.java) return null + } + val hashCodeFunc = other["hashCode"] + if (hashCodeFunc is BaseFunction) { + val hashCode = callFunction(hashCodeFunc, emptyArray()).jsUnwrapped() + if (hashCode is Number) return hashCode.toInt() + } + return null + } + class ObsoletedRhinoFunctionException(funcName: String) : Exception( "Function \"$funcName\" can no longer be used as it has been obsoleted", )