feat: remove selector_android

This commit is contained in:
lisonge 2023-05-30 20:33:49 +08:00
parent 0038d5eb98
commit b9cef851d7
37 changed files with 408 additions and 404 deletions

View File

@ -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)

View File

@ -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"

View File

@ -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
}
}

View File

@ -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(),

View File

@ -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,

View File

@ -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() }
}
}

View File

@ -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
}
}

View File

@ -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

View File

@ -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"
}
}

View File

@ -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

View File

@ -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"

View File

@ -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

View 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),
)
}
}
}

View 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
}
}

View File

@ -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 = ""
)

View File

@ -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)
)
}
}
}

View File

@ -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
}
}

View File

@ -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
}

View File

@ -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

View File

@ -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

View File

@ -18,6 +18,7 @@ object Singleton {
Json {
isLenient = true
ignoreUnknownKeys = true
encodeDefaults = true
}
}
val json5: Jankson by lazy { Jankson.builder().build() }

View File

@ -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"

View File

@ -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

View File

@ -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%)"
}
}
}
}

View File

@ -7,6 +7,3 @@ java {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
dependencies {
}

View File

@ -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 {
}

View File

@ -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
}

View File

@ -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("`", "\\`")}`"

View File

@ -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)
}
}

View File

@ -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)) })
}
}

View File

@ -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)

View File

@ -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 }

View File

@ -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)
}

View File

@ -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) {

View File

@ -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)
}
}
}

View File

@ -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)
}

View File

@ -2,7 +2,6 @@ rootProject.name = "gkd"
include(":app")
include(":router")
include(":selector_core")
include(":selector_android")
pluginManagement {
repositories {