mirror of
https://github.com/gkd-kit/gkd.git
synced 2024-11-16 03:32:38 +08:00
feat: remove selector_android
This commit is contained in:
parent
0038d5eb98
commit
b9cef851d7
|
@ -110,7 +110,6 @@ android {
|
|||
|
||||
dependencies {
|
||||
implementation(project(mapOf("path" to ":selector_core")))
|
||||
implementation(project(mapOf("path" to ":selector_android")))
|
||||
implementation(project(mapOf("path" to ":router")))
|
||||
|
||||
implementation(libs.androidx.appcompat)
|
||||
|
|
|
@ -78,7 +78,7 @@
|
|||
android:exported="false"
|
||||
android:foregroundServiceType="mediaProjection" />
|
||||
<service
|
||||
android:name=".debug.server.HttpService"
|
||||
android:name=".debug.HttpService"
|
||||
android:exported="false" />
|
||||
<service
|
||||
android:name=".debug.FloatingService"
|
||||
|
|
|
@ -15,14 +15,16 @@ import li.songe.gkd.data.RuleManager
|
|||
import li.songe.gkd.data.SubscriptionRaw
|
||||
import li.songe.gkd.db.table.SubsItem
|
||||
import li.songe.gkd.db.util.RoomX
|
||||
import li.songe.gkd.debug.server.api.Node
|
||||
import li.songe.gkd.debug.NodeSnapshot
|
||||
import li.songe.gkd.selector.click
|
||||
import li.songe.gkd.selector.querySelectorAll
|
||||
import li.songe.gkd.util.Ext.buildRuleManager
|
||||
import li.songe.gkd.util.Ext.getActivityIdByShizuku
|
||||
import li.songe.gkd.util.Ext.getSubsFileLastModified
|
||||
import li.songe.gkd.util.Ext.launchWhile
|
||||
import li.songe.gkd.util.Singleton
|
||||
import li.songe.gkd.util.Storage
|
||||
import li.songe.selector_android.GkdSelector
|
||||
import li.songe.selector_core.Selector
|
||||
import java.io.File
|
||||
|
||||
class GkdAbService : CompositionAbService({
|
||||
|
@ -93,6 +95,7 @@ class GkdAbService : CompositionAbService({
|
|||
}
|
||||
|
||||
scope.launchWhile {
|
||||
delay(50)
|
||||
if (!serviceConnected) return@launchWhile
|
||||
if (!Storage.settings.enableService || ScreenUtils.isScreenLock()) return@launchWhile
|
||||
|
||||
|
@ -101,18 +104,17 @@ class GkdAbService : CompositionAbService({
|
|||
)
|
||||
val shot = nodeSnapshot
|
||||
if (shot.root == null) return@launchWhile
|
||||
|
||||
for (rule in ruleManager.match(shot.appId, shot.activityId)) {
|
||||
val target = rule.query(shot.root) ?: continue
|
||||
val clickResult = GkdSelector.click(target, context)
|
||||
val clickResult = target.click(context)
|
||||
ruleManager.trigger(rule)
|
||||
LogUtils.d(
|
||||
*rule.matches.toTypedArray(),
|
||||
Node.info2data(target),
|
||||
NodeSnapshot.abNodeToNode(target),
|
||||
clickResult
|
||||
)
|
||||
}
|
||||
delay(200)
|
||||
delay(150)
|
||||
}
|
||||
|
||||
scope.launchWhile {
|
||||
|
@ -163,6 +165,11 @@ class GkdAbService : CompositionAbService({
|
|||
companion object {
|
||||
fun isRunning() = ServiceUtils.isServiceRunning(GkdAbService::class.java)
|
||||
fun currentNodeSnapshot() = service?.nodeSnapshot
|
||||
fun match(selector: String) {
|
||||
val rootAbNode = service?.rootInActiveWindow ?: return
|
||||
val list = rootAbNode.querySelectorAll(Selector.parse(selector)).map { it.value }.toList()
|
||||
}
|
||||
|
||||
private var service: GkdAbService? = null
|
||||
}
|
||||
}
|
|
@ -7,7 +7,7 @@ import kotlinx.serialization.Serializable
|
|||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.*
|
||||
import li.songe.gkd.util.Singleton
|
||||
import li.songe.selector_android.GkdSelector
|
||||
import li.songe.selector_core.Selector
|
||||
|
||||
|
||||
@Parcelize
|
||||
|
@ -136,11 +136,11 @@ data class SubscriptionRaw(
|
|||
matches = (getStringIArray(
|
||||
rulesJson,
|
||||
"matches"
|
||||
) ?: emptyList()).onEach { GkdSelector.gkdSelectorParser(it) },
|
||||
) ?: emptyList()).onEach { Selector.parse(it) },
|
||||
excludeMatches = (getStringIArray(
|
||||
rulesJson,
|
||||
"excludeMatches"
|
||||
) ?: emptyList()).onEach { GkdSelector.gkdSelectorParser(it) },
|
||||
) ?: emptyList()).onEach { Selector.parse(it) },
|
||||
key = getInt(rulesJson, "key"),
|
||||
name = getString(rulesJson, "name"),
|
||||
preKeys = getIntIArray(rulesJson, "preKeys") ?: emptyList(),
|
||||
|
|
|
@ -1,35 +1,46 @@
|
|||
package li.songe.gkd.debug.server.api
|
||||
package li.songe.gkd.debug
|
||||
|
||||
import android.graphics.Rect
|
||||
import android.view.accessibility.AccessibilityNodeInfo
|
||||
import kotlinx.serialization.Serializable
|
||||
import li.songe.gkd.selector.getDepth
|
||||
import li.songe.gkd.selector.getIndex
|
||||
|
||||
@Serializable
|
||||
data class Attr(
|
||||
data class AttrSnapshot(
|
||||
val id: String? = null,
|
||||
val className: String? = null,
|
||||
val childCount: Int = 0,
|
||||
val name: String? = null,
|
||||
val text: String? = null,
|
||||
val isClickable: Boolean = false,
|
||||
val textLen: Int? = text?.length,
|
||||
val desc: String? = null,
|
||||
val descLen: Int? = desc?.length,
|
||||
val isClickable: Boolean = false,
|
||||
val childCount: Int = 0,
|
||||
val index: Int = 0,
|
||||
val depth: Int = 0,
|
||||
val left: Int,
|
||||
val top: Int,
|
||||
val right: Int,
|
||||
val bottom: Int,
|
||||
) {
|
||||
companion object {
|
||||
/**
|
||||
* 不要在多线程中使用
|
||||
*/
|
||||
private val rect = Rect()
|
||||
fun info2data(
|
||||
nodeInfo: AccessibilityNodeInfo,
|
||||
): Attr {
|
||||
): AttrSnapshot {
|
||||
nodeInfo.getBoundsInScreen(rect)
|
||||
return Attr(
|
||||
return AttrSnapshot(
|
||||
id = nodeInfo.viewIdResourceName,
|
||||
className = nodeInfo.className?.toString(),
|
||||
childCount = nodeInfo.childCount,
|
||||
name = nodeInfo.className?.toString(),
|
||||
text = nodeInfo.text?.toString(),
|
||||
isClickable = nodeInfo.isClickable,
|
||||
desc = nodeInfo.contentDescription?.toString(),
|
||||
isClickable = nodeInfo.isClickable,
|
||||
childCount = nodeInfo.childCount,
|
||||
index = nodeInfo.getIndex(),
|
||||
depth = nodeInfo.getDepth(),
|
||||
left = rect.left,
|
||||
top = rect.top,
|
||||
right = rect.right,
|
|
@ -1,18 +1,25 @@
|
|||
package li.songe.gkd.debug.server.api
|
||||
package li.songe.gkd.debug
|
||||
|
||||
import android.os.Build
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class Device(
|
||||
data class DeviceSnapshot(
|
||||
@SerialName("device")
|
||||
val device: String = Build.DEVICE,
|
||||
@SerialName("model")
|
||||
val model: String = Build.MODEL,
|
||||
@SerialName("manufacturer")
|
||||
val manufacturer: String = Build.MANUFACTURER,
|
||||
@SerialName("brand")
|
||||
val brand: String = Build.BRAND,
|
||||
@SerialName("sdkInt")
|
||||
val sdkInt: Int = Build.VERSION.SDK_INT,
|
||||
@SerialName("release")
|
||||
val release: String = Build.VERSION.RELEASE,
|
||||
) {
|
||||
companion object {
|
||||
val singleton by lazy { Device() }
|
||||
){
|
||||
companion object{
|
||||
val instance by lazy { DeviceSnapshot() }
|
||||
}
|
||||
}
|
|
@ -1,60 +0,0 @@
|
|||
package li.songe.gkd.debug
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.encodeToString
|
||||
import li.songe.gkd.App
|
||||
import li.songe.gkd.accessibility.GkdAbService
|
||||
import li.songe.gkd.debug.server.RpcError
|
||||
import li.songe.gkd.debug.server.api.Snapshot
|
||||
import li.songe.gkd.debug.server.api.Window
|
||||
import li.songe.gkd.util.Ext.getApplicationInfoExt
|
||||
import li.songe.gkd.util.Singleton
|
||||
import java.io.File
|
||||
|
||||
object Ext {
|
||||
val snapshotDir by lazy {
|
||||
App.context.getExternalFilesDir("server-snapshot")!!.apply { if (!exists()) mkdir() }
|
||||
}
|
||||
val windowDir by lazy {
|
||||
App.context.getExternalFilesDir("server-window")!!.apply { if (!exists()) mkdir() }
|
||||
}
|
||||
val screenshotDir by lazy {
|
||||
App.context.getExternalFilesDir("server-screenshot")!!.apply { if (!exists()) mkdir() }
|
||||
}
|
||||
|
||||
suspend fun captureSnapshot(): Snapshot {
|
||||
if (!GkdAbService.isRunning()) {
|
||||
throw RpcError("无障碍不可用")
|
||||
}
|
||||
val packageManager = App.context.packageManager
|
||||
val windowInfo = Window.singleton
|
||||
val bitmap = ScreenshotService.screenshot() ?: throw RpcError("截屏不可用")
|
||||
val snapshot = Snapshot(
|
||||
appId = windowInfo.appId ?: "",
|
||||
activityId = windowInfo.activityId ?: "",
|
||||
appName = windowInfo.appId?.let { appId ->
|
||||
packageManager.getApplicationLabel(
|
||||
packageManager.getApplicationInfoExt(
|
||||
appId
|
||||
)
|
||||
).toString()
|
||||
} ?: "",
|
||||
)
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
File(windowDir.absolutePath + "/${snapshot.id}.json").writeText(
|
||||
Singleton.json.encodeToString(windowInfo)
|
||||
)
|
||||
val stream =
|
||||
File(screenshotDir.absolutePath + "/${snapshot.id}.png").outputStream()
|
||||
bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream)
|
||||
stream.close()
|
||||
File(snapshotDir.absolutePath + "/${snapshot.id}.json").writeText(
|
||||
Singleton.json.encodeToString(snapshot)
|
||||
)
|
||||
}
|
||||
return snapshot
|
||||
}
|
||||
}
|
|
@ -11,7 +11,6 @@ import li.songe.gkd.R
|
|||
import li.songe.gkd.composition.CompositionFbService
|
||||
import li.songe.gkd.composition.CompositionExt.useMessage
|
||||
import li.songe.gkd.composition.InvokeMessage
|
||||
import li.songe.gkd.debug.server.HttpService
|
||||
|
||||
class FloatingService : CompositionFbService({
|
||||
val context = this
|
||||
|
|
|
@ -1,14 +1,11 @@
|
|||
package li.songe.gkd.debug.server
|
||||
package li.songe.gkd.debug
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import com.blankj.utilcode.util.LogUtils
|
||||
import com.blankj.utilcode.util.ServiceUtils
|
||||
import io.ktor.client.request.get
|
||||
import io.ktor.client.statement.bodyAsText
|
||||
import com.blankj.utilcode.util.ToastUtils
|
||||
import io.ktor.http.CacheControl
|
||||
import io.ktor.http.HttpHeaders
|
||||
import io.ktor.http.HttpStatusCode
|
||||
import io.ktor.serialization.kotlinx.json.json
|
||||
import io.ktor.server.application.call
|
||||
import io.ktor.server.application.install
|
||||
|
@ -16,9 +13,7 @@ import io.ktor.server.engine.embeddedServer
|
|||
import io.ktor.server.netty.Netty
|
||||
import io.ktor.server.plugins.contentnegotiation.ContentNegotiation
|
||||
import io.ktor.server.plugins.cors.routing.CORS
|
||||
import io.ktor.server.request.uri
|
||||
import io.ktor.server.response.cacheControl
|
||||
import io.ktor.server.response.header
|
||||
import io.ktor.server.response.respond
|
||||
import io.ktor.server.response.respondFile
|
||||
import io.ktor.server.routing.get
|
||||
|
@ -30,18 +25,13 @@ import kotlinx.coroutines.cancel
|
|||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import li.songe.gkd.App
|
||||
import li.songe.gkd.composition.CompositionService
|
||||
import li.songe.gkd.composition.CompositionExt.useMessage
|
||||
import li.songe.gkd.composition.CompositionService
|
||||
import li.songe.gkd.composition.InvokeMessage
|
||||
import li.songe.gkd.debug.Ext.captureSnapshot
|
||||
import li.songe.gkd.debug.Ext.screenshotDir
|
||||
import li.songe.gkd.debug.Ext.snapshotDir
|
||||
import li.songe.gkd.debug.Ext.windowDir
|
||||
import li.songe.gkd.debug.FloatingService
|
||||
import li.songe.gkd.debug.server.api.Device
|
||||
import li.songe.gkd.debug.SnapshotExt.captureSnapshot
|
||||
import li.songe.gkd.util.Ext.getIpAddressInLocalNetwork
|
||||
import li.songe.gkd.util.Singleton
|
||||
import li.songe.gkd.util.Storage
|
||||
import java.io.File
|
||||
|
||||
class HttpService : CompositionService({
|
||||
val scope = CoroutineScope(Dispatchers.IO)
|
||||
|
@ -60,7 +50,9 @@ class HttpService : CompositionService({
|
|||
delay(200)
|
||||
try {
|
||||
captureSnapshot()
|
||||
ToastUtils.showShort("保存快照成功")
|
||||
} catch (e: Exception) {
|
||||
ToastUtils.showShort("保存快照失败")
|
||||
e.printStackTrace()
|
||||
}
|
||||
showBubbles()
|
||||
|
@ -78,57 +70,43 @@ class HttpService : CompositionService({
|
|||
install(ContentNegotiation) { json() }
|
||||
|
||||
routing {
|
||||
route("/api/rpc") {
|
||||
get("/device") {
|
||||
call.respond(Device.singleton)
|
||||
}
|
||||
get("/capture") {
|
||||
removeBubbles()
|
||||
delay(200)
|
||||
try {
|
||||
call.respond(captureSnapshot())
|
||||
} catch (e: Exception) {
|
||||
throw e
|
||||
} finally {
|
||||
showBubbles()
|
||||
}
|
||||
route("/api") {
|
||||
get("/device") { call.respond(DeviceSnapshot.instance) }
|
||||
get("/snapshotIds") {
|
||||
call.respond(SnapshotExt.getSnapshotIds())
|
||||
}
|
||||
get("/snapshot") {
|
||||
val id = call.request.queryParameters["id"]?.toLongOrNull()
|
||||
?: throw RpcError("miss id")
|
||||
call.response.cacheControl(CacheControl.MaxAge(3600))
|
||||
call.respondFile(snapshotDir, "/${id}.json")
|
||||
if (id != null) {
|
||||
val fp = File(SnapshotExt.getSnapshotPath(id))
|
||||
if (!fp.exists()) {
|
||||
throw RpcError("对应快照不存在")
|
||||
}
|
||||
call.response.cacheControl(CacheControl.MaxAge(3600))
|
||||
call.respondFile(fp)
|
||||
} else {
|
||||
removeBubbles()
|
||||
delay(200)
|
||||
try {
|
||||
call.respond(captureSnapshot())
|
||||
} catch (e: Exception) {
|
||||
throw e
|
||||
} finally {
|
||||
showBubbles()
|
||||
}
|
||||
}
|
||||
}
|
||||
get("/window") {
|
||||
val id = call.request.queryParameters["id"]?.toLongOrNull()
|
||||
?: throw RpcError("miss id")
|
||||
call.response.cacheControl(CacheControl.MaxAge(3600))
|
||||
call.respondFile(windowDir, "/${id}.json")
|
||||
}
|
||||
|
||||
get("/screenshot") {
|
||||
val id = call.request.queryParameters["id"]?.toLongOrNull()
|
||||
?: throw RpcError("miss id")
|
||||
val fp = File(SnapshotExt.getScreenshotPath(id))
|
||||
if (!fp.exists()) {
|
||||
throw RpcError("对应截图不存在")
|
||||
}
|
||||
call.response.cacheControl(CacheControl.MaxAge(3600))
|
||||
call.respondFile(screenshotDir, "/${id}.png")
|
||||
call.respondFile(fp)
|
||||
}
|
||||
}
|
||||
listOf("/", "/index.html").forEach { p ->
|
||||
get(p) {
|
||||
val response = Singleton.client.get("$proxyUrl${call.request.uri}")
|
||||
call.response.header(
|
||||
HttpHeaders.ContentType, "text/html; charset=UTF-8"
|
||||
)
|
||||
call.respond(response.bodyAsText())
|
||||
}
|
||||
}
|
||||
get("/assets/*") {
|
||||
call.response.header(
|
||||
HttpHeaders.Location,
|
||||
"$proxyUrl${context.request.uri}"
|
||||
)
|
||||
call.respond(HttpStatusCode.Found)
|
||||
}
|
||||
}
|
||||
}
|
||||
scope.launch {
|
||||
|
@ -156,6 +134,5 @@ class HttpService : CompositionService({
|
|||
context.startService(Intent(context, HttpService::class.java))
|
||||
}
|
||||
|
||||
private const val proxyUrl = "https://gkd-ui-viewer.netlify.app"
|
||||
}
|
||||
}
|
|
@ -1,8 +1,8 @@
|
|||
package li.songe.gkd.debug.server.api
|
||||
package li.songe.gkd.debug
|
||||
|
||||
import android.view.accessibility.AccessibilityNodeInfo
|
||||
import kotlinx.serialization.Serializable
|
||||
import li.songe.selector_android.forEach
|
||||
import li.songe.gkd.selector.forEachIndexed
|
||||
import java.util.ArrayDeque
|
||||
|
||||
|
||||
|
@ -11,37 +11,43 @@ import java.util.ArrayDeque
|
|||
*/
|
||||
|
||||
@Serializable
|
||||
data class Node(
|
||||
data class NodeSnapshot(
|
||||
val id: Int,
|
||||
val pid: Int,
|
||||
val attr: Attr
|
||||
val index: Int,
|
||||
/**
|
||||
* null: when getChild(i) return null
|
||||
*/
|
||||
val attr: AttrSnapshot?
|
||||
) {
|
||||
companion object {
|
||||
fun info2data(
|
||||
nodeInfo: AccessibilityNodeInfo,
|
||||
fun abNodeToNode(
|
||||
nodeInfo: AccessibilityNodeInfo?,
|
||||
id: Int = 0,
|
||||
pid: Int = -1
|
||||
): Node {
|
||||
return Node(
|
||||
pid: Int = -1,
|
||||
index: Int = 0,
|
||||
): NodeSnapshot {
|
||||
return NodeSnapshot(
|
||||
id,
|
||||
pid,
|
||||
Attr.info2data(nodeInfo)
|
||||
index,
|
||||
nodeInfo?.let { AttrSnapshot.info2data(nodeInfo) }
|
||||
)
|
||||
}
|
||||
|
||||
fun info2nodeList(nodeInfo: AccessibilityNodeInfo?): List<Node> {
|
||||
fun info2nodeList(nodeInfo: AccessibilityNodeInfo?): List<NodeSnapshot> {
|
||||
if (nodeInfo == null) {
|
||||
return emptyList()
|
||||
}
|
||||
val stack = ArrayDeque<Pair<Int, AccessibilityNodeInfo>>()
|
||||
val stack = ArrayDeque<Pair<Int, AccessibilityNodeInfo?>>()
|
||||
stack.push(0 to nodeInfo)
|
||||
val list = mutableListOf<Node>()
|
||||
list.add(info2data(nodeInfo))
|
||||
val list = mutableListOf<NodeSnapshot>()
|
||||
list.add(abNodeToNode(nodeInfo, index = 0))
|
||||
while (stack.isNotEmpty()) {
|
||||
val top = stack.pop()
|
||||
top.second.forEach { childNode ->
|
||||
top.second?.forEachIndexed { index, childNode ->
|
||||
stack.push(list.size to childNode)
|
||||
list.add(info2data(childNode, list.size, top.first))
|
||||
list.add(abNodeToNode(childNode, list.size, top.first, index))
|
||||
}
|
||||
}
|
||||
return list
|
|
@ -1,4 +1,4 @@
|
|||
package li.songe.gkd.debug.server
|
||||
package li.songe.gkd.debug
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
|
@ -6,6 +6,7 @@ import kotlinx.serialization.Serializable
|
|||
data class RpcError(
|
||||
override val message: String = "unknown error",
|
||||
val code: Int = 0,
|
||||
val X_Rpc_Result: Boolean = true
|
||||
) : Exception(message) {
|
||||
companion object {
|
||||
const val HeaderKey = "X_Rpc_Result"
|
|
@ -1,4 +1,4 @@
|
|||
package li.songe.gkd.debug.server
|
||||
package li.songe.gkd.debug
|
||||
|
||||
import android.util.Log
|
||||
import com.blankj.utilcode.util.LogUtils
|
||||
|
@ -35,7 +35,8 @@ val RpcErrorHeaderPlugin = createApplicationPlugin(name = "RpcErrorHeaderPlugin"
|
|||
}
|
||||
}
|
||||
onCallRespond { call, _ ->
|
||||
val status=call.response.status() ?: HttpStatusCode.OK
|
||||
call.response.header("Access-Control-Expose-Headers", "*")
|
||||
val status = call.response.status() ?: HttpStatusCode.OK
|
||||
if (status == HttpStatusCode.OK &&
|
||||
!call.response.headers.contains(
|
||||
RpcError.HeaderKey
|
32
app/src/main/java/li/songe/gkd/debug/Snapshot.kt
Normal file
32
app/src/main/java/li/songe/gkd/debug/Snapshot.kt
Normal file
|
@ -0,0 +1,32 @@
|
|||
package li.songe.gkd.debug
|
||||
|
||||
import com.blankj.utilcode.util.ScreenUtils
|
||||
import kotlinx.serialization.Serializable
|
||||
import li.songe.gkd.accessibility.GkdAbService
|
||||
import li.songe.gkd.util.Ext
|
||||
|
||||
@Serializable
|
||||
data class Snapshot(
|
||||
val id: Long = System.currentTimeMillis(),
|
||||
val device: DeviceSnapshot? = DeviceSnapshot.instance,
|
||||
val screenHeight: Int = ScreenUtils.getScreenHeight(),
|
||||
val screenWidth: Int = ScreenUtils.getScreenWidth(),
|
||||
val appId: String? = null,
|
||||
val appName: String? = null,
|
||||
val activityId: String? = null,
|
||||
val nodes: List<NodeSnapshot>? = null,
|
||||
) {
|
||||
companion object {
|
||||
fun current(): Snapshot {
|
||||
val shot = GkdAbService.currentNodeSnapshot()
|
||||
return Snapshot(
|
||||
appId = shot?.appId,
|
||||
appName = if (shot?.appId != null) {
|
||||
Ext.getAppName(shot.appId)
|
||||
} else null,
|
||||
activityId = shot?.activityId,
|
||||
nodes = NodeSnapshot.info2nodeList(shot?.root),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
59
app/src/main/java/li/songe/gkd/debug/SnapshotExt.kt
Normal file
59
app/src/main/java/li/songe/gkd/debug/SnapshotExt.kt
Normal file
|
@ -0,0 +1,59 @@
|
|||
package li.songe.gkd.debug
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import com.blankj.utilcode.util.LogUtils
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import kotlinx.serialization.encodeToString
|
||||
import li.songe.gkd.App
|
||||
import li.songe.gkd.accessibility.GkdAbService
|
||||
import li.songe.gkd.util.Singleton
|
||||
import java.io.File
|
||||
|
||||
object SnapshotExt {
|
||||
private val snapshotDir by lazy {
|
||||
App.context.getExternalFilesDir("snapshot")!!.apply { if (!exists()) mkdir() }
|
||||
}
|
||||
|
||||
private fun getSnapshotParentPath(snapshotId: Long) =
|
||||
"${snapshotDir.absolutePath}/${snapshotId}"
|
||||
|
||||
fun getSnapshotPath(snapshotId: Long) =
|
||||
"${getSnapshotParentPath(snapshotId)}/${snapshotId}.json"
|
||||
|
||||
fun getScreenshotPath(snapshotId: Long) =
|
||||
"${getSnapshotParentPath(snapshotId)}/${snapshotId}.png"
|
||||
|
||||
fun getSnapshotIds(): List<Long> {
|
||||
return snapshotDir.listFiles { f -> f.isDirectory }
|
||||
?.mapNotNull { f -> f.name.toLongOrNull() } ?: emptyList()
|
||||
}
|
||||
|
||||
suspend fun captureSnapshot(): Snapshot {
|
||||
if (!GkdAbService.isRunning()) {
|
||||
throw RpcError("无障碍不可用")
|
||||
}
|
||||
if (!ScreenshotService.isRunning()) {
|
||||
LogUtils.d("截屏不可用,即将使用空白图片")
|
||||
}
|
||||
val snapshot = Snapshot.current()
|
||||
val bitmap = withTimeoutOrNull(3_000) {
|
||||
ScreenshotService.screenshot()
|
||||
} ?: Bitmap.createBitmap(
|
||||
snapshot.screenWidth,
|
||||
snapshot.screenHeight,
|
||||
Bitmap.Config.ARGB_8888
|
||||
)
|
||||
withContext(Dispatchers.IO) {
|
||||
File(getSnapshotParentPath(snapshot.id)).apply { if (!exists()) mkdirs() }
|
||||
val stream =
|
||||
File(getScreenshotPath(snapshot.id)).outputStream()
|
||||
bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream)
|
||||
stream.close()
|
||||
val text = Singleton.json.encodeToString(snapshot)
|
||||
File(getSnapshotPath(snapshot.id)).writeText(text)
|
||||
}
|
||||
return snapshot
|
||||
}
|
||||
}
|
|
@ -1,18 +0,0 @@
|
|||
package li.songe.gkd.debug.server.api
|
||||
|
||||
import android.os.Build
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class Snapshot(
|
||||
val id: Long = System.currentTimeMillis(),
|
||||
val device: String = Build.DEVICE,
|
||||
val versionRelease: String = Build.VERSION.RELEASE,
|
||||
val model: String = Build.MODEL,
|
||||
val manufacturer: String = Build.MANUFACTURER,
|
||||
val androidVersion: Int = Build.VERSION.SDK_INT,
|
||||
val appId: String = "",
|
||||
val appName: String = "",
|
||||
val activityId: String = "",
|
||||
val comment: String = ""
|
||||
)
|
|
@ -1,24 +0,0 @@
|
|||
package li.songe.gkd.debug.server.api
|
||||
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import li.songe.gkd.accessibility.GkdAbService
|
||||
|
||||
@Serializable
|
||||
data class Window(
|
||||
val appId: String? = null,
|
||||
val activityId: String? = null,
|
||||
val nodes: List<Node>? = null,
|
||||
) {
|
||||
companion object {
|
||||
val singleton: Window
|
||||
get() {
|
||||
val shot = GkdAbService.currentNodeSnapshot()
|
||||
return Window(
|
||||
appId = shot?.appId,
|
||||
activityId = shot?.activityId,
|
||||
nodes = Node.info2nodeList(shot?.root)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,13 +1,13 @@
|
|||
package li.songe.gkd.selector
|
||||
|
||||
import android.view.accessibility.AccessibilityNodeInfo
|
||||
import li.songe.selector_core.Node
|
||||
import li.songe.selector_core.NodeExt
|
||||
|
||||
@JvmInline
|
||||
value class AbNode(val value: AccessibilityNodeInfo) : Node {
|
||||
override val parent: Node?
|
||||
value class AbNode(val value: AccessibilityNodeInfo) : NodeExt {
|
||||
override val parent: NodeExt?
|
||||
get() = value.parent?.let { AbNode(it) }
|
||||
override val children: Sequence<Node?>
|
||||
override val children: Sequence<NodeExt?>
|
||||
get() = sequence {
|
||||
repeat(value.childCount) { i ->
|
||||
val child = value.getChild(i)
|
||||
|
@ -32,8 +32,9 @@ value class AbNode(val value: AccessibilityNodeInfo) : Node {
|
|||
"desc" -> value.contentDescription
|
||||
"descLen" -> value.contentDescription?.length
|
||||
"isClickable" -> value.isClickable
|
||||
"isChecked" -> value.isChecked
|
||||
"childCount" -> value.childCount
|
||||
"index" -> value.getIndex()
|
||||
"depth" -> value.getDepth()
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,25 +1,27 @@
|
|||
package li.songe.gkd.selector
|
||||
|
||||
import android.accessibilityservice.AccessibilityService
|
||||
import android.accessibilityservice.GestureDescription
|
||||
import android.graphics.Path
|
||||
import android.graphics.Rect
|
||||
import android.view.accessibility.AccessibilityNodeInfo
|
||||
import li.songe.selector_core.Selector
|
||||
|
||||
fun AccessibilityNodeInfo.getIndex(): Int? {
|
||||
fun AccessibilityNodeInfo.getIndex(): Int {
|
||||
parent?.forEachIndexed { index, accessibilityNodeInfo ->
|
||||
if (accessibilityNodeInfo == this) {
|
||||
return index
|
||||
}
|
||||
}
|
||||
return null
|
||||
return 0
|
||||
}
|
||||
|
||||
inline fun AccessibilityNodeInfo.forEachIndexed(action: (index: Int, childNode: AccessibilityNodeInfo) -> Unit) {
|
||||
inline fun AccessibilityNodeInfo.forEachIndexed(action: (index: Int, childNode: AccessibilityNodeInfo?) -> Unit) {
|
||||
var index = 0
|
||||
val childCount = this.childCount
|
||||
while (index < childCount) {
|
||||
val child: AccessibilityNodeInfo? = getChild(index)
|
||||
if (child != null) {
|
||||
action(index, child)
|
||||
}
|
||||
action(index, child)
|
||||
index += 1
|
||||
}
|
||||
}
|
||||
|
@ -30,5 +32,46 @@ fun AccessibilityNodeInfo.querySelector(selector: Selector): AccessibilityNodeIn
|
|||
return result.value
|
||||
}
|
||||
|
||||
fun AccessibilityNodeInfo.querySelectorAll(selector: Selector) =
|
||||
(AbNode(this).querySelectorAll(selector) as Sequence<AbNode>)
|
||||
fun AccessibilityNodeInfo.querySelectorAll(selector: Selector): Sequence<AbNode> {
|
||||
val ab = AbNode(this)
|
||||
return ab.querySelectorAll(selector) as Sequence<AbNode>
|
||||
}
|
||||
|
||||
fun AccessibilityNodeInfo.click(service: AccessibilityService) = when {
|
||||
this.isClickable -> {
|
||||
this.performAction(AccessibilityNodeInfo.ACTION_CLICK)
|
||||
"self"
|
||||
}
|
||||
|
||||
else -> {
|
||||
val react = Rect()
|
||||
this.getBoundsInScreen(react)
|
||||
val x = react.left + 50f / 100f * (react.right - react.left)
|
||||
val y = react.top + 50f / 100f * (react.bottom - react.top)
|
||||
if (x >= 0 && y >= 0) {
|
||||
val gestureDescription = GestureDescription.Builder()
|
||||
val path = Path()
|
||||
path.moveTo(x, y)
|
||||
gestureDescription.addStroke(GestureDescription.StrokeDescription(path, 0, 300))
|
||||
service.dispatchGesture(gestureDescription.build(), null, null)
|
||||
"(50%, 50%)"
|
||||
} else {
|
||||
"($x, $y) no click"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun AccessibilityNodeInfo.getDepth(): Int {
|
||||
var p: AccessibilityNodeInfo? = this
|
||||
var depth = 0
|
||||
while (true) {
|
||||
val p2 = p?.parent
|
||||
if (p2 != null) {
|
||||
p = p2
|
||||
depth++
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
return depth
|
||||
}
|
|
@ -28,7 +28,7 @@ import kotlinx.coroutines.launch
|
|||
import li.songe.gkd.accessibility.GkdAbService
|
||||
import li.songe.gkd.debug.FloatingService
|
||||
import li.songe.gkd.debug.ScreenshotService
|
||||
import li.songe.gkd.debug.server.HttpService
|
||||
import li.songe.gkd.debug.HttpService
|
||||
import li.songe.gkd.ui.component.StatusBar
|
||||
import li.songe.gkd.ui.component.TextSwitch
|
||||
import li.songe.gkd.util.Ext
|
||||
|
|
|
@ -27,6 +27,7 @@ import kotlinx.coroutines.launch
|
|||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import li.songe.gkd.App
|
||||
import li.songe.gkd.MainActivity
|
||||
import li.songe.gkd.R
|
||||
import li.songe.gkd.data.RuleManager
|
||||
|
@ -59,6 +60,15 @@ object Ext {
|
|||
}
|
||||
}
|
||||
|
||||
fun getAppName(appId: String? = null): String? {
|
||||
appId ?: return null
|
||||
return App.context.packageManager.getApplicationLabel(
|
||||
App.context.packageManager.getApplicationInfoExt(
|
||||
appId
|
||||
)
|
||||
).toString()
|
||||
}
|
||||
|
||||
fun Bitmap.isEmptyBitmap(): Boolean {
|
||||
val emptyBitmap = Bitmap.createBitmap(width, height, config)
|
||||
return this.sameAs(emptyBitmap)
|
||||
|
@ -212,9 +222,8 @@ object Ext {
|
|||
}
|
||||
|
||||
|
||||
|
||||
fun createNotificationChannel(context: Service) {
|
||||
val channelId = "CHANNEL_TEST"
|
||||
val channelId = "channel_service_ab"
|
||||
val intent = Intent(context, MainActivity::class.java).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
}
|
||||
|
@ -235,8 +244,8 @@ object Ext {
|
|||
.setOngoing(true)
|
||||
.setAutoCancel(false)
|
||||
|
||||
val name = "调试模式2"
|
||||
val descriptionText = "测试后台任务"
|
||||
val name = "无障碍服务"
|
||||
val descriptionText = "无障碍服务保持活跃"
|
||||
val importance = NotificationManager.IMPORTANCE_DEFAULT
|
||||
val channel = NotificationChannel(channelId, name, importance).apply {
|
||||
description = descriptionText
|
||||
|
|
|
@ -18,6 +18,7 @@ object Singleton {
|
|||
Json {
|
||||
isLenient = true
|
||||
ignoreUnknownKeys = true
|
||||
encodeDefaults = true
|
||||
}
|
||||
}
|
||||
val json5: Jankson by lazy { Jankson.builder().build() }
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:accessibilityEventTypes="typeAllMask"
|
||||
android:accessibilityFeedbackType="feedbackGeneric"
|
||||
android:accessibilityFeedbackType="feedbackAllMask"
|
||||
android:accessibilityFlags="flagReportViewIds|flagIncludeNotImportantViews|flagDefault|flagRetrieveInteractiveWindows"
|
||||
android:canPerformGestures="true"
|
||||
android:canRetrieveWindowContent="true"
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
package li.songe.gkd
|
||||
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import li.songe.gkd.debug.server.api.Window
|
||||
import li.songe.gkd.debug.Snapshot
|
||||
import li.songe.gkd.util.Singleton
|
||||
import li.songe.selector_core.Node
|
||||
import li.songe.selector_core.NodeExt
|
||||
import li.songe.selector_core.Selector
|
||||
import org.junit.Test
|
||||
import java.io.File
|
||||
import li.songe.gkd.debug.server.api.Node as ApiNode
|
||||
import li.songe.gkd.debug.NodeSnapshot as ApiNode
|
||||
|
||||
/**
|
||||
* Example local unit test, which will execute on the development machine (host).
|
||||
|
@ -14,34 +15,32 @@ import li.songe.gkd.debug.server.api.Node as ApiNode
|
|||
* See [testing documentation](http://d.android.com/tools/testing).
|
||||
*/
|
||||
class ExampleUnitTest {
|
||||
// @Test
|
||||
@Test
|
||||
fun check_selector() {
|
||||
// println(Selector.parse("X View >n Text > Button[a=1][b=false][c=null][d!=`hello`] + A - X < Z"))
|
||||
// println(Selector.parse("A[a=1][a!=3][a*=3][a!*=3][a^=null]"))
|
||||
// println(Selector.parse("@LinearLayout > TextView[id=`com.byted.pangle:id/tt_item_tv`][text=`不感兴趣`]"))
|
||||
|
||||
// val s1 = "ImageView < @FrameLayout < LinearLayout < RelativeLayout <n\n" +
|
||||
// "LinearLayout < RelativeLayout + LinearLayout > RelativeLayout > TextView[text$=`广告`]"
|
||||
// val selector = Selector.parse(s1)
|
||||
//// Selector.parse("ImageView < @FrameLayout < LinearLayout < RelativeLayout <n LinearLayout < RelativeLayout + LinearLayout > RelativeLayout > TextView[text$=`广告`]")
|
||||
//
|
||||
//
|
||||
// val nodes =
|
||||
// Singleton.json.decodeFromString<Window>(File("D:/User/Downloads/gkd/snapshot-1684381133305/window.json").readText()).nodes
|
||||
// ?: emptyList()
|
||||
//
|
||||
// val simpleNodes = nodes.map { n ->
|
||||
// SimpleNode(
|
||||
// value = n
|
||||
// )
|
||||
// }
|
||||
// simpleNodes.forEach { simpleNode ->
|
||||
// simpleNode.parent = simpleNodes.getOrNull(simpleNode.value.pid)?.apply {
|
||||
// children.add(simpleNode)
|
||||
// }
|
||||
// }
|
||||
// val rootWrapper = simpleNodes.map { SimpleNodeWrapper(it) }[0]
|
||||
// println(rootWrapper.querySelector(selector))
|
||||
val s1 =
|
||||
"TextView[text$=`的广告`] - Image[id=null]" to "D:/User/Downloads/edge/snapshot-1684652764082/snapshot.json"
|
||||
|
||||
val selector = Selector.parse(s1.first)
|
||||
val nodes =
|
||||
Singleton.json.decodeFromString<Snapshot>(File(s1.second).readText()).nodes
|
||||
?: emptyList()
|
||||
|
||||
val simpleNodes = nodes.map { n ->
|
||||
SimpleNode(
|
||||
value = n
|
||||
)
|
||||
}
|
||||
simpleNodes.forEach { simpleNode ->
|
||||
simpleNode.parent = simpleNodes.getOrNull(simpleNode.value.pid)?.apply {
|
||||
children.add(simpleNode)
|
||||
}
|
||||
}
|
||||
val rootWrapper = simpleNodes.map { SimpleNodeWrapper(it) }[0]
|
||||
println(rootWrapper.querySelector(selector))
|
||||
}
|
||||
|
||||
class SimpleNode(
|
||||
|
@ -54,27 +53,27 @@ class ExampleUnitTest {
|
|||
}
|
||||
}
|
||||
|
||||
data class SimpleNodeWrapper(val value: SimpleNode) : Node {
|
||||
data class SimpleNodeWrapper(val value: SimpleNode) : NodeExt {
|
||||
|
||||
override val parent: Node?
|
||||
override val parent: NodeExt?
|
||||
get() = value.parent?.let { SimpleNodeWrapper(it) }
|
||||
override val children: Sequence<Node?>
|
||||
override val children: Sequence<NodeExt?>
|
||||
get() = sequence {
|
||||
value.children.forEach { yield(SimpleNodeWrapper(it)) }
|
||||
}
|
||||
override val name: CharSequence
|
||||
get() = value.value.attr.className ?: ""
|
||||
get() = value.value.attr?.name ?: ""
|
||||
|
||||
override fun attr(name: String): Any? {
|
||||
val attr = value.value.attr
|
||||
return when (name) {
|
||||
"id" -> attr.id
|
||||
"name" -> attr.className
|
||||
"text" -> attr.text
|
||||
"textLen" -> attr.text?.length
|
||||
"desc" -> attr.desc
|
||||
"descLen" -> attr.desc?.length
|
||||
"isClickable" -> attr.isClickable
|
||||
"id" -> attr?.id
|
||||
"name" -> attr?.name
|
||||
"text" -> attr?.text
|
||||
"textLen" -> attr?.text?.length
|
||||
"desc" -> attr?.desc
|
||||
"descLen" -> attr?.desc?.length
|
||||
"isClickable" -> attr?.isClickable
|
||||
"isChecked" -> null
|
||||
"index" -> {
|
||||
val children = value.parent?.children ?: return null
|
||||
|
|
|
@ -1,49 +0,0 @@
|
|||
package li.songe.selector_android
|
||||
|
||||
import android.accessibilityservice.AccessibilityService
|
||||
import android.accessibilityservice.GestureDescription
|
||||
import android.graphics.Path
|
||||
import android.graphics.Rect
|
||||
import android.view.accessibility.AccessibilityNodeInfo
|
||||
import li.songe.selector_android.parser.Transform
|
||||
import li.songe.selector_android.wrapper.PropertySelectorWrapper
|
||||
|
||||
data class GkdSelector(val wrapper: PropertySelectorWrapper) {
|
||||
|
||||
override fun toString() = wrapper.toString()
|
||||
|
||||
fun collect(nodeInfo: AccessibilityNodeInfo): AccessibilityNodeInfo? {
|
||||
for (child in nodeInfo.traverse()) {
|
||||
val trackNodes = wrapper.match(child)
|
||||
if (trackNodes != null) {
|
||||
return trackNodes.findLast { it != null } ?: child
|
||||
}
|
||||
}
|
||||
nodeInfo.getChild(1)
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
companion object {
|
||||
val gkdSelectorParser = Transform.gkdSelectorParser
|
||||
fun click(nodeInfo: AccessibilityNodeInfo, service: AccessibilityService) = when {
|
||||
nodeInfo.isClickable -> {
|
||||
nodeInfo.performAction(AccessibilityNodeInfo.ACTION_CLICK)
|
||||
"self"
|
||||
}
|
||||
else -> {
|
||||
val react = Rect()
|
||||
nodeInfo.getBoundsInScreen(react)
|
||||
val x = react.left + 50f / 100f * (react.right - react.left)
|
||||
val y = react.top + 50f / 100f * (react.bottom - react.top)
|
||||
val gestureDescription = GestureDescription.Builder()
|
||||
val path = Path()
|
||||
path.moveTo(x, y)
|
||||
gestureDescription.addStroke(GestureDescription.StrokeDescription(path, 0, 300))
|
||||
service.dispatchGesture(gestureDescription.build(), null, null)
|
||||
"(50%, 50%)"
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
|
@ -7,6 +7,3 @@ java {
|
|||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
dependencies {
|
||||
}
|
|
@ -1,21 +1,19 @@
|
|||
package li.songe.selector_core
|
||||
|
||||
interface Node {
|
||||
val parent: Node?
|
||||
val children: Sequence<Node?>
|
||||
|
||||
interface NodeExt {
|
||||
val parent: NodeExt?
|
||||
val children: Sequence<NodeExt?>
|
||||
val name: CharSequence
|
||||
fun attr(name: String): Any?
|
||||
|
||||
/**
|
||||
* constant traversal
|
||||
*/
|
||||
fun getChild(offset: Int) = children.elementAtOrNull(offset)
|
||||
|
||||
fun attr(name: String): Any?
|
||||
|
||||
val ancestors: Sequence<Node>
|
||||
val ancestors: Sequence<NodeExt>
|
||||
get() = sequence {
|
||||
var parentVar: Node? = parent ?: return@sequence
|
||||
var parentVar: NodeExt? = parent ?: return@sequence
|
||||
while (parentVar != null) {
|
||||
yield(parentVar)
|
||||
parentVar = parentVar.parent
|
||||
|
@ -25,10 +23,10 @@ interface Node {
|
|||
fun getAncestor(offset: Int) = ancestors.elementAtOrNull(offset)
|
||||
|
||||
// if index=3, traverse 2,1,0
|
||||
val beforeBrothers: Sequence<Node?>
|
||||
val beforeBrothers: Sequence<NodeExt?>
|
||||
get() = sequence {
|
||||
val parentVal = parent ?: return@sequence
|
||||
val list = parentVal.children.takeWhile { it != this@Node }.toMutableList()
|
||||
val list = parentVal.children.takeWhile { it != this@NodeExt }.toMutableList()
|
||||
list.reverse()
|
||||
yieldAll(list)
|
||||
}
|
||||
|
@ -36,26 +34,33 @@ interface Node {
|
|||
fun getBeforeBrother(offset: Int) = beforeBrothers.elementAtOrNull(offset)
|
||||
|
||||
// if index=3, traverse 4,5,6...
|
||||
val afterBrothers: Sequence<Node?>
|
||||
val afterBrothers: Sequence<NodeExt?>
|
||||
get() = sequence {
|
||||
val parentVal = parent ?: return@sequence
|
||||
yieldAll(parentVal.children.dropWhile { it == this@Node })
|
||||
yieldAll(parentVal.children.dropWhile { it != this@NodeExt }.drop(1))
|
||||
}
|
||||
|
||||
fun getAfterBrother(offset: Int) = afterBrothers.elementAtOrNull(offset)
|
||||
|
||||
val descendants: Sequence<Node>
|
||||
val descendants: Sequence<NodeExt>
|
||||
get() = sequence {
|
||||
val stack = mutableListOf<Node>()
|
||||
stack.add(this@Node)
|
||||
// 深度优先先序遍历
|
||||
// https://developer.mozilla.org/zh-CN/docs/Web/API/Document/querySelector
|
||||
val stack = mutableListOf(this@NodeExt)
|
||||
val reverseList = mutableListOf<NodeExt>()
|
||||
do {
|
||||
val top = stack.removeLast()
|
||||
yield(top)
|
||||
for (childNode in top.children) {
|
||||
if (childNode != null) {
|
||||
stack.add(childNode)
|
||||
reverseList.add(childNode)
|
||||
}
|
||||
}
|
||||
if (reverseList.isNotEmpty()) {
|
||||
reverseList.reverse()
|
||||
stack.addAll(reverseList)
|
||||
reverseList.clear()
|
||||
}
|
||||
} while (stack.isNotEmpty())
|
||||
}
|
||||
|
||||
|
@ -71,3 +76,4 @@ interface Node {
|
|||
}
|
||||
|
||||
|
||||
|
|
@ -17,9 +17,7 @@ data class Selector(private val propertyWrapper: PropertyWrapper) {
|
|||
// }.toList().reversed()
|
||||
// }
|
||||
|
||||
fun match(node: Node): Node? {
|
||||
val text= node.attr("text") as CharSequence?
|
||||
|
||||
fun match(node: NodeExt): NodeExt? {
|
||||
val trackNodes = propertyWrapper.match(node) ?: return null
|
||||
return trackNodes.lastOrNull() ?: node
|
||||
}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
package li.songe.selector_core.data
|
||||
|
||||
import li.songe.selector_core.Node
|
||||
import li.songe.selector_core.NodeExt
|
||||
|
||||
data class BinaryExpression(val name: String, val operator: CompareOperator, val value: Any?) {
|
||||
fun match(node: Node) = operator.compare(node.attr(name), value)
|
||||
fun match(node: NodeExt) = operator.compare(node.attr(name), value)
|
||||
override fun toString() = "[${name}${operator}${
|
||||
if (value is String) {
|
||||
"`${value.replace("`", "\\`")}`"
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
package li.songe.selector_core.data
|
||||
|
||||
import li.songe.selector_core.Node
|
||||
import li.songe.selector_core.NodeExt
|
||||
|
||||
sealed class ConnectOperator(val key: String) {
|
||||
override fun toString() = key
|
||||
abstract fun traversal(node: Node): Sequence<Node?>
|
||||
abstract fun traversal(node: Node, offset: Int): Node?
|
||||
abstract fun traversal(node: NodeExt): Sequence<NodeExt?>
|
||||
abstract fun traversal(node: NodeExt, offset: Int): NodeExt?
|
||||
|
||||
companion object {
|
||||
val allSubClasses = listOf(
|
||||
|
@ -20,31 +20,31 @@ sealed class ConnectOperator(val key: String) {
|
|||
* A + B, 1,2,3,A,B,7,8
|
||||
*/
|
||||
object BeforeBrother : ConnectOperator("+") {
|
||||
override fun traversal(node: Node) = node.beforeBrothers
|
||||
override fun traversal(node: Node, offset: Int): Node? = node.getBeforeBrother(offset)
|
||||
override fun traversal(node: NodeExt) = node.beforeBrothers
|
||||
override fun traversal(node: NodeExt, offset: Int): NodeExt? = node.getBeforeBrother(offset)
|
||||
}
|
||||
|
||||
/**
|
||||
* A - B, 1,2,3,B,A,7,8
|
||||
*/
|
||||
object AfterBrother : ConnectOperator("-") {
|
||||
override fun traversal(node: Node) = node.afterBrothers
|
||||
override fun traversal(node: Node, offset: Int): Node? = node.getAfterBrother(offset)
|
||||
override fun traversal(node: NodeExt) = node.afterBrothers
|
||||
override fun traversal(node: NodeExt, offset: Int): NodeExt? = node.getAfterBrother(offset)
|
||||
}
|
||||
|
||||
/**
|
||||
* A > B, A is the ancestor of B
|
||||
*/
|
||||
object Ancestor : ConnectOperator(">") {
|
||||
override fun traversal(node: Node) = node.ancestors
|
||||
override fun traversal(node: Node, offset: Int): Node? = node.getAncestor(offset)
|
||||
override fun traversal(node: NodeExt) = node.ancestors
|
||||
override fun traversal(node: NodeExt, offset: Int): NodeExt? = node.getAncestor(offset)
|
||||
}
|
||||
|
||||
/**
|
||||
* A < B, A is the child of B
|
||||
*/
|
||||
object Child : ConnectOperator("<") {
|
||||
override fun traversal(node: Node) = node.children
|
||||
override fun traversal(node: Node, offset: Int): Node? = node.getChild(offset)
|
||||
override fun traversal(node: NodeExt) = node.children
|
||||
override fun traversal(node: NodeExt, offset: Int): NodeExt? = node.getChild(offset)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
package li.songe.selector_core.data
|
||||
|
||||
import li.songe.selector_core.Node
|
||||
import li.songe.selector_core.NodeExt
|
||||
|
||||
data class ConnectSegment(
|
||||
val operator: ConnectOperator = ConnectOperator.Ancestor,
|
||||
|
@ -13,15 +13,17 @@ data class ConnectSegment(
|
|||
return operator.toString() + polynomialExpression.toString()
|
||||
}
|
||||
|
||||
fun traversal(node: Node): Sequence<Node?> {
|
||||
if (polynomialExpression.isConstant) {
|
||||
return sequence {
|
||||
val traversal: (node: NodeExt) -> Sequence<NodeExt?> = if (polynomialExpression.isConstant) {
|
||||
({ node ->
|
||||
sequence {
|
||||
val node1 = operator.traversal(node, polynomialExpression.b1)
|
||||
if (node1 != null) {
|
||||
yield(node1)
|
||||
}
|
||||
}
|
||||
}
|
||||
return polynomialExpression.traversal(operator.traversal(node))
|
||||
})
|
||||
} else {
|
||||
({ node -> polynomialExpression.traversal(operator.traversal(node)) })
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
package li.songe.selector_core.data
|
||||
|
||||
import li.songe.selector_core.Node
|
||||
import li.songe.selector_core.NodeExt
|
||||
|
||||
data class ConnectWrapper(
|
||||
val connectSegment: ConnectSegment,
|
||||
|
@ -11,9 +11,9 @@ data class ConnectWrapper(
|
|||
}
|
||||
|
||||
fun match(
|
||||
node: Node,
|
||||
trackNodes: MutableList<Node> = mutableListOf(),
|
||||
): List<Node>? {
|
||||
node: NodeExt,
|
||||
trackNodes: MutableList<NodeExt> = mutableListOf(),
|
||||
): List<NodeExt>? {
|
||||
connectSegment.traversal(node).forEach {
|
||||
if (it == null) return@forEach
|
||||
val r = to.match(it, trackNodes)
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
package li.songe.selector_core.data
|
||||
|
||||
import li.songe.selector_core.Node
|
||||
import li.songe.selector_core.NodeExt
|
||||
|
||||
/**
|
||||
* an+b
|
||||
|
@ -9,6 +9,7 @@ data class PolynomialExpression(val a: Int = 0, val b: Int = 1) {
|
|||
|
||||
override fun toString(): String {
|
||||
if (a == 0 && b == 0) return "0"
|
||||
if (a == 1 && b == 1) return "(n+1)"
|
||||
if (b == 0) {
|
||||
if (a == 1) return "n"
|
||||
return if (a > 0) {
|
||||
|
@ -34,7 +35,7 @@ data class PolynomialExpression(val a: Int = 0, val b: Int = 1) {
|
|||
*/
|
||||
val b1 = b - 1
|
||||
|
||||
val traversal: (Sequence<Node?>) -> Sequence<Node?> =
|
||||
val traversal: (Sequence<NodeExt?>) -> Sequence<NodeExt?> =
|
||||
if (a <= 0 && b <= 0) ({ emptySequence() })
|
||||
else ({ sequence ->
|
||||
sequence.filterIndexed { x, _ -> (x - b1) % a == 0 && (x - b1) / a > 0 }
|
||||
|
|
|
@ -1,21 +1,21 @@
|
|||
package li.songe.selector_core.data
|
||||
|
||||
import li.songe.selector_core.Node
|
||||
import li.songe.selector_core.NodeExt
|
||||
|
||||
data class PropertySegment(
|
||||
/**
|
||||
* 此属性选择器是否被 @ 标记
|
||||
*/
|
||||
val match: Boolean,
|
||||
val tracked: Boolean,
|
||||
val name: String,
|
||||
val expressions: List<BinaryExpression>,
|
||||
) {
|
||||
override fun toString(): String {
|
||||
val matchTag = if (match) "@" else ""
|
||||
val matchTag = if (tracked) "@" else ""
|
||||
return matchTag + name + expressions.joinToString("")
|
||||
}
|
||||
|
||||
val matchName: (node: Node) -> Boolean =
|
||||
val matchName: (node: NodeExt) -> Boolean =
|
||||
if (name.isBlank() || name == "*")
|
||||
({ true })
|
||||
else ({ node ->
|
||||
|
@ -24,11 +24,11 @@ data class PropertySegment(
|
|||
(str.endsWith(name) && str[str.length - name.length - 1] == '.')
|
||||
})
|
||||
|
||||
val matchExpressions: (node: Node) -> Boolean = { node ->
|
||||
val matchExpressions: (node: NodeExt) -> Boolean = { node ->
|
||||
expressions.all { ex -> ex.match(node) }
|
||||
}
|
||||
|
||||
fun match(node: Node): Boolean {
|
||||
fun match(node: NodeExt): Boolean {
|
||||
return matchName(node) && matchExpressions(node)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
package li.songe.selector_core.data
|
||||
|
||||
import li.songe.selector_core.Node
|
||||
import li.songe.selector_core.NodeExt
|
||||
|
||||
data class PropertyWrapper(
|
||||
val propertySegment: PropertySegment,
|
||||
|
@ -15,13 +15,13 @@ data class PropertyWrapper(
|
|||
}
|
||||
|
||||
fun match(
|
||||
node: Node,
|
||||
trackNodes: MutableList<Node> = mutableListOf(),
|
||||
): List<Node>? {
|
||||
node: NodeExt,
|
||||
trackNodes: MutableList<NodeExt> = mutableListOf(),
|
||||
): List<NodeExt>? {
|
||||
if (!propertySegment.match(node)) {
|
||||
return null
|
||||
}
|
||||
if (propertySegment.match || trackNodes.isEmpty()) {
|
||||
if (propertySegment.tracked || trackNodes.isEmpty()) {
|
||||
trackNodes.add(node)
|
||||
}
|
||||
if (to == null) {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
package li.songe.selector_core.parser
|
||||
|
||||
data class SyntaxError(val expectedValue: String, val position: Int, val source: String) :
|
||||
data class ExtSyntaxError(val expectedValue: String, val position: Int, val source: String) :
|
||||
Exception(
|
||||
"expected $expectedValue in selector at position $position, but got ${
|
||||
source.getOrNull(
|
||||
|
@ -11,12 +11,12 @@ data class SyntaxError(val expectedValue: String, val position: Int, val source:
|
|||
companion object {
|
||||
fun assert(source: String, offset: Int, value: String = "", expectedValue: String? = null) {
|
||||
if (offset >= source.length || (value.isNotEmpty() && !value.contains(source[offset]))) {
|
||||
throw SyntaxError(expectedValue ?: value, offset, source)
|
||||
throw ExtSyntaxError(expectedValue ?: value, offset, source)
|
||||
}
|
||||
}
|
||||
|
||||
fun throwError(source: String, offset: Int, expectedValue: String = ""): Nothing {
|
||||
throw SyntaxError(expectedValue, offset, source)
|
||||
throw ExtSyntaxError(expectedValue, offset, source)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -21,7 +21,7 @@ internal object ParserSet {
|
|||
ParserResult(data, i - offset)
|
||||
}
|
||||
val whiteCharStrictParser = Parser("\u0020\t\r\n") { source, offset, prefix ->
|
||||
SyntaxError.assert(source, offset, prefix, "whitespace")
|
||||
ExtSyntaxError.assert(source, offset, prefix, "whitespace")
|
||||
whiteCharParser(source, offset)
|
||||
}
|
||||
val nameParser =
|
||||
|
@ -31,7 +31,7 @@ internal object ParserSet {
|
|||
if (s0 != null && !prefix.contains(s0)) {
|
||||
return@Parser ParserResult("")
|
||||
}
|
||||
SyntaxError.assert(source, i, prefix, "*0-9a-zA-Z_")
|
||||
ExtSyntaxError.assert(source, i, prefix, "*0-9a-zA-Z_")
|
||||
var data = source[i].toString()
|
||||
i++
|
||||
if (data == "*") { // 范匹配
|
||||
|
@ -41,7 +41,7 @@ internal object ParserSet {
|
|||
while (i < source.length) {
|
||||
// . 不能在开头和结尾
|
||||
if (data[i - offset - 1] == '.') {
|
||||
SyntaxError.assert(source, i, prefix, "[0-9a-zA-Z_]")
|
||||
ExtSyntaxError.assert(source, i, prefix, "[0-9a-zA-Z_]")
|
||||
}
|
||||
if (center.contains(source[i])) {
|
||||
data += source[i]
|
||||
|
@ -60,13 +60,13 @@ internal object ParserSet {
|
|||
subOperator.key,
|
||||
offset
|
||||
)
|
||||
} ?: SyntaxError.throwError(source, offset, "ConnectOperator")
|
||||
return@Parser ParserResult(operator, operator.key.length)
|
||||
} ?: ExtSyntaxError.throwError(source, offset, "ConnectOperator")
|
||||
ParserResult(operator, operator.key.length)
|
||||
}
|
||||
|
||||
val integerParser = Parser("1234567890") { source, offset, prefix ->
|
||||
var i = offset
|
||||
SyntaxError.assert(source, i, prefix, "number")
|
||||
ExtSyntaxError.assert(source, i, prefix, "number")
|
||||
var s = ""
|
||||
while (prefix.contains(source[i])) {
|
||||
s += source[i]
|
||||
|
@ -79,7 +79,7 @@ internal object ParserSet {
|
|||
// [+-][a][n[^b]]
|
||||
val monomialParser = Parser("+-1234567890n") { source, offset, prefix ->
|
||||
var i = offset
|
||||
SyntaxError.assert(source, i, prefix)
|
||||
ExtSyntaxError.assert(source, i, prefix)
|
||||
/**
|
||||
* one of 1, -1
|
||||
*/
|
||||
|
@ -98,7 +98,7 @@ internal object ParserSet {
|
|||
}
|
||||
i += whiteCharParser(source, i).length
|
||||
// [a][n[^b]]
|
||||
SyntaxError.assert(source, i, integerParser.prefix + "n")
|
||||
ExtSyntaxError.assert(source, i, integerParser.prefix + "n")
|
||||
val coefficient =
|
||||
if (integerParser.prefix.contains(source[i])) {
|
||||
val coefficientResult = integerParser(source, i)
|
||||
|
@ -126,23 +126,23 @@ internal object ParserSet {
|
|||
// ([+-][a][n[^b]] [+-][a][n[^b]])
|
||||
val expressionParser = Parser("(0123456789n") { source, offset, prefix ->
|
||||
var i = offset
|
||||
SyntaxError.assert(source, i, prefix)
|
||||
ExtSyntaxError.assert(source, i, prefix)
|
||||
val monomialResultList = mutableListOf<ParserResult<Pair<Int, Int>>>()
|
||||
when (source[i]) {
|
||||
'(' -> {
|
||||
i++
|
||||
i += whiteCharParser(source, i).length
|
||||
SyntaxError.assert(source, i, monomialParser.prefix)
|
||||
ExtSyntaxError.assert(source, i, monomialParser.prefix)
|
||||
while (source[i] != ')') {
|
||||
if (monomialResultList.size > 0) {
|
||||
SyntaxError.assert(source, i, "+-")
|
||||
ExtSyntaxError.assert(source, i, "+-")
|
||||
}
|
||||
val monomialResult = monomialParser(source, i)
|
||||
monomialResultList.add(monomialResult)
|
||||
i += monomialResult.length
|
||||
i += whiteCharParser(source, i).length
|
||||
if (i >= source.length) {
|
||||
SyntaxError.assert(source, i, ")")
|
||||
ExtSyntaxError.assert(source, i, ")")
|
||||
}
|
||||
}
|
||||
i++
|
||||
|
@ -161,7 +161,7 @@ internal object ParserSet {
|
|||
}
|
||||
map.mapKeys { power ->
|
||||
if (power.key > 1) {
|
||||
SyntaxError.throwError(source, offset, "power must be 0 or 1")
|
||||
ExtSyntaxError.throwError(source, offset, "power must be 0 or 1")
|
||||
}
|
||||
}
|
||||
ParserResult(PolynomialExpression(map[1] ?: 0, map[0] ?: 0), i - offset)
|
||||
|
@ -189,25 +189,25 @@ internal object ParserSet {
|
|||
Parser(CompareOperator.allSubClasses.joinToString("") { it.key }) { source, offset, _ ->
|
||||
val operator = CompareOperator.allSubClasses.find { SubOperator ->
|
||||
source.startsWith(SubOperator.key, offset)
|
||||
} ?: SyntaxError.throwError(source, offset, "CompareOperator")
|
||||
} ?: ExtSyntaxError.throwError(source, offset, "CompareOperator")
|
||||
ParserResult(operator, operator.key.length)
|
||||
}
|
||||
val stringParser = Parser("`") { source, offset, prefix ->
|
||||
var i = offset
|
||||
SyntaxError.assert(source, i, prefix)
|
||||
ExtSyntaxError.assert(source, i, prefix)
|
||||
i++
|
||||
var data = ""
|
||||
while (source[i] != '`') {
|
||||
if (i == source.length - 1) {
|
||||
SyntaxError.assert(source, i, "`")
|
||||
ExtSyntaxError.assert(source, i, "`")
|
||||
break
|
||||
}
|
||||
if (source[i] == '\\') {
|
||||
i++
|
||||
SyntaxError.assert(source, i)
|
||||
ExtSyntaxError.assert(source, i)
|
||||
if (source[i] == '`') {
|
||||
data += source[i]
|
||||
SyntaxError.assert(source, i + 1)
|
||||
ExtSyntaxError.assert(source, i + 1)
|
||||
} else {
|
||||
data += '\\' + source[i].toString()
|
||||
}
|
||||
|
@ -221,9 +221,9 @@ internal object ParserSet {
|
|||
}
|
||||
|
||||
val propertyParser =
|
||||
Parser((('0'..'9') + ('a'..'z') + ('A'..'Z')).joinToString("") + "_") { source, offset, prefix ->
|
||||
Parser("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_") { source, offset, prefix ->
|
||||
var i = offset
|
||||
SyntaxError.assert(source, i, prefix)
|
||||
ExtSyntaxError.assert(source, i, prefix)
|
||||
var data = source[i].toString()
|
||||
i++
|
||||
while (i < source.length) {
|
||||
|
@ -238,12 +238,12 @@ internal object ParserSet {
|
|||
|
||||
val valueParser = Parser("tfn`1234567890") { source, offset, prefix ->
|
||||
var i = offset
|
||||
SyntaxError.assert(source, i, prefix)
|
||||
ExtSyntaxError.assert(source, i, prefix)
|
||||
val value: Any? = when (source[i]) {
|
||||
't' -> {
|
||||
i++
|
||||
"rue".forEach { c ->
|
||||
SyntaxError.assert(source, i, c.toString())
|
||||
ExtSyntaxError.assert(source, i, c.toString())
|
||||
i++
|
||||
}
|
||||
true
|
||||
|
@ -252,7 +252,7 @@ internal object ParserSet {
|
|||
'f' -> {
|
||||
i++
|
||||
"alse".forEach { c ->
|
||||
SyntaxError.assert(source, i, c.toString())
|
||||
ExtSyntaxError.assert(source, i, c.toString())
|
||||
i++
|
||||
}
|
||||
false
|
||||
|
@ -261,7 +261,7 @@ internal object ParserSet {
|
|||
'n' -> {
|
||||
i++
|
||||
"ull".forEach { c ->
|
||||
SyntaxError.assert(source, i, c.toString())
|
||||
ExtSyntaxError.assert(source, i, c.toString())
|
||||
i++
|
||||
}
|
||||
null
|
||||
|
@ -280,7 +280,7 @@ internal object ParserSet {
|
|||
}
|
||||
|
||||
else -> {
|
||||
SyntaxError.throwError(source, i, prefix)
|
||||
ExtSyntaxError.throwError(source, i, prefix)
|
||||
}
|
||||
}
|
||||
ParserResult(value, i - offset)
|
||||
|
@ -288,7 +288,7 @@ internal object ParserSet {
|
|||
|
||||
val attrParser = Parser("[") { source, offset, prefix ->
|
||||
var i = offset
|
||||
SyntaxError.assert(source, i, prefix)
|
||||
ExtSyntaxError.assert(source, i, prefix)
|
||||
i++
|
||||
val parserResult = propertyParser(source, i)
|
||||
i += parserResult.length
|
||||
|
@ -296,7 +296,7 @@ internal object ParserSet {
|
|||
i += operatorResult.length
|
||||
val valueResult = valueParser(source, i)
|
||||
i += valueResult.length
|
||||
SyntaxError.assert(source, i, "]")
|
||||
ExtSyntaxError.assert(source, i, "]")
|
||||
i++
|
||||
ParserResult(
|
||||
BinaryExpression(
|
||||
|
@ -309,9 +309,9 @@ internal object ParserSet {
|
|||
|
||||
val selectorUnitParser = Parser { source, offset, _ ->
|
||||
var i = offset
|
||||
var match = false
|
||||
var tracked = false
|
||||
if (source.getOrNull(i) == '@') {
|
||||
match = true
|
||||
tracked = true
|
||||
i++
|
||||
}
|
||||
val nameResult = nameParser(source, i)
|
||||
|
@ -323,9 +323,9 @@ internal object ParserSet {
|
|||
attrList.add(attrResult.data)
|
||||
}
|
||||
if (nameResult.length == 0 && attrList.size == 0) {
|
||||
SyntaxError.throwError(source, i, "[")
|
||||
ExtSyntaxError.throwError(source, i, "[")
|
||||
}
|
||||
ParserResult(PropertySegment(match, nameResult.data, attrList), i - offset)
|
||||
ParserResult(PropertySegment(tracked, nameResult.data, attrList), i - offset)
|
||||
}
|
||||
|
||||
val connectSelectorParser = Parser { source, offset, _ ->
|
||||
|
@ -353,7 +353,7 @@ internal object ParserSet {
|
|||
|
||||
val endParser = Parser { source, offset, _ ->
|
||||
if (offset != source.length) {
|
||||
SyntaxError.throwError(source, offset, "end")
|
||||
ExtSyntaxError.throwError(source, offset, "end")
|
||||
}
|
||||
ParserResult(Unit, 0)
|
||||
}
|
||||
|
|
|
@ -2,7 +2,6 @@ rootProject.name = "gkd"
|
|||
include(":app")
|
||||
include(":router")
|
||||
include(":selector_core")
|
||||
include(":selector_android")
|
||||
|
||||
pluginManagement {
|
||||
repositories {
|
||||
|
|
Loading…
Reference in New Issue
Block a user