feat: build ok

This commit is contained in:
lisonge 2022-12-09 22:39:24 +08:00
parent 6c72cbab9f
commit acdd10ddb5
66 changed files with 991 additions and 771 deletions

View File

@ -4,10 +4,10 @@ plugins {
id("kotlin-parcelize")
id("kotlin-kapt")
id("org.jetbrains.kotlin.plugin.serialization")
id("com.google.devtools.ksp") version "1.7.10-1.0.6"
id("com.google.devtools.ksp") version "1.7.20-1.0.7"
}
val composeVersion = "1.3.0-beta01"
val composeVersion = "1.3.0"
android {
compileSdk = 33
buildToolsVersion = "33.0.0"
@ -87,7 +87,9 @@ android {
compose = true
}
composeOptions {
kotlinCompilerExtensionVersion = composeVersion
// compose 编译器的版本, 需要注意它与 compose 的版本不一致
// https://mvnrepository.com/artifact/androidx.compose.compiler/compiler
kotlinCompilerExtensionVersion = "1.3.2"
}
packagingOptions {
resources {
@ -103,7 +105,7 @@ dependencies {
// normal
implementation("androidx.appcompat:appcompat:1.5.1")
implementation("com.google.android.material:material:1.6.1")
implementation("com.google.android.material:material:1.7.0")
// ktx
implementation("androidx.core:core-ktx:1.9.0")
@ -115,7 +117,7 @@ dependencies {
implementation("androidx.compose.ui:ui-tooling-preview:$composeVersion")
debugImplementation("androidx.compose.ui:ui-tooling:$composeVersion")
androidTestImplementation("androidx.compose.ui:ui-test-junit4:$composeVersion")
implementation("androidx.activity:activity-compose:1.6.0")
implementation("androidx.activity:activity-compose:1.6.1")
// test
testImplementation("junit:junit:4.13.2")
@ -151,10 +153,6 @@ dependencies {
// https://github.com/Tencent/MMKV/blob/master/README_CN.md
implementation("com.tencent:mmkv:1.2.13")
// https://juejin.cn/post/6969841959082917901
implementation("com.squareup.moshi:moshi-kotlin:1.13.0")
kapt("com.squareup.moshi:moshi-kotlin-codegen:1.13.0")
// ktor
implementation("io.ktor:ktor-server-core:2.1.0")
implementation("io.ktor:ktor-server-netty:2.1.0")
@ -186,6 +184,12 @@ dependencies {
ksp(project(":room_processor"))
implementation(project(mapOf("path" to ":node_selector")))
implementation(project(mapOf("path" to ":selector")))
// https://github.com/falkreon/Jankson
implementation("blue.endless:jankson:1.2.1")
// https://github.com/Kotlin/kotlinx.collections.immutable
implementation("org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.5")
}

View File

@ -2,11 +2,11 @@
"formatVersion": 1,
"database": {
"version": 1,
"identityHash": "f3082a1bb735b6f7f163e448585542ce",
"identityHash": "8ca34ac65f8956691667eb1cbca0bfb3",
"entities": [
{
"tableName": "subs_item",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `ctime` INTEGER NOT NULL, `mtime` INTEGER NOT NULL, `url` TEXT NOT NULL, `file_path` TEXT NOT NULL, `comment` TEXT NOT NULL, `description` TEXT NOT NULL, `enable` INTEGER NOT NULL)",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `ctime` INTEGER NOT NULL, `mtime` INTEGER NOT NULL, `update_url` TEXT NOT NULL, `file_path` TEXT NOT NULL, `comment` TEXT NOT NULL, `enable` INTEGER NOT NULL)",
"fields": [
{
"fieldPath": "id",
@ -27,8 +27,8 @@
"notNull": true
},
{
"fieldPath": "url",
"columnName": "url",
"fieldPath": "updateUrl",
"columnName": "update_url",
"affinity": "TEXT",
"notNull": true
},
@ -44,12 +44,6 @@
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "description",
"columnName": "description",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "enable",
"columnName": "enable",
@ -65,20 +59,20 @@
},
"indices": [
{
"name": "index_subs_item_url",
"name": "index_subs_item_update_url",
"unique": true,
"columnNames": [
"url"
"update_url"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_subs_item_url` ON `${TABLE_NAME}` (`url`)"
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_subs_item_update_url` ON `${TABLE_NAME}` (`update_url`)"
}
],
"foreignKeys": []
},
{
"tableName": "subs_config",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `ctime` INTEGER NOT NULL, `mtime` INTEGER NOT NULL, `type` INTEGER NOT NULL, `enable` INTEGER NOT NULL, `subs_item_id` INTEGER NOT NULL, `package_name` TEXT NOT NULL, `group_key` INTEGER NOT NULL, `rule_key` INTEGER NOT NULL)",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `ctime` INTEGER NOT NULL, `mtime` INTEGER NOT NULL, `type` INTEGER NOT NULL, `enable` INTEGER NOT NULL, `subs_item_id` INTEGER NOT NULL, `app_id` TEXT NOT NULL, `group_key` INTEGER NOT NULL, `rule_key` INTEGER NOT NULL)",
"fields": [
{
"fieldPath": "id",
@ -117,8 +111,8 @@
"notNull": true
},
{
"fieldPath": "packageName",
"columnName": "package_name",
"fieldPath": "appId",
"columnName": "app_id",
"affinity": "TEXT",
"notNull": true
},
@ -148,7 +142,7 @@
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'f3082a1bb735b6f7f163e448585542ce')"
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '8ca34ac65f8956691667eb1cbca0bfb3')"
]
}
}

View File

@ -51,7 +51,7 @@
</activity>
<service
android:name="li.songe.gkd.service.GkdAccessService"
android:name="li.songe.gkd.accessibility.GkdAccessService"
android:exported="false"
android:label="@string/accessibility_service_label"
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
@ -67,9 +67,9 @@
android:exported="false"
android:foregroundServiceType="mediaProjection" />
<service
android:name=".service.HttpServerService"
android:name=".server.HttpServerService"
android:exported="false" />
<service android:name=".service.TestService" android:exported="false" />
<service android:name=".accessibility.KeepAliveService" android:exported="false" />
<!-- This provider is required by Shizuku, remove this if your app only supports Sui -->
<provider

View File

@ -1,10 +0,0 @@
// IUserService.aidl
package li.songe.gkd;
interface IUserService {
void destroy() = 16777114; // Destroy method defined by Shizuku server
void exit() = 1; // Exit method defined by user
String doSomething() = 2;
}

View File

@ -9,6 +9,7 @@ import androidx.lifecycle.lifecycleScope
import com.blankj.utilcode.util.LogUtils
import com.dylanc.activityresult.launcher.StartActivityLauncher
import com.journeyapps.barcodescanner.BarcodeEncoder
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import li.songe.gkd.router.RouterHost

View File

@ -1,32 +1,21 @@
package li.songe.gkd.service
package li.songe.gkd.accessibility
import android.accessibilityservice.AccessibilityService
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Intent
import android.view.accessibility.AccessibilityEvent
import android.view.accessibility.AccessibilityNodeInfo
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import com.blankj.utilcode.util.LogUtils
import com.blankj.utilcode.util.ScreenUtils
import com.blankj.utilcode.util.ServiceUtils
import kotlinx.coroutines.*
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.time.withTimeoutOrNull
import li.songe.gkd.MainActivity
import li.songe.gkd.R
import li.songe.gkd.data.RuleMap
import li.songe.gkd.data.Subscription
import li.songe.gkd.data.api.NodeData
import li.songe.gkd.server.api.NodeData
import li.songe.gkd.shizuku.Shell
import li.songe.gkd.shizuku.ShizukuShell
import li.songe.gkd.store.Storage
import li.songe.node_selector.GkdSelector
import kotlin.concurrent.thread
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
import li.songe.selector.GkdSelector
/**
@ -324,7 +313,7 @@ class GkdAccessService : AccessibilityService() {
private fun createNotificationChannel(cls: String = "未知") {
TestService.start(this)
KeepAliveService.start(this)
//
// val channelId = "gkd_service"
// val intent = Intent(this, MainActivity::class.java).apply {

View File

@ -1,26 +1,25 @@
package li.songe.gkd.service
package li.songe.gkd.accessibility
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service
import android.content.Context
import android.content.Intent
import android.content.pm.ServiceInfo
import android.os.Build
import android.os.IBinder
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import com.blankj.utilcode.util.LogUtils
import li.songe.gkd.App
import li.songe.gkd.MainActivity
import li.songe.gkd.R
import li.songe.gkd.service.CompositionService
import kotlin.concurrent.thread
class TestService : CompositionService() {
class KeepAliveService : CompositionService() {
companion object {
fun start(context: Context = App.context) {
context.startForegroundService(Intent(context, TestService::class.java))
context.startForegroundService(Intent(context, KeepAliveService::class.java))
}
}
override fun onCreate() {

View File

@ -1,23 +1,23 @@
package li.songe.gkd.data
import li.songe.node_selector.GkdSelector
import li.songe.selector.GkdSelector
class RuleMap {
private val data = mutableMapOf<String, MutableMap<String, MutableList<GkdSelector>>>()
fun load(subscription: Subscription) {
subscription.appList.forEach {
if (!data.containsKey(it.packageName)) {
data[it.packageName] = mutableMapOf()
if (!data.containsKey(it.id)) {
data[it.id] = mutableMapOf()
}
val m2 = data[it.packageName]!!
val m2 = data[it.id]!!
it.groupList.forEach { group ->
group.ruleList.forEach loop@{ rule ->
val name = (rule.className ?: group.className)
val name = (rule.activityId ?: group.activityId)
if (!m2.containsKey(name)) {
m2[name] = mutableListOf()
}
val list = m2[name]!!
list.add(GkdSelector.gkdSelectorParser(rule.selector))
list.add(GkdSelector.gkdSelectorParser(rule.match))
}
}
}

View File

@ -8,84 +8,55 @@ import li.songe.gkd.util.Singleton
@Serializable
//@JsonClass(generateAdapter = true)
data class Subscription(
@SerialName("appList")
//@Json(name = "appList")
val appList: List<App>,
@SerialName("author")
//@Json(name = "author")
val author: String?=null,
@SerialName("description")
//@Json(name = "description")
val description: String = "",
@SerialName("version")
//@Json(name = "version")
val version: Int,
@SerialName("url")
//@Json(name = "url")
var url: String = ""
@SerialName("name") val name: String? = null,
@SerialName("version") val version: Int,
@SerialName("author") val author: String? = null,
@SerialName("updateUrl") val updateUrl: String? = null,
@SerialName("appList") val appList: List<App>,
) {
companion object {
fun parse(source: String) = Singleton.json.decodeFromString<Subscription>(source)
fun parse5(source: String): Subscription {
return Singleton.json.decodeFromString(
Singleton.json5.load(source).toJson()
)
}
fun stringify(source: Subscription) = Singleton.json.encodeToString(source)
}
@Serializable
//@JsonClass(generateAdapter = true)
data class App(
@SerialName("groupList")
//@Json(name = "groupList")
val groupList: List<Group>,
@SerialName("packageName")
//@Json(name = "packageName")
val packageName: String
@SerialName("id") val id: String,
@SerialName("groupList") val groupList: List<Group>,
)
@Serializable
//@JsonClass(generateAdapter = true)
data class Group(
@SerialName("className")
//@Json(name = "className")
val className: String,
@SerialName("description")
//@Json(name = "description")
val description: String?=null,
@SerialName("key")
//@Json(name = "key")
val key: Int?=null,
@SerialName("ruleList")
//@Json(name = "ruleList")
val ruleList: List<Rule>
@SerialName("key") val key: Int? = null,
@SerialName("name") val name: String? = null,
@SerialName("activityId") val activityId: String,
@SerialName("cd") val cd: Int? = null,
@SerialName("ruleList") val ruleList: List<Rule>,
)
@Serializable
//@JsonClass(generateAdapter = true)
data class Rule(
@SerialName("className")
//@Json(name = "className")
val className: String?=null,
@SerialName("key") val key: Int? = null,
@SerialName("name") val name: String? = null,
@SerialName("activityId") val activityId: String? = null,
@SerialName("cd") val cd: Int? = null,
@SerialName("prompt") val prompt: String? = null,
@SerialName("match") val match: String,
@SerialName("action") val action: Action? = null,
@SerialName("ordered") val ordered: Boolean = false,
)
@SerialName("description")
//@Json(name = "description")
val description: String?=null,
@SerialName("selector")
//@Json(name = "selector")
val selector: String,
@SerialName("prompt")
//@Json(name = "prompt")
val prompt: String?=null
@Serializable
data class Action(
@SerialName("type") val type: String,
@SerialName("target") val target: String,
@SerialName("position") val position: String? = null,
)
}

View File

@ -1,12 +1,12 @@
package li.songe.gkd.db
import androidx.room.AutoMigration
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import com.blankj.utilcode.util.PathUtils
import li.songe.gkd.App
import li.songe.gkd.db.table.*
import li.songe.gkd.db.table.SubsConfig
import li.songe.gkd.db.table.SubsItem
import java.io.File
@Database(
@ -20,9 +20,6 @@ import java.io.File
)
abstract class AppDatabase : RoomDatabase() {
abstract fun subsItemRoomDao(): SubsItem.RoomDao
// abstract fun subsAppItemRoomDao(): SubsAppItem.RoomDao
// abstract fun subsGroupItemRoomDao(): SubsGroupItem.RoomDao
// abstract fun subsRuleItemRoomDao(): SubsRuleItem.RoomDao
abstract fun subsConfigRoomDao(): SubsConfig.RoomDao
companion object {

View File

@ -27,10 +27,10 @@ data class SubsConfig(
@ColumnInfo(name = "type") var type: Int = 0,
@ColumnInfo(name = "enable") var enable: Boolean = true,
@ColumnInfo(name = "subs_item_id") var subsItemId: Long = 0,
@ColumnInfo(name = "package_name") var packageName: String = "",
@ColumnInfo(name = "group_key") var groupKey: Int = 0,
@ColumnInfo(name = "rule_key") var ruleKey: Int = 0,
@ColumnInfo(name = "subs_item_id") var subsItemId: Long = -1,
@ColumnInfo(name = "app_id") var appId: String = "",
@ColumnInfo(name = "group_key") var groupKey: Int = -1,
@ColumnInfo(name = "rule_key") var ruleKey: Int = -1,
) : BaseTable, Parcelable {
companion object {

View File

@ -8,7 +8,7 @@ import li.songe.gkd.db.BaseTable
@Entity(
tableName = "subs_item",
indices = [Index(value = ["url"], unique = true)]
indices = [Index(value = ["update_url"], unique = true)]
)
@Parcelize
data class SubsItem(
@ -18,23 +18,22 @@ data class SubsItem(
@PrimaryKey(autoGenerate = true) @ColumnInfo(name = "id") override var id: Long = 0,
@ColumnInfo(name = "ctime") override var ctime: Long = System.currentTimeMillis(),
@ColumnInfo(name = "mtime") override var mtime: Long = System.currentTimeMillis(),
/**
* 订阅文件下载地址,也是更新链接
*/
@ColumnInfo(name = "url") var url: String,
/**
* 订阅文件下载后存放的路径
*/
@ColumnInfo(name = "file_path") var filePath: String,
@ColumnInfo(name = "enable") var enable: Boolean = true,
/**
* 用户自定义备注
*/
@ColumnInfo(name = "comment") var comment: String = "",
/**
* 来自订阅文件的描述,不应该由用户write
* 订阅文件下载地址,也是更新链接
*/
@ColumnInfo(name = "description") var description: String = "",
@ColumnInfo(name = "enable") var enable: Boolean = true,
@ColumnInfo(name = "update_url") var updateUrl: String,
/**
* 订阅文件下载后存放的路径
*/
@ColumnInfo(name = "file_path") var filePath: String,
) : Parcelable, BaseTable {
@Dao

View File

@ -15,7 +15,6 @@ import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.platform.LocalContext
import com.blankj.utilcode.util.LogUtils
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import li.songe.gkd.router.Router.Companion.LocalRouter

View File

@ -1,4 +1,4 @@
package li.songe.gkd.service
package li.songe.gkd.server
import android.app.Service
import android.content.Context
@ -23,7 +23,9 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import li.songe.gkd.App
import li.songe.gkd.data.api.WindowData
import li.songe.gkd.accessibility.GkdAccessService
import li.songe.gkd.server.api.WindowData
import li.songe.gkd.service.ScreenshotService
import li.songe.gkd.store.Storage
import java.net.NetworkInterface

View File

@ -1,8 +1,10 @@
package li.songe.gkd.service
package li.songe.gkd.server
import kotlinx.serialization.Serializable
@Serializable
data class RpcError(
val message: String = "unknown error",
val code: Int = 0,
val __error: Boolean = true
)

View File

@ -1,33 +1,20 @@
package li.songe.gkd.data.api
package li.songe.gkd.server.api
import android.graphics.Rect
import android.view.accessibility.AccessibilityNodeInfo
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import kotlinx.serialization.Serializable
@Serializable
@JsonClass(generateAdapter = true)
data class AttrData(
@Json(name = "id")
val id: String?,
@Json(name = "className")
val className: String?,
@Json(name = "childCount")
val childCount: Int,
@Json(name = "text")
val text: String?,
@Json(name = "isClickable")
val isClickable: Boolean,
@Json(name = "contentDescription")
val contentDescription: String?,
@Json(name = "left")
val left: Int,
@Json(name = "top")
val top: Int,
@Json(name = "right")
val right: Int,
@Json(name = "bottom")
val bottom: Int,
) {
companion object {

View File

@ -1,9 +1,7 @@
package li.songe.gkd.data.api
package li.songe.gkd.server.api
import android.view.accessibility.AccessibilityNodeInfo
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import li.songe.node_selector.forEachIndexed
import li.songe.selector.forEachIndexed
import java.util.*
import kotlinx.serialization.Serializable
@ -13,13 +11,9 @@ import kotlinx.serialization.Serializable
*/
@Serializable
@JsonClass(generateAdapter = true)
data class NodeData(
@Json(name = "id")
val id: Int,
@Json(name = "pid")
val pid: Int,
@Json(name = "attr")
val attrData: AttrData
) {
companion object {

View File

@ -1,25 +1,17 @@
package li.songe.gkd.data.api
package li.songe.gkd.server.api
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import li.songe.gkd.service.GkdAccessService
import li.songe.gkd.accessibility.GkdAccessService
import kotlinx.serialization.Serializable
import java.util.*
@Serializable
@JsonClass(generateAdapter = true)
data class WindowData(
@Json(name = "packageName")
val packageName: String?=null,
@Json(name = "activityName")
val activityName: String?=null,
@Json(name = "nodeDataList")
val nodeDataList: List<NodeData>?=null,
) {
companion object {
val singleton:WindowData
val singleton: WindowData
get() = WindowData(
packageName = GkdAccessService.currentPackageName(),
activityName = GkdAccessService.currentActivityName(),
@ -32,10 +24,7 @@ data class WindowData(
activityName = GkdAccessService.currentActivityName(),
nodeDataList = NodeData.info2nodeList(GkdAccessService.rootInActiveWindow())
)
val moshi: Moshi = Moshi.Builder()
.addLast(KotlinJsonAdapterFactory())
.build()
return moshi.adapter(WindowData::class.java).toJson(window)
return ""
}
}
}

View File

@ -1,15 +0,0 @@
package li.songe.gkd.service
import li.songe.gkd.IUserService
class ShizukuService: IUserService.Stub() {
override fun destroy() {
}
override fun exit() {
}
override fun doSomething(): String {
return ""
}
}

View File

@ -0,0 +1,12 @@
package li.songe.gkd.subs
import kotlinx.serialization.decodeFromString
import li.songe.gkd.subs.data.GkdSubs
import li.songe.gkd.util.Singleton.json
import li.songe.gkd.util.Singleton.json5
object SubsParser : (String) -> Unit {
override fun invoke(p0: String) {
val gkdSubs: GkdSubs = json.decodeFromString(json5.load(p0).toJson(false, false))
}
}

View File

@ -0,0 +1,14 @@
package li.songe.gkd.subs.data
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class GkdAction(
@SerialName("type")
val type: String,
@SerialName("target")
val target: String,
@SerialName("position")
val position: String?
)

View File

@ -0,0 +1,10 @@
package li.songe.gkd.subs.data
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class GkdApp(
@SerialName("id") val id: String,
@SerialName("groupList") val groupList: List<GkdGroup>
)

View File

@ -0,0 +1,18 @@
package li.songe.gkd.subs.data
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class GkdGroup(
@SerialName("key")
val key: Int,
@SerialName("ruleList")
val ruleList: List<GkdRule>,
@SerialName("desc")
val desc: String? = null,
@SerialName("activityId")
val activityId: String? = null,
@SerialName("cd")
val cd: Int? = null
)

View File

@ -0,0 +1,18 @@
package li.songe.gkd.subs.data
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class GkdRule(
@SerialName("key")
val key: Int,
@SerialName("match")
val match: String,
@SerialName("desc")
val desc: String? = null,
@SerialName("cd")
val cd: Int? = null,
@SerialName("activityId")
val activityId: String? = null,
)

View File

@ -0,0 +1,18 @@
package li.songe.gkd.subs.data
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class GkdSubs(
@SerialName("version")
val version: Int,
@SerialName("desc")
val desc: String? = null,
@SerialName("author")
val author: String? = null,
@SerialName("updateUrl")
val updateUrl: String? = null,
@SerialName("appList")
val appList: List<GkdApp>
)

View File

@ -71,7 +71,7 @@ object AppItemPage : Page<AppItemPage.Params, Unit> {
delay(400)
val config = params.subsConfig
val mutableSet =
RoomX.select { (SubsConfig::type eq SubsConfig.GroupType) and (SubsConfig::subsItemId eq config.subsItemId) and (SubsConfig::packageName eq config.packageName) }
RoomX.select { (SubsConfig::type eq SubsConfig.GroupType) and (SubsConfig::subsItemId eq config.subsItemId) and (SubsConfig::appId eq config.appId) }
.toMutableSet()
val list = mutableListOf<SubsConfig?>()
params.subsApp.groupList.forEach { group ->
@ -80,7 +80,7 @@ object AppItemPage : Page<AppItemPage.Params, Unit> {
} else {
val item = mutableSet.find { s -> s.groupKey == group.key } ?: SubsConfig(
subsItemId = config.subsItemId,
packageName = config.packageName,
appId = config.appId,
groupKey = group.key,
type = SubsConfig.GroupType
)
@ -107,7 +107,7 @@ object AppItemPage : Page<AppItemPage.Params, Unit> {
)
Spacer(modifier = Modifier.width(10.dp))
Text(
text = params.subsApp.packageName,
text = params.subsApp.id,
maxLines = 1,
softWrap = false,
overflow = TextOverflow.Ellipsis,
@ -143,7 +143,7 @@ object AppItemPage : Page<AppItemPage.Params, Unit> {
verticalArrangement = Arrangement.SpaceBetween
) {
Text(
text = group.description ?: "-",
text = group.name ?: "-",
maxLines = 1,
softWrap = false,
overflow = TextOverflow.Ellipsis,
@ -151,10 +151,10 @@ object AppItemPage : Page<AppItemPage.Params, Unit> {
.fillMaxWidth()
)
Text(
text = if (group.className.startsWith(params.subsApp.packageName)) {
group.className.substring(params.subsApp.packageName.length)
text = if (group.activityId.startsWith(params.subsApp.id)) {
group.activityId.substring(params.subsApp.id.length)
} else {
group.className
group.activityId
},
maxLines = 1,
softWrap = false,

View File

@ -24,8 +24,8 @@ import kotlinx.coroutines.*
import li.songe.gkd.MainActivity
import li.songe.gkd.router.Page
import li.songe.gkd.router.Router
import li.songe.gkd.service.GkdAccessService
import li.songe.gkd.service.HttpServerService
import li.songe.gkd.accessibility.GkdAccessService
import li.songe.gkd.server.HttpServerService
import li.songe.gkd.service.ScreenshotService
import li.songe.gkd.ui.component.StatusBar
import li.songe.gkd.ui.component.TextSwitch

View File

@ -10,7 +10,6 @@ import androidx.compose.material.TextButton
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
@ -46,7 +45,7 @@ object GroupItemPage : Page<GroupItemPage.Params, Unit> {
.padding(10.dp, 0.dp)
)
Text(
text = params.group.className, modifier = Modifier
text = params.group.activityId, modifier = Modifier
.fillMaxWidth()
.padding(10.dp, 0.dp)
)
@ -71,7 +70,7 @@ object GroupItemPage : Page<GroupItemPage.Params, Unit> {
verticalArrangement = Arrangement.SpaceBetween
) {
Text(
text = ruleList[i].description ?: "-",
text = ruleList[i].name ?: "-",
maxLines = 1,
softWrap = false,
overflow = TextOverflow.Ellipsis,
@ -79,7 +78,7 @@ object GroupItemPage : Page<GroupItemPage.Params, Unit> {
.fillMaxWidth(),
)
Text(
text = ruleList[i].selector,
text = ruleList[i].match,
maxLines = 1,
softWrap = false,
overflow = TextOverflow.Ellipsis,
@ -107,11 +106,11 @@ object GroupItemPage : Page<GroupItemPage.Params, Unit> {
setShowData(null)
},
title = {
Text(text = showData.description ?: "-")
Text(text = showData.name ?: "-")
},
text = {
Text(
text = showData.selector,
text = showData.match,
fontSize = 16.sp
)
},

View File

@ -94,18 +94,20 @@ object SubsItemInsertPage : Page<Unit, SubsItem?> {
Button(onClick = {
scope.launch {
subReqStatus = Status.Progress()
if (RoomX.select { SubsItem::url eq url }.isNotEmpty()) {
if (RoomX.select { SubsItem::updateUrl eq url }.isNotEmpty()) {
subReqStatus = Status.Error("链接已经存在,不可重复添加")
return@launch
}
val sub = try {
var sub = try {
Singleton.client.get(url).body<Subscription>()
} catch (e: Exception) {
e.printStackTrace()
subReqStatus = Status.Error(e)
return@launch
}
sub.url = url
if (sub.updateUrl == null) {
sub = sub.copy(updateUrl = url)
}
File(
PathUtils.getExternalAppFilesPath()
.plus("/subscription/")
@ -126,9 +128,8 @@ object SubsItemInsertPage : Page<Unit, SubsItem?> {
val newSubsItem = SubsItem(
enable = enable,
comment = comment,
url = url,
updateUrl = url,
filePath = fp.absolutePath,
description = sub.description
)
RoomX.insert(newSubsItem)
subReqStatus = Status.Success(Unit)

View File

@ -32,7 +32,7 @@ object SubsItemUpdatePage : Page<SubsItem, SubsItem?> {
val scope = rememberCoroutineScope()
var comment by remember { mutableStateOf(params.comment) }
var url by remember { mutableStateOf(params.url) }
var url by remember { mutableStateOf(params.updateUrl) }
var enable by remember { mutableStateOf(params.enable) }
Column(modifier = Modifier.padding(10.dp, 0.dp)) {
@ -75,7 +75,7 @@ object SubsItemUpdatePage : Page<SubsItem, SubsItem?> {
val newSubsItem = params.copy(
enable = enable,
comment = comment,
url = url,
updateUrl = url,
mtime = System.currentTimeMillis()
)
RoomX.update(newSubsItem)

View File

@ -65,7 +65,7 @@ object SubsPage : Page<SubsItem, Unit> {
)!!,
subsConfig = SubsConfig(
subsItemId = it.toLong(),
packageName = "" + it
appId = "" + it
)
)
)
@ -126,16 +126,16 @@ object SubsPage : Page<SubsItem, Unit> {
val defaultName = "-"
val newSubsAppCardDataList = (subStatus as Status.Success).value.appList.map { app ->
mutableSet.firstOrNull { v ->
v.packageName == app.packageName
v.appId == app.id
}.apply {
if (this != null) {
mutableSet.remove(this)
}
} ?: SubsConfig(subsItemId = subsItem.id, packageName = app.packageName, type = SubsConfig.AppType)
} ?: SubsConfig(subsItemId = subsItem.id, appId = app.id, type = SubsConfig.AppType)
}.map { subsConfig ->
async(IO) {
val info: ApplicationInfo = try {
packageManager.getApplicationInfo(subsConfig.packageName, 0)
packageManager.getApplicationInfo(subsConfig.appId, 0)
} catch (e: Exception) {
return@async SubsAppCardData(
defaultName,
@ -183,7 +183,7 @@ object SubsPage : Page<SubsItem, Unit> {
modifier = textModifier
)
Text(
text = "描述: ${sub?.description}",
text = "描述: ${sub?.name}",
modifier = textModifier
)
}
@ -222,6 +222,7 @@ object SubsPage : Page<SubsItem, Unit> {
.wrapContentSize()
) {
SubsAppCard(loading = true, args = placeholderList[i])
Text(text = "")
}
}
}

View File

@ -60,7 +60,7 @@ fun SubsAppCard(
.placeholder(loading, highlight = PlaceholderHighlight.fade())
)
Text(
text = args.subsConfig.packageName, maxLines = 1,
text = args.subsConfig.appId, maxLines = 1,
softWrap = false,
overflow = TextOverflow.Ellipsis,
modifier = Modifier

View File

@ -0,0 +1,63 @@
package li.songe.gkd.ui.component
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import li.songe.gkd.R
import li.songe.gkd.subs.data.GkdApp
import li.songe.gkd.subs.data.GkdSubs
@Composable
fun SubsCard(gkdSubs: GkdSubs) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(4.dp)
) {
Column(modifier = Modifier.weight(1f)) {
Text(
text = gkdSubs.desc ?: "简单描述", maxLines = 1,
softWrap = false,
overflow = TextOverflow.Ellipsis
)
Text(text = gkdSubs.appList.size.toString())
}
Spacer(modifier = Modifier.width(5.dp))
// Box() {
// Text(text = "几秒前更新", fontSize = 5.sp)
Image(
painter = painterResource(R.drawable.ic_menu),
contentDescription = "",
modifier = Modifier
.clickable {
}
.size(20.dp)
)
// }
}
}
@Preview(backgroundColor = 0xFFFFFFFF, showBackground = true)
@Composable
fun PreviewSubsCard() {
Surface(modifier = Modifier.width(200.dp)) {
SubsCard(
GkdSubs(
version = 1,
appList = listOf(GkdApp(id = "com.tencent.mm", groupList = listOf()))
)
)
}
}

View File

@ -6,11 +6,9 @@ import androidx.compose.foundation.layout.*
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
@ -34,13 +32,13 @@ fun SubsItemCard(
) {
Column(modifier = Modifier.weight(1f)) {
Text(
text = if (data.comment.isNotEmpty()) "${data.comment}:${data.description}" else data.description,
text = data.comment,
maxLines = 1,
softWrap = false,
overflow = TextOverflow.Ellipsis
)
Text(
text = data.url,
text = data.updateUrl,
maxLines = 1,
softWrap = false,
overflow = TextOverflow.Ellipsis
@ -89,7 +87,7 @@ fun PreviewSubscriptionItemCard() {
SubsItemCard(
SubsItem(
filePath = "filepath",
url = "https://raw.githubusercontents.com/lisonge/gkd-subscription/main/src/ad-startup.gkd.json",
updateUrl = "https://raw.githubusercontents.com/lisonge/gkd-subscription/main/src/ad-startup.gkd.json",
comment = "备注注注注注注注注注注注注注注注注注注注注注注注注注注注注注注注注注注注注注注注注注注注注注注"
)
)

View File

@ -0,0 +1,34 @@
package li.songe.gkd.ui.component
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.width
import androidx.compose.material.LocalTextStyle
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@Composable
fun Text114514() {
CompositionLocalProvider(
LocalTextStyle provides TextStyle(fontSize = 80.sp)
) {
Row {
Text("ping")
Text("pong")
}
}
}
@Preview(backgroundColor = 0xFFFFFFFF, showBackground = true)
@Composable
fun Text114514Preview() {
Surface(modifier = Modifier.width(200.dp)) {
Text114514()
}
}

View File

@ -11,16 +11,6 @@ data class BottomNavItem(
)
val BottomNavItems = listOf(
BottomNavItem(
label = "统计",
icon = R.drawable.ic_chart_bar,
route = "statistics"
),
BottomNavItem(
label = "本地",
icon = R.drawable.ic_database_set,
route = "native"
),
BottomNavItem(
label = "订阅",
icon = R.drawable.ic_link,

View File

@ -1,13 +1,10 @@
package li.songe.gkd.ui.home
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.material.Scaffold
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.zIndex
import li.songe.gkd.router.Page
import li.songe.gkd.router.Router
@ -26,7 +23,7 @@ object HomePage : Page<Unit, Unit> {
params: Unit,
router: Router<Unit>
) -> Unit = @Composable { _, _ ->
var tabInt by remember { mutableStateOf(2) }
var tabInt by remember { mutableStateOf(0) }
Column(modifier = Modifier.fillMaxSize()) {
StatusBar()
Scaffold(
@ -39,7 +36,7 @@ object HomePage : Page<Unit, Unit> {
.alpha(if (tabInt == 0) 1f else 0f)
.zIndex(if (tabInt == 0) 1f else 0f)
.noRippleClickable { }) {
StatisticsPage()
SubscriptionManagePage()
}
Box(
modifier = Modifier
@ -47,22 +44,6 @@ object HomePage : Page<Unit, Unit> {
.alpha(if (tabInt == 1) 1f else 0f)
.zIndex(if (tabInt == 1) 1f else 0f)
.noRippleClickable { }) {
NativePage()
}
Box(
modifier = Modifier
.fillMaxSize()
.alpha(if (tabInt == 2) 1f else 0f)
.zIndex(if (tabInt == 2) 1f else 0f)
.noRippleClickable { }) {
SubscriptionManagePage()
}
Box(
modifier = Modifier
.fillMaxSize()
.alpha(if (tabInt == 3) 1f else 0f)
.zIndex(if (tabInt == 3) 1f else 0f)
.noRippleClickable { }) {
SettingsPage()
}
}

View File

@ -14,10 +14,8 @@ import com.blankj.utilcode.util.LogUtils
import li.songe.gkd.MainActivity
import li.songe.gkd.R
import li.songe.gkd.router.Router.Companion.LocalRouter
import li.songe.gkd.service.TestService
import li.songe.gkd.store.Storage
import li.songe.gkd.ui.DebugPage
import li.songe.gkd.ui.component.StatusBar
import li.songe.gkd.ui.component.TextSwitch
import li.songe.gkd.util.LocaleString.Companion.localeString

View File

@ -1,20 +1,17 @@
package li.songe.gkd.util
import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import blue.endless.jankson.Jankson
import io.ktor.client.*
import io.ktor.client.engine.cio.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.http.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.serialization.json.Json
import java.lang.reflect.InvocationHandler
import java.lang.reflect.Proxy
object Singleton {
val moshi: Moshi by lazy {
Moshi.Builder()
.addLast(KotlinJsonAdapterFactory())
.build()
}
val json by lazy {
Json {
prettyPrint = true
@ -22,6 +19,7 @@ object Singleton {
ignoreUnknownKeys = true
}
}
val json5: Jankson by lazy { Jankson.builder().build() }
val client by lazy {
HttpClient(CIO) {
install(ContentNegotiation) {
@ -29,4 +27,17 @@ object Singleton {
}
}
}
// inline fun <reified T : Any> produce(data: T, block: (data: T) -> Unit): T {
// val proxyData = Proxy.newProxyInstance(
// T::class.java.classLoader,
// arrayOf(),
// InvocationHandler { proxy, method, args ->
//
// }) as T
// block(proxyData)
// return proxyData
// }
}

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="32"
android:viewportHeight="32">
<path
android:pathData="M8,16m-2,0a2,2 0,1 1,4 0a2,2 0,1 1,-4 0"
android:fillColor="#000"/>
<path
android:pathData="M16,16m-2,0a2,2 0,1 1,4 0a2,2 0,1 1,-4 0"
android:fillColor="#000"/>
<path
android:pathData="M24,16m-2,0a2,2 0,1 1,4 0a2,2 0,1 1,-4 0"
android:fillColor="#000"/>
</vector>

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">gkd</string>
<string name="accessibility_service_label">搞快点</string>
<string name="accessibility_service_description">基于规则匹配的无障碍速点服务</string>
<string name="accessibility_service_label" translatable="false">搞快点</string>
<string name="accessibility_service_description" translatable="false">基于规则匹配的无障碍速点服务</string>
</resources>

View File

@ -18,9 +18,10 @@ buildscript {
}
dependencies {
classpath("com.android.tools.build:gradle:7.3.0")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.7.10")
classpath("org.jetbrains.kotlin:kotlin-serialization:1.7.10")
classpath("com.android.tools.build:gradle:7.3.1")
// 当前 android 项目 kotlin 的版本
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.7.20")
classpath("org.jetbrains.kotlin:kotlin-serialization:1.7.20")
classpath("dev.rikka.tools.refine:gradle-plugin:3.0.3")
}
}

View File

@ -1,85 +0,0 @@
package li.songe.node_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.node_selector.parser.Tools
import li.songe.node_selector.selector.Position
import li.songe.node_selector.wrapper.PropertySelectorWrapper
import java.util.concurrent.Executors
import java.util.concurrent.Future
data class GkdSelector(val wrapper: PropertySelectorWrapper, val position: Position?) {
override fun toString() = wrapper.toString() + (position ?: "").toString()
fun collect(nodeInfo: AccessibilityNodeInfo) = nodeInfo.traverseAll { wrapper.match(it) }
fun collectParallel(nodeInfo: AccessibilityNodeInfo): AccessibilityNodeInfo? {
var resultNodeInfo: AccessibilityNodeInfo? = null
nodeInfo.traverseAll { child ->
taskList.add(
executorService.submit<Unit> {
if (resultNodeInfo != null) {
return@submit
}
if (wrapper.match(child)) {
resultNodeInfo = child
}
}
)
resultNodeInfo != null
}
if (resultNodeInfo != null) {
return resultNodeInfo
}
taskList.forEach { task -> task.get() }
taskList.clear()
return resultNodeInfo
}
fun click(nodeInfo: AccessibilityNodeInfo, service: AccessibilityService) = when {
position != null -> {
val react = Rect()
nodeInfo.getBoundsInScreen(react)
val x = react.left + position.x.number.toFloat() / 100f * (react.right - react.left)
val y = react.top + position.x.number.toFloat() / 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)
position.toString()
}
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%)"
}
}
companion object {
val gkdSelectorParser = Tools.gkdSelectorParser
private val executorService by lazy {
Executors.newFixedThreadPool(
Runtime.getRuntime().availableProcessors(),
)
}
private val taskList = mutableListOf<Future<Unit>>()
}
}

View File

@ -1,38 +0,0 @@
package li.songe.node_selector.parser
class Parser<T>(
val prefix: String = "",
private val invokeTemp: (source: String, offset: Int, prefix: String) -> ParserResult<T>
) : (String, Int) -> ParserResult<T> {
companion object {
fun assert(source: String, i: Int) {
if (i >= source.length) {
throw ParserException(source, i)
}
}
fun assert(source: String, i: Int, expectValue: Char) {
assert(source, i, expectValue.toString())
}
fun assert(source: String, i: Int, expectValue: String) {
if (i >= source.length) {
throw ParserException(source, i, expectValue)
}
if (!expectValue.contains(source[i])) {
throw ParserException(source, i, expectValue)
}
}
fun throwError(source: String, i: Int, expectValue: String? = ""): Nothing {
throw ParserException(source, i, expectValue)
}
}
override fun invoke(source: String, offset: Int): ParserResult<T> {
return invokeTemp(source, offset, prefix)
}
}

View File

@ -1,40 +0,0 @@
package li.songe.node_selector.parser
class ParserException(private val source: String, val i: Int, val expectValue: String? = "") :
Exception() {
override val message: String
get() {
return if (expectValue != null) {
when {
expectValue.isEmpty() -> {
return "source: $source, index:${i}, expect:any, actual:${source.getOrNull(i) ?: "array bound"}"
}
expectValue.length == 1 -> {
return "source: $source, index:${i}, expect:${expectValue}, actual:${source.getOrNull(i) ?: "array bound"}"
}
else -> {
val list = expectValue.toList().map {
when (it) {
'\n' -> "\\n"
'\t' -> "\\t"
'\r' -> "\\r"
'\u0020' -> "\\u0020"
else -> it.toString()
}
}
return "source: $source, index:${i}, expect one of:${list.joinToString(",")}, actual:${
source.getOrNull(
i
) ?: "array bound"
}"
}
}
} else {
"source: $source, index:${i}, expect eof, actual:${
source.getOrNull(
i
) ?: "array bound"
}"
}
}
}

View File

@ -1,3 +0,0 @@
package li.songe.node_selector.parser
data class ParserResult<T>(val data: T, val length: Int=0)

View File

@ -1,14 +0,0 @@
package li.songe.node_selector.selector
data class Position(val x: NumberUnit, val y: NumberUnit) {
override fun toString() = "(${x}, ${y})"
data class NumberUnit(val number: Number, val unit: Unit) {
override fun toString() = number.toString() + unit.toString()
}
sealed class Unit(private val key: String) {
override fun toString() = key
object Percentage : Unit("%")
}
}

View File

@ -1,83 +0,0 @@
package li.songe.node_selector.selector
/**
* 属性选择器
*/
data class PropertySelector(
val name: String,
val expressionList: List<BinaryExpression>
) {
override fun toString() = "${name}${expressionList.joinToString("")}"
sealed class Operator(private val key: String) {
abstract fun compare(a: Any?, b: Any?): Boolean
abstract fun acceptValue(a: Any?): Boolean
object More : Operator(">") {
override fun compare(a: Any?, b: Any?) =
(a is Int && b is Int && a > b) || (a is Float && b is Int && a > b) || (a is Float && b is Float && a > b) || (a is Int && b is Float && a > b)
override fun acceptValue(a: Any?) = a is Int || a is Float
}
object Less : Operator("<") {
override fun compare(a: Any?, b: Any?) =
(a is Int && b is Int && a < b) || (a is Float && b is Int && a < b) || (a is Float && b is Float && a < b) || (a is Int && b is Float && a < b)
override fun acceptValue(a: Any?) = a is Int || a is Float
}
object Equal : Operator("=") {
override fun compare(a: Any?, b: Any?) = a == b
override fun acceptValue(a: Any?) =
a is Int? || a is String? || a is Boolean? || a is Float?
}
object NotEqual : Operator("!=") {
override fun compare(a: Any?, b: Any?) = a != b
override fun acceptValue(a: Any?) =
a is Int? || a is String? || a is Boolean? || a is Float?
}
object MoreEqual : Operator(">=") {
override fun compare(a: Any?, b: Any?) =
(a is Int && b is Int && a >= b) || (a is Float && b is Int && a >= b) || (a is Float && b is Float && a >= b) || (a is Int && b is Float && a >= b)
override fun acceptValue(a: Any?) = a is Int || a is Float
}
object LessEqual : Operator("<=") {
override fun compare(a: Any?, b: Any?) =
(a is Int && b is Int && a <= b) || (a is Float && b is Int && a <= b) || (a is Float && b is Float && a <= b) || (a is Int && b is Float && a <= b)
override fun acceptValue(a: Any?) = a is Int || a is Float
}
object Include : Operator("*=") {
override fun compare(a: Any?, b: Any?) = (a is String && b is String && a.contains(b))
override fun acceptValue(a: Any?) = a is String
}
object Start : Operator("^=") {
override fun compare(a: Any?, b: Any?) = (a is String && b is String && a.startsWith(b))
override fun acceptValue(a: Any?) = a is String
}
object End : Operator("$=") {
override fun compare(a: Any?, b: Any?) = (a is String && b is String && a.endsWith(b))
override fun acceptValue(a: Any?) = a is String
}
override fun toString() = key
}
data class BinaryExpression(val name: String, val operator: Operator, val value: Any?) {
fun compare(otherValue: Any?) = operator.compare(otherValue, value)
override fun toString() = "[${name}${operator}${
if (value is String) {
"`${value.replace("`", "\\`")}`"
} else {
value
}
}]"
}
}

View File

@ -1,70 +0,0 @@
package li.songe.node_selector.wrapper
import android.graphics.Rect
import android.view.accessibility.AccessibilityNodeInfo
import li.songe.node_selector.getDepth
import li.songe.node_selector.getIndex
import li.songe.node_selector.selector.PropertySelector
data class PropertySelectorWrapper(
private val propertySelector: PropertySelector,
val to: CombinatorSelectorWrapper? = null
) {
override fun toString(): String {
return (if (to != null) {
to.toString() + "\u0020"
} else {
""
}) + propertySelector.toString()
}
fun match(nodeInfo: AccessibilityNodeInfo): Boolean {
val className = nodeInfo.className.toString()
if (!((className.endsWith(propertySelector.name) && className[className.length - propertySelector.name.length - 1] == '.')
|| className == propertySelector.name
)
) {
return false
}
val index by lazy { nodeInfo.getIndex() }
val childCount by lazy { nodeInfo.childCount }
val depth by lazy { nodeInfo.getDepth() }
val id by lazy { nodeInfo.viewIdResourceName?.toString() }
val text by lazy { nodeInfo.text?.toString() }
// val text = nodeInfo.text?.toString() ?: ""
val hintText by lazy { nodeInfo.hintText?.toString() }
val contentDescription by lazy { nodeInfo.contentDescription?.toString() }
val isPassword by lazy { nodeInfo.isPassword }
// val rect = Rect()
// nodeInfo.getBoundsInScreen(rect)
val isChecked by lazy { nodeInfo.isChecked }
propertySelector.expressionList.forEach { expression ->
val nodeValue: Any? = when (expression.name) {
"index" -> index
"childCount" -> childCount
"depth" -> depth
"id" -> id
"text" -> text
"text.length" -> text?.length
"hintText" -> hintText
"hintText.length" -> hintText?.length
"contentDescription" -> contentDescription
"contentDescription.length" -> contentDescription?.length
"isPassword" -> isPassword
"isChecked" -> isChecked
else -> return false
}
if (!expression.compare(nodeValue)) {
return false
}
}
if (to == null) {
return true
}
return to.match(nodeInfo)
}
}

View File

@ -20,7 +20,7 @@ tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
dependencies{
//ksp依赖
implementation("com.google.devtools.ksp:symbol-processing-api:1.7.10-1.0.6")
implementation("com.google.devtools.ksp:symbol-processing-api:1.7.20-1.0.7")
//poet依赖
implementation("com.squareup:kotlinpoet:1.11.0")
implementation("com.squareup:kotlinpoet-ksp:1.11.0")

View File

@ -1,4 +1,4 @@
package li.songe.node_selector
package li.songe.selector
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="li.songe.node_selector">
package="li.songe.selector">
</manifest>

View File

@ -1,4 +1,4 @@
package li.songe.node_selector
package li.songe.selector
import android.view.accessibility.AccessibilityNodeInfo
import java.util.*

View File

@ -0,0 +1,51 @@
package li.songe.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.parser.Transform
import li.songe.selector.wrapper.PropertySelectorWrapper
data class GkdSelector(val wrapper: PropertySelectorWrapper) {
override fun toString() = wrapper.toString()
fun collect(nodeInfo: AccessibilityNodeInfo): AccessibilityNodeInfo? {
val trackList: MutableList<AccessibilityNodeInfo?> = mutableListOf()
nodeInfo.traverseAll {
val match = wrapper.match(it, trackList)
if (!match) {
trackList.clear()
}
return@traverseAll match
}
return trackList.findLast { it != null }
}
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%)"
}
}
companion object {
val gkdSelectorParser = Transform.gkdSelectorParser
}
}

View File

@ -0,0 +1,8 @@
package li.songe.selector.parser
open class Parser<T>(
val prefix: String = "",
private val invokeTemp: (source: String, offset: Int, prefix: String) -> ParserResult<T>
) : (String, Int) -> ParserResult<T> {
override fun invoke(source: String, offset: Int) = invokeTemp(source, offset, prefix)
}

View File

@ -0,0 +1,3 @@
package li.songe.selector.parser
data class ParserResult<T>(val data: T, val length: Int = 0)

View File

@ -0,0 +1,22 @@
package li.songe.selector.parser
data class SyntaxError(val expectedValue: String, val position: Int, val source: String) :
Exception(
"expected $expectedValue in selector at position $position, but got ${
source.getOrNull(
position
)
}"
) {
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)
}
}
fun throwError(source: String, offset: Int, expectedValue: String = ""):Nothing {
throw SyntaxError(expectedValue, offset, source)
}
}
}

View File

@ -1,13 +1,12 @@
package li.songe.node_selector.parser
package li.songe.selector.parser
import li.songe.node_selector.GkdSelector
import li.songe.node_selector.selector.CombinatorSelector
import li.songe.node_selector.selector.Position
import li.songe.node_selector.selector.PropertySelector
import li.songe.node_selector.wrapper.CombinatorSelectorWrapper
import li.songe.node_selector.wrapper.PropertySelectorWrapper
import li.songe.selector.GkdSelector
import li.songe.selector.selector.CombinatorSelector
import li.songe.selector.selector.PropertySelector
import li.songe.selector.wrapper.CombinatorSelectorWrapper
import li.songe.selector.wrapper.PropertySelectorWrapper
object Tools {
object Transform {
val whiteCharParser = Parser("\u0020\t\r\n") { source, offset, prefix ->
var i = offset
var data = ""
@ -19,27 +18,28 @@ object Tools {
}
val whiteCharStrictParser = Parser("\u0020\t\r\n") { source, offset, prefix ->
var i = offset
Parser.assert(source, i, prefix)
var data = source[i].toString()
i++
while (i < source.length && prefix.contains(source[i])) {
data += source[i]
i++
}
ParserResult(data, i - offset)
SyntaxError.assert(source, offset, prefix, "whitespace")
whiteCharParser(source, offset)
}
val nameParser =
Parser("1234567890qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM_") { source, offset, prefix ->
Parser("*1234567890qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM_") { source, offset, prefix ->
var i = offset
Parser.assert(source, i, prefix)
val s0 = source.getOrNull(i)
if (s0 != null && !prefix.contains(s0)) {
return@Parser ParserResult("", i - offset)
}
SyntaxError.assert(source, i, prefix, "*0-9a-zA-Z_")
var data = source[i].toString()
i++
if (data == "*") { // 范匹配
return@Parser ParserResult(data, i - offset)
}
val center = "1234567890qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM_."
while (i < source.length) {
// . 不能在开头和结尾
if (data[i - offset - 1] == '.') {
Parser.assert(source, i, prefix)
SyntaxError.assert(source, i, prefix, "[0-9a-zA-Z_]")
}
if (center.contains(source[i])) {
data += source[i]
@ -53,19 +53,19 @@ object Tools {
val combinatorOperatorParser = Parser("+-><") { source, offset, prefix ->
var i = offset
Parser.assert(source, i, prefix)
SyntaxError.assert(source, i, prefix)
return@Parser when (source[i]) {
'+' -> ParserResult(CombinatorSelector.Operator.ElderBrother, ++i - offset)
'-' -> ParserResult(CombinatorSelector.Operator.YoungerBrother, ++i - offset)
'>' -> ParserResult(CombinatorSelector.Operator.Ancestor, ++i - offset)
'<' -> ParserResult(CombinatorSelector.Operator.Child, ++i - offset)
else -> throw ParserException(source, i, prefix)
else -> SyntaxError.throwError(source, i, prefix)
}
}
val integerParser = Parser("1234567890") { source, offset, prefix ->
var i = offset
Parser.assert(source, i, prefix)
SyntaxError.assert(source, i, prefix, "number")
var s = ""
while (prefix.contains(source[i]) && i < source.length) {
s += source[i]
@ -74,8 +74,13 @@ object Tools {
ParserResult(s.toInt(), i - offset)
}
val monomialParser = Parser("+-") { source, offset, _ ->
// [+-][a][n[^b]]
val monomialParser = Parser("+-1234567890n") { source, offset, prefix ->
var i = offset
SyntaxError.assert(source, i, prefix)
/**
* one of 1, -1
*/
val signal = when (source[i]) {
'+' -> {
i++
@ -87,9 +92,9 @@ object Tools {
}
else -> 1
}
Parser.assert(source, i)
i += whiteCharParser(source, i).length
Parser.assert(source, i)
// [a][n[^b]]
SyntaxError.assert(source, i, integerParser.prefix + "n")
val coefficient =
if (integerParser.prefix.contains(source[i])) {
val coefficientResult = integerParser(source, i)
@ -98,6 +103,7 @@ object Tools {
} else {
1
} * signal
// [n[^b]]
if (i < source.length && source[i] == 'n') {
i++
if (i < source.length && source[i] == '^') {
@ -113,28 +119,29 @@ object Tools {
}
}
// ([+-][a][n[^b]] [+-][a][n[^b]])
val expressionParser = Parser("(0123456789n") { source, offset, prefix ->
var i = offset
Parser.assert(source, i, prefix)
SyntaxError.assert(source, i, prefix)
val monomialResultList = mutableListOf<ParserResult<Pair<Int, Int>>>()
when (source[i]) {
'(' -> {
i += 1
i++
i += whiteCharParser(source, i).length
Parser.assert(source, i, "+-n1234567890")
SyntaxError.assert(source, i, monomialParser.prefix)
while (source[i] != ')') {
if (monomialResultList.size > 0) {
Parser.assert(source, i, "+-")
SyntaxError.assert(source, i, "+-")
}
val monomialResult = monomialParser(source, i)
monomialResultList.add(monomialResult)
i += monomialResult.length
i += whiteCharParser(source, i).length
if (i >= source.length) {
Parser.assert(source, i, ")")
SyntaxError.assert(source, i, ")")
}
}
i += 1
i++
}
else -> {
val monomialResult = monomialParser(source, i)
@ -152,6 +159,7 @@ object Tools {
}), i - offset)
}
// [+-><](a*n^b)
val combinatorParser = Parser(combinatorOperatorParser.prefix) { source, offset, _ ->
var i = offset
val operatorResult = combinatorOperatorParser(source, i)
@ -171,7 +179,7 @@ object Tools {
val attrOperatorParser = Parser("><!*$^=") { source, offset, prefix ->
var i = offset
Parser.assert(source, i, prefix)
SyntaxError.assert(source, i, prefix)
val attrOperator =
when (source[i]) {
'=' -> {
@ -212,30 +220,30 @@ object Tools {
}
'!' -> {
i++
Parser.assert(source, i, '=')
SyntaxError.assert(source, i, "=")
i++
PropertySelector.Operator.NotEqual
}
'*' -> {
i++
Parser.assert(source, i, '=')
SyntaxError.assert(source, i, "=")
i++
PropertySelector.Operator.Include
}
'^' -> {
i++
Parser.assert(source, i, '=')
SyntaxError.assert(source, i, "=")
i++
PropertySelector.Operator.Start
}
'$' -> {
i++
Parser.assert(source, i, '=')
SyntaxError.assert(source, i, "=")
i++
PropertySelector.Operator.End
}
else -> {
Parser.throwError(source, i, prefix)
SyntaxError.throwError(source, i, prefix)
}
}
ParserResult(attrOperator, i - offset)
@ -243,20 +251,20 @@ object Tools {
val stringParser = Parser("`") { source, offset, prefix ->
var i = offset
Parser.assert(source, i, prefix)
SyntaxError.assert(source, i, prefix)
i++
var data = ""
while (source[i] != '`') {
if (i == source.length - 1) {
Parser.assert(source, i, '`')
SyntaxError.assert(source, i, "`")
break
}
if (source[i] == '\\') {
i++
Parser.assert(source, i)
SyntaxError.assert(source, i)
if (source[i] == '`') {
data += source[i]
Parser.assert(source, i + 1)
SyntaxError.assert(source, i + 1)
} else {
data += '\\' + source[i].toString()
}
@ -271,12 +279,12 @@ object Tools {
val numberParser = Parser("1234567890.") { source, offset, prefix ->
var i = offset
Parser.assert(source, i, prefix)
SyntaxError.assert(source, i, prefix)
var value = ""
value = if (source[i] == '.') {
value += source[i]
i++
Parser.assert(source, i, "1234567890")
SyntaxError.assert(source, i, "1234567890")
while ("1234567890".contains(source[i])) {
value += source[i]
i++
@ -316,40 +324,49 @@ object Tools {
ParserResult<Number>(data, i - offset)
}
val propertyParser =
Parser("1234567890qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM_") { source, offset, prefix ->
var i = offset
SyntaxError.assert(source, i, prefix)
var data = source[i].toString()
i++
while (i < source.length) {
if (!prefix.contains(source[i])) {
break
}
data += source[i]
i++
}
ParserResult(data, i - offset)
}
val valueParser = Parser("tfn`1234567890.") { source, offset, prefix ->
var i = offset
Parser.assert(source, i, prefix)
SyntaxError.assert(source, i, prefix)
val value: Any? = when (source[i]) {
't' -> {
i++
Parser.assert(source, i, 'r')
i++
Parser.assert(source, i, 'u')
i++
Parser.assert(source, i, 'e')
i++
"rue".forEach { c ->
SyntaxError.assert(source, i, c.toString())
i++
}
true
}
'f' -> {
i++
Parser.assert(source, i, 'a')
i++
Parser.assert(source, i, 'l')
i++
Parser.assert(source, i, 's')
i++
Parser.assert(source, i, 'e')
i++
"alse".forEach { c ->
SyntaxError.assert(source, i, c.toString())
i++
}
false
}
'n' -> {
i++
Parser.assert(source, i, 'u')
i++
Parser.assert(source, i, 'l')
i++
Parser.assert(source, i, 'l')
i++
"ull".forEach { c ->
SyntaxError.assert(source, i, c.toString())
i++
}
null
}
'`' -> {
@ -363,7 +380,7 @@ object Tools {
n.data
}
else -> {
Parser.throwError(source, i, prefix)
SyntaxError.throwError(source, i, prefix)
}
}
ParserResult(value, i - offset)
@ -371,27 +388,32 @@ object Tools {
val attrParser = Parser("[") { source, offset, prefix ->
var i = offset
Parser.assert(source, i, prefix)
SyntaxError.assert(source, i, prefix)
i++
val nameResult = nameParser(source, i)
i += nameResult.length
val parserResult = propertyParser(source, i)
i += parserResult.length
val operatorResult = attrOperatorParser(source, i)
i += operatorResult.length
val valueResult = valueParser(source, i)
i += valueResult.length
Parser.assert(source, i, ']')
SyntaxError.assert(source, i, "]")
i++
ParserResult(
PropertySelector.BinaryExpression(
nameResult.data,
parserResult.data,
operatorResult.data,
valueResult.data
), i - offset
)
}
val selectorParser = Parser(nameParser.prefix) { source, offset, _ ->
val selectorParser = Parser { source, offset, _ ->
var i = offset
var match = false
if (source.getOrNull(i) == '@') {
match = true
i++
}
val nameResult = nameParser(source, i)
i += nameResult.length
val attrList = mutableListOf<PropertySelector.BinaryExpression>()
@ -400,7 +422,10 @@ object Tools {
i += attrResult.length
attrList.add(attrResult.data)
}
ParserResult(PropertySelector(nameResult.data, attrList), i - offset)
if (nameResult.length == 0 && attrList.size == 0) {
SyntaxError.throwError(source, i, "[")
}
ParserResult(PropertySelector(match, nameResult.data, attrList), i - offset)
}
val combinatorSelectorParser = Parser { source, offset, _ ->
@ -426,72 +451,26 @@ object Tools {
ParserResult(topSelector.data to selectorList, i - offset)
}
val unitParser = Parser<Position.Unit> { source, offset, _ ->
var i = offset
Parser.assert(source, i, "%")
i++
ParserResult(Position.Unit.Percentage, i - offset)
}
val numberUnitParser = Parser { source, offset, _ ->
var i = offset
val valueResult = numberParser(source, i)
i += valueResult.length
val unitResult = unitParser(source, i)
i += unitResult.length
ParserResult(Position.NumberUnit(valueResult.data, unitResult.data), i - offset)
}
val positionParser = Parser("(") { source, offset, _ ->
var i = offset
Parser.assert(source, i, '(')
i++
i += whiteCharParser(source, i).length
val xNumberUnitResult = numberUnitParser(source, i)
i += xNumberUnitResult.length
i += whiteCharParser(source, i).length
Parser.assert(source, i, ',')
i++
i += whiteCharParser(source, i).length
val yNumberUnitResult = numberUnitParser(source, i)
i += yNumberUnitResult.length
i += whiteCharParser(source, i).length
Parser.assert(source, i, ')')
i++
ParserResult(Position(xNumberUnitResult.data, yNumberUnitResult.data), i - offset)
}
val endParser = Parser { source, offset, _ ->
if (offset != source.length) {
Parser.throwError(source, offset, null)
SyntaxError.throwError(source, offset, "end")
}
ParserResult(Unit, 0)
}
val combinatorPositionSelectorParser = Parser { source, offset, _ ->
var i = offset
val gkdSelectorParser: (String) -> GkdSelector = { source ->
var i = 0
i += whiteCharParser(source, i).length
val combinatorSelectorResult = combinatorSelectorParser(source, i)
i += combinatorSelectorResult.length
val position =
if (i < source.length && positionParser.prefix.contains(source[i])) {
val positionResult = positionParser(source, i)
i += positionResult.length
positionResult.data
} else {
null
}
i += whiteCharParser(source, i).length
i += endParser(source, i).length
ParserResult(Pair(combinatorSelectorResult.data, position), i - offset)
}
val gkdSelectorParser: (String) -> GkdSelector = { source ->
val (data) = combinatorPositionSelectorParser(source, 0)
val data = combinatorSelectorResult.data
val propertySelectorList = mutableListOf<PropertySelector>()
val combinatorSelectorList = mutableListOf<CombinatorSelector>()
propertySelectorList.add(data.first.first)
data.first.second.forEach {
propertySelectorList.add(data.first)
data.second.forEach {
propertySelectorList.add(it.second)
combinatorSelectorList.add(it.first)
}
@ -499,9 +478,10 @@ object Tools {
combinatorSelectorList.forEachIndexed { index, combinatorSelector ->
val combinatorSelectorWrapper =
CombinatorSelectorWrapper(combinatorSelector, wrapperList.last())
val propertySelectorWrapper = PropertySelectorWrapper(propertySelectorList[index + 1], combinatorSelectorWrapper)
val propertySelectorWrapper =
PropertySelectorWrapper(propertySelectorList[index + 1], combinatorSelectorWrapper)
wrapperList.add(propertySelectorWrapper)
}
GkdSelector(wrapperList.last(), data.second)
GkdSelector(wrapperList.last())
}
}

View File

@ -1,4 +1,4 @@
package li.songe.node_selector.selector
package li.songe.selector.selector
/**
* 关系连接选择器

View File

@ -0,0 +1,350 @@
package li.songe.selector.selector
import android.view.accessibility.AccessibilityNodeInfo
import li.songe.selector.getDepth
import li.songe.selector.getIndex
/**
* 属性选择器
*/
data class PropertySelector(
val match: Boolean,
val name: String,
val expressionList: List<BinaryExpression>
) {
override fun toString() = "${if (match) "@" else ""}${name}${expressionList.joinToString("")}"
sealed class Operator(private val key: String) {
object More : Operator(">")
object Less : Operator("<")
object Equal : Operator("=")
object NotEqual : Operator("!=")
object MoreEqual : Operator(">=")
object LessEqual : Operator("<=")
object Include : Operator("*=")
object Start : Operator("^=")
object End : Operator("$=")
override fun toString() = key
}
data class BinaryExpression(val name: String, val operator: Operator, val value: Any?) {
override fun toString() = "[${name}${operator}${
if (value is String) {
"`${value.replace("`", "\\`")}`"
} else {
value
}
}]"
val matchNodeInfo: (nodeInfo: AccessibilityNodeInfo) -> Boolean = when (operator) {
Operator.Start -> when (name) {
"id" -> when (value) {
is String -> ({ nodeInfo -> nodeInfo.viewIdResourceName?.startsWith(value) == true })
else -> ({ false })
}
"text" -> when (value) {
is String -> ({ nodeInfo -> nodeInfo.text?.startsWith(value) == true })
else -> ({ false })
}
"hintText" -> when (value) {
is String -> ({ nodeInfo -> nodeInfo.hintText?.startsWith(value) == true })
else -> ({ false })
}
"contentDesc" -> when (value) {
is String -> ({ nodeInfo -> nodeInfo.contentDescription?.startsWith(value) == true })
else -> ({ false })
}
"className" -> when (value) {
is String -> ({ nodeInfo -> nodeInfo.className?.startsWith(value) == true })
else -> ({ false })
}
else -> ({ false })
}
Operator.Include -> when (name) {
"id" -> when (value) {
is String -> ({ nodeInfo -> nodeInfo.viewIdResourceName?.contains(value) == true })
else -> ({ false })
}
"text" -> when (value) {
is String -> ({ nodeInfo -> nodeInfo.text?.contains(value) == true })
else -> ({ false })
}
"hintText" -> when (value) {
is String -> ({ nodeInfo -> nodeInfo.hintText?.contains(value) == true })
else -> ({ false })
}
"contentDesc" -> when (value) {
is String -> ({ nodeInfo -> nodeInfo.contentDescription?.contains(value) == true })
else -> ({ false })
}
"className" -> when (value) {
is String -> ({ nodeInfo -> nodeInfo.className?.contains(value) == true })
else -> ({ false })
}
else -> ({ false })
}
Operator.End -> {
when (name) {
"id" -> when (value) {
is String -> ({ nodeInfo -> nodeInfo.viewIdResourceName?.endsWith(value) == true })
else -> ({ false })
}
"text" -> when (value) {
is String -> ({ nodeInfo -> nodeInfo.text?.endsWith(value) == true })
else -> ({ false })
}
"hintText" -> when (value) {
is String -> ({ nodeInfo -> nodeInfo.hintText?.endsWith(value) == true })
else -> ({ false })
}
"contentDesc" -> when (value) {
is String -> ({ nodeInfo -> nodeInfo.contentDescription?.endsWith(value) == true })
else -> ({ false })
}
"className" -> when (value) {
is String -> ({ nodeInfo -> nodeInfo.className?.endsWith(value) == true })
else -> ({ false })
}
else -> ({ false })
}
}
Operator.Less -> when (name) {
"index" -> when (value) {
is Int -> ({ nodeInfo -> nodeInfo.getIndex()?.let { it < value } == true })
else -> ({ false })
}
"childCount" -> when (value) {
is Int -> ({ nodeInfo -> nodeInfo.childCount < value })
else -> ({ false })
}
"depth" -> when (value) {
is Int -> ({ nodeInfo -> nodeInfo.getDepth() < value })
else -> ({ false })
}
"text.length" -> when (value) {
is Int -> ({ nodeInfo -> nodeInfo.text?.length?.let { it < value } == true })
else -> ({ false })
}
"hintText.length" -> when (value) {
is Int -> ({ nodeInfo -> nodeInfo.hintText?.length?.let { it < value } == true })
else -> ({ false })
}
"contentDesc.length" -> when (value) {
is Int -> ({ nodeInfo -> nodeInfo.contentDescription?.length?.let { it < value } == true })
else -> ({ false })
}
"className.length" -> when (value) {
is Int -> ({ nodeInfo -> nodeInfo.className?.length?.let { it < value } == true })
else -> ({ false })
}
else -> ({ false })
}
Operator.LessEqual -> when (name) {
"index" -> when (value) {
is Int -> ({ nodeInfo -> nodeInfo.getIndex()?.let { it <= value } == true })
else -> ({ false })
}
"childCount" -> when (value) {
is Int -> ({ nodeInfo -> nodeInfo.childCount <= value })
else -> ({ false })
}
"depth" -> when (value) {
is Int -> ({ nodeInfo -> nodeInfo.getDepth() <= value })
else -> ({ false })
}
"text.length" -> when (value) {
is Int -> ({ nodeInfo -> nodeInfo.text?.length?.let { it <= value } == true })
else -> ({ false })
}
"hintText.length" -> when (value) {
is Int -> ({ nodeInfo -> nodeInfo.hintText?.length?.let { it <= value } == true })
else -> ({ false })
}
"contentDesc.length" -> when (value) {
is Int -> ({ nodeInfo -> nodeInfo.contentDescription?.length?.let { it <= value } == true })
else -> ({ false })
}
"className.length" -> when (value) {
is Int -> ({ nodeInfo -> nodeInfo.className?.length?.let { it <= value } == true })
else -> ({ false })
}
else -> ({ false })
}
Operator.More -> when (name) {
"index" -> when (value) {
is Int -> ({ nodeInfo -> nodeInfo.getIndex()?.let { it > value } == true })
else -> ({ false })
}
"childCount" -> when (value) {
is Int -> ({ nodeInfo -> nodeInfo.childCount > value })
else -> ({ false })
}
"depth" -> when (value) {
is Int -> ({ nodeInfo -> nodeInfo.getDepth() > value })
else -> ({ false })
}
"text.length" -> when (value) {
is Int -> ({ nodeInfo -> nodeInfo.text?.length?.let { it > value } == true })
else -> ({ false })
}
"hintText.length" -> when (value) {
is Int -> ({ nodeInfo -> nodeInfo.hintText?.length?.let { it > value } == true })
else -> ({ false })
}
"contentDesc.length" -> when (value) {
is Int -> ({ nodeInfo -> nodeInfo.contentDescription?.length?.let { it > value } == true })
else -> ({ false })
}
"className.length" -> when (value) {
is Int -> ({ nodeInfo -> nodeInfo.className?.length?.let { it > value } == true })
else -> ({ false })
}
else -> ({ false })
}
Operator.MoreEqual -> when (name) {
"index" -> when (value) {
is Int -> ({ nodeInfo -> nodeInfo.getIndex()?.let { it >= value } == true })
else -> ({ false })
}
"childCount" -> when (value) {
is Int -> ({ nodeInfo -> nodeInfo.childCount >= value })
else -> ({ false })
}
"depth" -> when (value) {
is Int -> ({ nodeInfo -> nodeInfo.getDepth() >= value })
else -> ({ false })
}
"text.length" -> when (value) {
is Int -> ({ nodeInfo -> nodeInfo.text?.length?.let { it >= value } == true })
else -> ({ false })
}
"hintText.length" -> when (value) {
is Int -> ({ nodeInfo -> nodeInfo.hintText?.length?.let { it >= value } == true })
else -> ({ false })
}
"contentDesc.length" -> when (value) {
is Int -> ({ nodeInfo -> nodeInfo.contentDescription?.length?.let { it >= value } == true })
else -> ({ false })
}
"className.length" -> when (value) {
is Int -> ({ nodeInfo -> nodeInfo.className?.length?.let { it >= value } == true })
else -> ({ false })
}
else -> ({ false })
}
Operator.Equal -> when (name) {
"index" -> when (value) {
is Int? -> ({ nodeInfo -> nodeInfo.getIndex() == value })
else -> ({ false })
}
"childCount" -> when (value) {
is Int? -> ({ nodeInfo -> nodeInfo.getIndex() == value })
else -> ({ false })
}
"depth" -> when (value) {
is Int? -> ({ nodeInfo -> nodeInfo.getDepth() == value })
else -> ({ false })
}
"text" -> when (value) {
is String? -> ({ nodeInfo -> nodeInfo.text == value })
else -> ({ false })
}
"text.length" -> when (value) {
is Int? -> ({ nodeInfo -> nodeInfo.text?.length == value })
else -> ({ false })
}
"hintText" -> when (value) {
is String? -> ({ nodeInfo -> nodeInfo.hintText == value })
else -> ({ false })
}
"hintText.length" -> when (value) {
is Int? -> ({ nodeInfo -> nodeInfo.hintText?.length == value })
else -> ({ false })
}
"contentDesc" -> when (value) {
is String? -> ({ nodeInfo -> nodeInfo.contentDescription == value })
else -> ({ false })
}
"contentDesc.length" -> when (value) {
is Int? -> ({ nodeInfo -> nodeInfo.contentDescription?.length == value })
else -> ({ false })
}
"isPassword" -> when (value) {
is Boolean? -> ({ nodeInfo -> nodeInfo.isPassword == value })
else -> ({ false })
}
"isChecked" -> when (value) {
is Boolean? -> ({ nodeInfo -> nodeInfo.isChecked == value })
else -> ({ false })
}
"className" -> when (value) {
is String? -> ({ nodeInfo -> nodeInfo.className == value })
else -> ({ false })
}
else -> ({ false })
}
Operator.NotEqual -> when (name) {
"index" -> when (value) {
is Int? -> ({ nodeInfo -> nodeInfo.getIndex() != value })
else -> ({ false })
}
"childCount" -> when (value) {
is Int? -> ({ nodeInfo -> nodeInfo.getIndex() != value })
else -> ({ false })
}
"depth" -> when (value) {
is Int? -> ({ nodeInfo -> nodeInfo.getDepth() != value })
else -> ({ false })
}
"text" -> when (value) {
is String? -> ({ nodeInfo -> nodeInfo.text != value })
else -> ({ false })
}
"text.length" -> when (value) {
is Int? -> ({ nodeInfo -> nodeInfo.text?.length != value })
else -> ({ false })
}
"hintText" -> when (value) {
is String? -> ({ nodeInfo -> nodeInfo.hintText != value })
else -> ({ false })
}
"hintText.length" -> when (value) {
is Int? -> ({ nodeInfo -> nodeInfo.hintText?.length != value })
else -> ({ false })
}
"contentDesc" -> when (value) {
is String? -> ({ nodeInfo -> nodeInfo.contentDescription != value })
else -> ({ false })
}
"contentDesc.length" -> when (value) {
is Int? -> ({ nodeInfo -> nodeInfo.contentDescription?.length != value })
else -> ({ false })
}
"isPassword" -> when (value) {
is Boolean? -> ({ nodeInfo -> nodeInfo.isPassword != value })
else -> ({ false })
}
"isChecked" -> when (value) {
is Boolean? -> ({ nodeInfo -> nodeInfo.isChecked != value })
else -> ({ false })
}
"className" -> when (value) {
is String? -> ({ nodeInfo -> nodeInfo.className != value })
else -> ({ false })
}
else -> ({ false })
}
}
}
}

View File

@ -1,8 +1,8 @@
package li.songe.node_selector.wrapper
package li.songe.selector.wrapper
import android.view.accessibility.AccessibilityNodeInfo
import li.songe.node_selector.*
import li.songe.node_selector.selector.CombinatorSelector
import li.songe.selector.*
import li.songe.selector.selector.CombinatorSelector
data class CombinatorSelectorWrapper(
private val combinatorSelector: CombinatorSelector,
@ -12,7 +12,10 @@ data class CombinatorSelectorWrapper(
return to.toString() + "\u0020" + combinatorSelector.toString()
}
fun match(nodeInfo: AccessibilityNodeInfo): Boolean {
fun match(
nodeInfo: AccessibilityNodeInfo,
trackList: MutableList<AccessibilityNodeInfo?>
): Boolean {
val expression = combinatorSelector.polynomialExpression
val isConstant = expression.isConstant
when (combinatorSelector.operator) {
@ -22,9 +25,9 @@ data class CombinatorSelectorWrapper(
if (constantValue > 0) {
nodeInfo.traverseAncestor { depth, ancestorNode ->
if (depth == constantValue) {
val targetNode = to.match(ancestorNode)
val targetNode = to.match(ancestorNode, trackList)
if (targetNode) {
return targetNode
return true
}
return@traverseAncestor true
}
@ -46,9 +49,9 @@ data class CombinatorSelectorWrapper(
nodeInfo.traverseAncestor { depth, ancestorNode ->
if (set.contains(depth)) {
set.remove(depth)
val targetNode = to.match(ancestorNode)
val targetNode = to.match(ancestorNode, trackList)
if (targetNode) {
return targetNode
return true
}
}
}
@ -60,7 +63,7 @@ data class CombinatorSelectorWrapper(
if (0 < constantValue && constantValue <= nodeInfo.childCount) {
val childNode: AccessibilityNodeInfo? = nodeInfo.getChild(constantValue - 1)
if (childNode != null) {
return to.match(childNode)
return to.match(childNode, trackList)
}
}
} else {
@ -77,9 +80,9 @@ data class CombinatorSelectorWrapper(
nodeInfo.forEachIndexed { index, childNode ->
if (set.contains(index)) {
set.remove(index)
val targetNode = to.match(childNode)
val targetNode = to.match(childNode, trackList)
if (targetNode) {
return targetNode
return true
}
}
}
@ -92,9 +95,9 @@ data class CombinatorSelectorWrapper(
if (constantValue in 1..i) {
nodeInfo.traverseElderBrother { offset, brotherNode ->
if (offset == constantValue) {
val targetNode = to.match(brotherNode)
val targetNode = to.match(brotherNode, trackList)
if (targetNode) {
return targetNode
return true
}
return@traverseElderBrother true
}
@ -115,9 +118,9 @@ data class CombinatorSelectorWrapper(
nodeInfo.traverseElderBrother { offset, brotherNode ->
if (set.contains(offset)) {
set.remove(offset)
val targetNode = to.match(brotherNode)
val targetNode = to.match(brotherNode, trackList)
if (targetNode) {
return targetNode
return true
}
}
}
@ -130,9 +133,9 @@ data class CombinatorSelectorWrapper(
if (0 < constantValue && i + constantValue < nodeInfo.parent.childCount) {
nodeInfo.traverseYoungerBrother { offset, brotherNode ->
if (offset == constantValue) {
val targetNode = to.match(brotherNode)
val targetNode = to.match(brotherNode, trackList)
if (targetNode) {
return targetNode
return true
}
return@traverseYoungerBrother true
}
@ -154,9 +157,9 @@ data class CombinatorSelectorWrapper(
nodeInfo.traverseYoungerBrother { offset, brotherNode ->
if (set.contains(offset)) {
set.remove(offset)
val targetNode = to.match(brotherNode)
val targetNode = to.match(brotherNode, trackList)
if (targetNode) {
return targetNode
return true
}
}
}

View File

@ -0,0 +1,51 @@
package li.songe.selector.wrapper
import android.view.accessibility.AccessibilityNodeInfo
import li.songe.selector.selector.PropertySelector
data class PropertySelectorWrapper(
private val propertySelector: PropertySelector,
val to: CombinatorSelectorWrapper? = null
) {
override fun toString(): String {
return (if (to != null) {
to.toString() + "\u0020"
} else {
""
}) + propertySelector.toString()
}
fun match(
nodeInfo: AccessibilityNodeInfo,
trackList: MutableList<AccessibilityNodeInfo?>
): Boolean {
if (propertySelector.name != "*" || propertySelector.name.isNotEmpty()) {
val className = nodeInfo.className.toString()
if (!((className.endsWith(propertySelector.name) &&
className[className.length - propertySelector.name.length - 1] == '.')
|| className == propertySelector.name
)
) {
return false
}
}
propertySelector.expressionList.forEach { expression ->
if (!expression.matchNodeInfo(nodeInfo)) {
return false
}
}
// 属性匹配单元 完全匹配 之后
if (propertySelector.match || trackList.isEmpty()) {
trackList.add(nodeInfo)
} else {
trackList.add(null)
}
if (to == null) {
return true
}
return to.match(nodeInfo, trackList)
}
}

View File

@ -1,6 +1,6 @@
package li.songe.node_selector
package li.songe.selector
import li.songe.node_selector.parser.Tools.gkdSelectorParser
import li.songe.selector.parser.Transform.gkdSelectorParser
import org.json.JSONObject
import org.junit.Test
@ -28,7 +28,7 @@ class ExampleUnitTest {
@Test
fun check_property() {
val source = "View[k^=.3] <2 P + Z - G <2 P[k=1][k==2] T Sing V > V(1.%, 50%)"
val source = "View[k^=.3] <2 @P + Z - G <2 P[k=1][k==2] T Sing V > V"
println("source:$source")
// val result = combinatorPositionSelectorParser(source, 0)
// println("first:"+result.data.first.first)

View File

@ -20,7 +20,7 @@ pluginManagement {
rootProject.name = "gkd"
include(":app")
include(":node_selector")
include(":selector")
include(":room_processor")
//include(":shizuku_automator")
//include(":shizuku_automator:hidden_api")