mirror of
https://github.com/gkd-kit/gkd.git
synced 2024-11-15 19:22:26 +08:00
feat: build ok
This commit is contained in:
parent
6c72cbab9f
commit
acdd10ddb5
|
@ -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")
|
||||
|
||||
}
|
|
@ -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')"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
|
@ -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() {
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
@ -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
|
||||
)
|
|
@ -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 {
|
|
@ -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 {
|
|
@ -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 ""
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 ""
|
||||
}
|
||||
}
|
12
app/src/main/java/li/songe/gkd/subs/SubsParser.kt
Normal file
12
app/src/main/java/li/songe/gkd/subs/SubsParser.kt
Normal 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))
|
||||
}
|
||||
}
|
14
app/src/main/java/li/songe/gkd/subs/data/GkdAction.kt
Normal file
14
app/src/main/java/li/songe/gkd/subs/data/GkdAction.kt
Normal 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?
|
||||
)
|
10
app/src/main/java/li/songe/gkd/subs/data/GkdApp.kt
Normal file
10
app/src/main/java/li/songe/gkd/subs/data/GkdApp.kt
Normal 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>
|
||||
)
|
18
app/src/main/java/li/songe/gkd/subs/data/GkdGroup.kt
Normal file
18
app/src/main/java/li/songe/gkd/subs/data/GkdGroup.kt
Normal 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
|
||||
)
|
18
app/src/main/java/li/songe/gkd/subs/data/GkdRule.kt
Normal file
18
app/src/main/java/li/songe/gkd/subs/data/GkdRule.kt
Normal 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,
|
||||
)
|
18
app/src/main/java/li/songe/gkd/subs/data/GkdSubs.kt
Normal file
18
app/src/main/java/li/songe/gkd/subs/data/GkdSubs.kt
Normal 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>
|
||||
)
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
)
|
||||
},
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 = "")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
63
app/src/main/java/li/songe/gkd/ui/component/SubsCard.kt
Normal file
63
app/src/main/java/li/songe/gkd/ui/component/SubsCard.kt
Normal 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()))
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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 = "备注注注注注注注注注注注注注注注注注注注注注注注注注注注注注注注注注注注注注注注注注注注注注注"
|
||||
)
|
||||
)
|
||||
|
|
34
app/src/main/java/li/songe/gkd/ui/component/Text114514.kt
Normal file
34
app/src/main/java/li/songe/gkd/ui/component/Text114514.kt
Normal 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()
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
// }
|
||||
|
||||
|
||||
}
|
16
app/src/main/res/drawable/ic_menu.xml
Normal file
16
app/src/main/res/drawable/ic_menu.xml
Normal 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>
|
|
@ -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>
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>>()
|
||||
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
|
@ -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"
|
||||
}"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
package li.songe.node_selector.parser
|
||||
|
||||
data class ParserResult<T>(val data: T, val length: Int=0)
|
|
@ -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("%")
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}]"
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -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")
|
||||
|
|
|
@ -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
|
|
@ -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>
|
|
@ -1,4 +1,4 @@
|
|||
package li.songe.node_selector
|
||||
package li.songe.selector
|
||||
|
||||
import android.view.accessibility.AccessibilityNodeInfo
|
||||
import java.util.*
|
51
selector/src/main/java/li/songe/selector/GkdSelector.kt
Normal file
51
selector/src/main/java/li/songe/selector/GkdSelector.kt
Normal 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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
package li.songe.selector.parser
|
||||
|
||||
data class ParserResult<T>(val data: T, val length: Int = 0)
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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())
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package li.songe.node_selector.selector
|
||||
package li.songe.selector.selector
|
||||
|
||||
/**
|
||||
* 关系连接选择器
|
|
@ -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 })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -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)
|
|
@ -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")
|
||||
|
|
Loading…
Reference in New Issue
Block a user