mirror of
https://github.com/gkd-kit/gkd.git
synced 2024-11-16 11:42:22 +08:00
0.支持控制常驻通知栏 1.支持全局规则 2.优化规则触发逻辑 3.优化界面布局 4.优化申请权限流程 5.优化复制规则组为json5 6.快照新增editable字段 7.允许开启匹配未安装应用
This commit is contained in:
parent
5c430e0dbe
commit
8c99ba7f4c
302
app/schemas/li.songe.gkd.db.AppDb/4.json
Normal file
302
app/schemas/li.songe.gkd.db.AppDb/4.json
Normal file
|
@ -0,0 +1,302 @@
|
|||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 4,
|
||||
"identityHash": "d1a618bf8475b588793fb1d201815a08",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "subs_item",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `ctime` INTEGER NOT NULL, `mtime` INTEGER NOT NULL, `enable` INTEGER NOT NULL, `enable_update` INTEGER NOT NULL, `order` INTEGER NOT NULL, `update_url` TEXT, PRIMARY KEY(`id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "ctime",
|
||||
"columnName": "ctime",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "mtime",
|
||||
"columnName": "mtime",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "enable",
|
||||
"columnName": "enable",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "enableUpdate",
|
||||
"columnName": "enable_update",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "order",
|
||||
"columnName": "order",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "updateUrl",
|
||||
"columnName": "update_url",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "snapshot",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `app_id` TEXT, `activity_id` TEXT, `app_name` TEXT, `app_version_code` INTEGER, `app_version_name` TEXT, `screen_height` INTEGER NOT NULL, `screen_width` INTEGER NOT NULL, `is_landscape` INTEGER NOT NULL, `github_asset_id` INTEGER, PRIMARY KEY(`id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "appId",
|
||||
"columnName": "app_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "activityId",
|
||||
"columnName": "activity_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "appName",
|
||||
"columnName": "app_name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "appVersionCode",
|
||||
"columnName": "app_version_code",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "appVersionName",
|
||||
"columnName": "app_version_name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "screenHeight",
|
||||
"columnName": "screen_height",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "screenWidth",
|
||||
"columnName": "screen_width",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isLandscape",
|
||||
"columnName": "is_landscape",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "githubAssetId",
|
||||
"columnName": "github_asset_id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "subs_config",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` 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, PRIMARY KEY(`id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "type",
|
||||
"columnName": "type",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "enable",
|
||||
"columnName": "enable",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "subsItemId",
|
||||
"columnName": "subs_item_id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "appId",
|
||||
"columnName": "app_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "groupKey",
|
||||
"columnName": "group_key",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "click_log",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `app_id` TEXT, `activity_id` TEXT, `subs_id` INTEGER NOT NULL, `subs_version` INTEGER NOT NULL DEFAULT 0, `group_key` INTEGER NOT NULL, `group_type` INTEGER NOT NULL DEFAULT 2, `rule_index` INTEGER NOT NULL, `rule_key` INTEGER, PRIMARY KEY(`id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "appId",
|
||||
"columnName": "app_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "activityId",
|
||||
"columnName": "activity_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "subsId",
|
||||
"columnName": "subs_id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "subsVersion",
|
||||
"columnName": "subs_version",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "0"
|
||||
},
|
||||
{
|
||||
"fieldPath": "groupKey",
|
||||
"columnName": "group_key",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "groupType",
|
||||
"columnName": "group_type",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "2"
|
||||
},
|
||||
{
|
||||
"fieldPath": "ruleIndex",
|
||||
"columnName": "rule_index",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "ruleKey",
|
||||
"columnName": "rule_key",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "category_config",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `enable` INTEGER, `subs_item_id` INTEGER NOT NULL, `category_key` INTEGER NOT NULL, PRIMARY KEY(`id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "enable",
|
||||
"columnName": "enable",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "subsItemId",
|
||||
"columnName": "subs_item_id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "categoryKey",
|
||||
"columnName": "category_key",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
}
|
||||
],
|
||||
"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, 'd1a618bf8475b588793fb1d201815a08')"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -39,7 +39,6 @@
|
|||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:configChanges="uiMode|screenSize|orientation|keyboardHidden|touchscreen|smallestScreenSize|screenLayout|navigation|mnc|mcc|locale|layoutDirection|keyboard|fontWeightAdjustment|fontScale|density|colorMode"
|
||||
android:exported="true"
|
||||
android:theme="@style/SplashScreenTheme">
|
||||
|
||||
|
|
|
@ -12,29 +12,29 @@ import com.dylanc.activityresult.launcher.RequestPermissionLauncher
|
|||
import com.dylanc.activityresult.launcher.StartActivityLauncher
|
||||
import com.ramcosta.composedestinations.DestinationsNavHost
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import li.songe.gkd.composition.CompositionActivity
|
||||
import li.songe.gkd.composition.CompositionExt.useLifeCycleLog
|
||||
import li.songe.gkd.ui.NavGraphs
|
||||
import li.songe.gkd.ui.component.ConfirmDialog
|
||||
import li.songe.gkd.ui.theme.AppTheme
|
||||
import li.songe.gkd.util.AuthDialog
|
||||
import li.songe.gkd.util.LocalLauncher
|
||||
import li.songe.gkd.util.LocalNavController
|
||||
import li.songe.gkd.util.LocalPickContentLauncher
|
||||
import li.songe.gkd.util.LocalRequestPermissionLauncher
|
||||
import li.songe.gkd.util.UpgradeDialog
|
||||
import li.songe.gkd.util.launchTry
|
||||
import li.songe.gkd.util.map
|
||||
import li.songe.gkd.util.storeFlow
|
||||
|
||||
@AndroidEntryPoint
|
||||
class MainActivity : CompositionActivity({
|
||||
useLifeCycleLog()
|
||||
|
||||
val launcher = StartActivityLauncher(this)
|
||||
val pickContentLauncher = PickContentLauncher(this)
|
||||
val requestPermissionLauncher = RequestPermissionLauncher(this)
|
||||
|
||||
lifecycleScope.launchTry(Dispatchers.IO) {
|
||||
lifecycleScope.launch {
|
||||
storeFlow.map(lifecycleScope) { s -> s.excludeFromRecents }.collect {
|
||||
(app.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager).let { manager ->
|
||||
manager.appTasks.forEach { task ->
|
||||
|
@ -48,6 +48,8 @@ class MainActivity : CompositionActivity({
|
|||
val navController = rememberNavController()
|
||||
|
||||
AppTheme {
|
||||
ConfirmDialog()
|
||||
AuthDialog()
|
||||
UpgradeDialog()
|
||||
CompositionLocalProvider(
|
||||
LocalLauncher provides launcher,
|
||||
|
|
47
app/src/main/kotlin/li/songe/gkd/data/AppRule.kt
Normal file
47
app/src/main/kotlin/li/songe/gkd/data/AppRule.kt
Normal file
|
@ -0,0 +1,47 @@
|
|||
package li.songe.gkd.data
|
||||
|
||||
import li.songe.gkd.service.TopActivity
|
||||
import li.songe.selector.Selector
|
||||
|
||||
class AppRule(
|
||||
matches: List<Selector>,
|
||||
excludeMatches: List<Selector>,
|
||||
actionDelay: Long,
|
||||
quickFind: Boolean,
|
||||
matchDelay: Long,
|
||||
matchTime: Long?,
|
||||
resetMatch: String?,
|
||||
key: Int?,
|
||||
preKeys: Set<Int>,
|
||||
index: Int,
|
||||
rule: RawSubscription.RawAppRule,
|
||||
subsItem: SubsItem,
|
||||
group: RawSubscription.RawAppGroup,
|
||||
rawSubs: RawSubscription,
|
||||
val appId: String,
|
||||
val activityIds: List<String>,
|
||||
val excludeActivityIds: List<String>,
|
||||
val app: RawSubscription.RawApp,
|
||||
) : ResolvedRule(
|
||||
matches = matches,
|
||||
excludeMatches = excludeMatches,
|
||||
actionDelay = actionDelay,
|
||||
quickFind = quickFind,
|
||||
matchDelay = matchDelay,
|
||||
matchTime = matchTime,
|
||||
resetMatch = resetMatch,
|
||||
key = key,
|
||||
preKeys = preKeys,
|
||||
index = index,
|
||||
rule = rule,
|
||||
group = group,
|
||||
subsItem = subsItem,
|
||||
rawSubs = rawSubs,
|
||||
) {
|
||||
override fun matchActivity(topActivity: TopActivity?): Boolean {
|
||||
topActivity ?: return false
|
||||
topActivity.activityId ?: return true
|
||||
if (excludeActivityIds.any { topActivity.activityId.startsWith(it) }) return false
|
||||
return activityIds.isEmpty() || activityIds.any { topActivity.activityId.startsWith(it) }
|
||||
}
|
||||
}
|
|
@ -16,6 +16,7 @@ data class AttrInfo(
|
|||
val focusable: Boolean,
|
||||
val checkable: Boolean,
|
||||
val checked: Boolean,
|
||||
val editable: Boolean,
|
||||
val longClickable: Boolean,
|
||||
val visibleToUser: Boolean,
|
||||
|
||||
|
@ -63,6 +64,7 @@ data class AttrInfo(
|
|||
focusable = node.isFocusable,
|
||||
checkable = node.isCheckable,
|
||||
checked = node.isChecked,
|
||||
editable = node.isEditable,
|
||||
longClickable = node.isLongClickable,
|
||||
visibleToUser = node.isVisibleToUser,
|
||||
|
||||
|
|
|
@ -21,7 +21,9 @@ data class ClickLog(
|
|||
@ColumnInfo(name = "app_id") val appId: String? = null,
|
||||
@ColumnInfo(name = "activity_id") val activityId: String? = null,
|
||||
@ColumnInfo(name = "subs_id") val subsId: Long,
|
||||
@ColumnInfo(name = "subs_version", defaultValue = "0") val subsVersion: Int,
|
||||
@ColumnInfo(name = "group_key") val groupKey: Int,
|
||||
@ColumnInfo(name = "group_type", defaultValue = "2") val groupType: Int,
|
||||
@ColumnInfo(name = "rule_index") val ruleIndex: Int,
|
||||
@ColumnInfo(name = "rule_key") val ruleKey: Int? = null,
|
||||
) : Parcelable {
|
||||
|
|
119
app/src/main/kotlin/li/songe/gkd/data/GkdAction.kt
Normal file
119
app/src/main/kotlin/li/songe/gkd/data/GkdAction.kt
Normal file
|
@ -0,0 +1,119 @@
|
|||
package li.songe.gkd.data
|
||||
|
||||
import android.accessibilityservice.AccessibilityService
|
||||
import android.accessibilityservice.GestureDescription
|
||||
import android.graphics.Path
|
||||
import android.graphics.Rect
|
||||
import android.view.ViewConfiguration
|
||||
import android.view.accessibility.AccessibilityNodeInfo
|
||||
import com.blankj.utilcode.util.ScreenUtils
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
|
||||
typealias ActionFc = (context: AccessibilityService, node: AccessibilityNodeInfo) -> ActionResult
|
||||
|
||||
|
||||
@Serializable
|
||||
data class GkdAction(
|
||||
val selector: String,
|
||||
val quickFind: Boolean = false,
|
||||
val action: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ActionResult(
|
||||
val action: String,
|
||||
val result: Boolean,
|
||||
)
|
||||
|
||||
val clickNode: ActionFc = { _, node ->
|
||||
ActionResult(
|
||||
action = "clickNode", result = node.performAction(AccessibilityNodeInfo.ACTION_CLICK)
|
||||
)
|
||||
}
|
||||
|
||||
val clickCenter: ActionFc = { context, node ->
|
||||
val react = Rect()
|
||||
node.getBoundsInScreen(react)
|
||||
val x = (react.right + react.left) / 2f
|
||||
val y = (react.bottom + react.top) / 2f
|
||||
ActionResult(
|
||||
action = "clickCenter",
|
||||
result = if (0 <= x && 0 <= y && x <= ScreenUtils.getScreenWidth() && y <= ScreenUtils.getScreenHeight()) {
|
||||
val gestureDescription = GestureDescription.Builder()
|
||||
val path = Path()
|
||||
path.moveTo(x, y)
|
||||
gestureDescription.addStroke(
|
||||
GestureDescription.StrokeDescription(
|
||||
path, 0, ViewConfiguration.getTapTimeout().toLong()
|
||||
)
|
||||
)
|
||||
context.dispatchGesture(gestureDescription.build(), null, null)
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
val click: ActionFc = { context, node ->
|
||||
if (node.isClickable) clickNode(context, node) else clickCenter(context, node)
|
||||
}
|
||||
|
||||
val longClickNode: ActionFc = { _, node ->
|
||||
ActionResult(
|
||||
action = "longClickNode",
|
||||
result = node.performAction(AccessibilityNodeInfo.ACTION_LONG_CLICK)
|
||||
)
|
||||
}
|
||||
|
||||
val longClickCenter: ActionFc = { context, node ->
|
||||
val react = Rect()
|
||||
node.getBoundsInScreen(react)
|
||||
val x = (react.right + react.left) / 2f
|
||||
val y = (react.bottom + react.top) / 2f
|
||||
// 内部的 DEFAULT_LONG_PRESS_TIMEOUT 常量是 400
|
||||
// 而 ViewConfiguration.getLongPressTimeout() 返回 300, 这将导致触发普通的 click 事件
|
||||
ActionResult(
|
||||
action = "longClickCenter",
|
||||
result = if (0 <= x && 0 <= y && x <= ScreenUtils.getScreenWidth() && y <= ScreenUtils.getScreenHeight()) {
|
||||
val gestureDescription = GestureDescription.Builder()
|
||||
val path = Path()
|
||||
path.moveTo(x, y)
|
||||
gestureDescription.addStroke(
|
||||
GestureDescription.StrokeDescription(
|
||||
path, 0, 400L
|
||||
)
|
||||
)
|
||||
// TODO 传入处理 callback
|
||||
context.dispatchGesture(gestureDescription.build(), null, null)
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
val longClick: ActionFc = { context, node ->
|
||||
if (node.isLongClickable) longClickNode(context, node) else longClickCenter(context, node)
|
||||
}
|
||||
|
||||
val backFc: ActionFc = { context, _ ->
|
||||
ActionResult(
|
||||
action = "back",
|
||||
result = context.performGlobalAction(AccessibilityService.GLOBAL_ACTION_BACK)
|
||||
)
|
||||
}
|
||||
|
||||
fun getActionFc(action: String?): ActionFc {
|
||||
return when (action) {
|
||||
"clickNode" -> clickNode
|
||||
"clickCenter" -> clickCenter
|
||||
"back" -> backFc
|
||||
"longClick" -> longClick
|
||||
"longClickNode" -> longClickNode
|
||||
"longClickCenter" -> longClickCenter
|
||||
else -> click
|
||||
}
|
||||
}
|
60
app/src/main/kotlin/li/songe/gkd/data/GlobalRule.kt
Normal file
60
app/src/main/kotlin/li/songe/gkd/data/GlobalRule.kt
Normal file
|
@ -0,0 +1,60 @@
|
|||
package li.songe.gkd.data
|
||||
|
||||
import li.songe.gkd.service.TopActivity
|
||||
import li.songe.selector.Selector
|
||||
|
||||
data class GlobalApp(
|
||||
val id: String,
|
||||
val enable: Boolean,
|
||||
val activityIds: List<String>,
|
||||
val excludeActivityIds: List<String>,
|
||||
)
|
||||
|
||||
class GlobalRule(
|
||||
matches: List<Selector>,
|
||||
excludeMatches: List<Selector>,
|
||||
actionDelay: Long,
|
||||
quickFind: Boolean,
|
||||
matchDelay: Long,
|
||||
matchTime: Long?,
|
||||
resetMatch: String?,
|
||||
key: Int?,
|
||||
preKeys: Set<Int>,
|
||||
index: Int,
|
||||
subsItem: SubsItem,
|
||||
rule: RawSubscription.RawGlobalRule,
|
||||
group: RawSubscription.RawGlobalGroup,
|
||||
rawSubs: RawSubscription,
|
||||
val apps: Map<String, GlobalApp>,
|
||||
val matchAnyApp: Boolean,
|
||||
) : ResolvedRule(
|
||||
matches = matches,
|
||||
excludeMatches = excludeMatches,
|
||||
actionDelay = actionDelay,
|
||||
quickFind = quickFind,
|
||||
matchDelay = matchDelay,
|
||||
matchTime = matchTime,
|
||||
resetMatch = resetMatch,
|
||||
key = key,
|
||||
preKeys = preKeys,
|
||||
index = index,
|
||||
rule = rule,
|
||||
group = group,
|
||||
subsItem = subsItem,
|
||||
rawSubs = rawSubs,
|
||||
) {
|
||||
private val excludeAppIds = apps.filter { e -> !e.value.enable }.keys
|
||||
override fun matchActivity(topActivity: TopActivity?): Boolean {
|
||||
topActivity ?: return false
|
||||
if (excludeAppIds.contains(topActivity.appId)) {
|
||||
return false
|
||||
}
|
||||
val app = apps[topActivity.appId] ?: return matchAnyApp
|
||||
topActivity.activityId ?: return true
|
||||
if (app.excludeActivityIds.any { e -> e.startsWith(topActivity.activityId) }) {
|
||||
return false
|
||||
}
|
||||
return app.activityIds.isEmpty() || app.activityIds.any { e -> e.startsWith(topActivity.activityId) }
|
||||
}
|
||||
|
||||
}
|
|
@ -115,7 +115,7 @@ data class NodeInfo(
|
|||
// https://github.com/gkd-kit/gkd/issues/28
|
||||
ToastUtils.showShort("节点数量至多保留$MAX_KEEP_SIZE,丢弃后续节点")
|
||||
LogUtils.w(
|
||||
rootNodeInfo.packageName, topActivityFlow.value?.activityId, "节点数量过多"
|
||||
rootNodeInfo.packageName, topActivityFlow.value.activityId, "节点数量过多"
|
||||
)
|
||||
break
|
||||
}
|
||||
|
|
608
app/src/main/kotlin/li/songe/gkd/data/RawSubscription.kt
Normal file
608
app/src/main/kotlin/li/songe/gkd/data/RawSubscription.kt
Normal file
|
@ -0,0 +1,608 @@
|
|||
package li.songe.gkd.data
|
||||
|
||||
import kotlinx.parcelize.IgnoredOnParcel
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.JsonArray
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
import kotlinx.serialization.json.JsonNull
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
import kotlinx.serialization.json.boolean
|
||||
import kotlinx.serialization.json.int
|
||||
import kotlinx.serialization.json.jsonArray
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.long
|
||||
import li.songe.gkd.util.json
|
||||
import li.songe.gkd.util.json5ToJson
|
||||
import li.songe.selector.Selector
|
||||
|
||||
@Serializable
|
||||
data class RawSubscription(
|
||||
val id: Long,
|
||||
val name: String,
|
||||
val version: Int,
|
||||
val author: String? = null,
|
||||
val updateUrl: String? = null,
|
||||
val supportUri: String? = null,
|
||||
val checkUpdateUrl: String? = null,
|
||||
val apps: List<RawApp> = emptyList(),
|
||||
val categories: List<RawCategory> = emptyList(),
|
||||
val globalGroups: List<RawGlobalGroup> = emptyList(),
|
||||
) {
|
||||
|
||||
@IgnoredOnParcel
|
||||
val categoryToGroupsMap by lazy {
|
||||
val allAppGroups = apps.flatMap { a -> a.groups.map { g -> g to a } }
|
||||
allAppGroups.groupBy { g ->
|
||||
categories.find { c -> g.first.name.startsWith(c.name) }
|
||||
}
|
||||
}
|
||||
|
||||
@IgnoredOnParcel
|
||||
val groupToCategoryMap by lazy {
|
||||
val map = mutableMapOf<RawAppGroup, RawCategory>()
|
||||
categoryToGroupsMap.forEach { (key, value) ->
|
||||
value.forEach { (g) ->
|
||||
if (key != null) {
|
||||
map[g] = key
|
||||
}
|
||||
}
|
||||
}
|
||||
map
|
||||
}
|
||||
|
||||
@IgnoredOnParcel
|
||||
val appGroups by lazy {
|
||||
apps.flatMap { a -> a.groups }
|
||||
}
|
||||
|
||||
@IgnoredOnParcel
|
||||
val allGroupSize by lazy {
|
||||
globalGroups.size + appGroups.size
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class RawApp(
|
||||
val id: String,
|
||||
val name: String?,
|
||||
val groups: List<RawAppGroup> = emptyList(),
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class RawCategory(val key: Int, val name: String, val enable: Boolean?)
|
||||
|
||||
|
||||
interface RawCommonProps {
|
||||
val actionCd: Long?
|
||||
val actionDelay: Long?
|
||||
val quickFind: Boolean?
|
||||
val matchDelay: Long?
|
||||
val matchTime: Long?
|
||||
val actionMaximum: Int?
|
||||
val resetMatch: String?
|
||||
val actionCdKey: Int?
|
||||
val actionMaximumKey: Int?
|
||||
val snapshotUrls: List<String>?
|
||||
val exampleUrls: List<String>?
|
||||
}
|
||||
|
||||
interface RawRuleProps : RawCommonProps {
|
||||
val name: String?
|
||||
val key: Int?
|
||||
val preKeys: List<Int>?
|
||||
val action: String?
|
||||
val matches: List<String>
|
||||
val excludeMatches: List<String>?
|
||||
}
|
||||
|
||||
interface RawGroupProps : RawCommonProps {
|
||||
val name: String
|
||||
val key: Int
|
||||
val desc: String?
|
||||
val enable: Boolean?
|
||||
val rules: List<RawRuleProps>
|
||||
}
|
||||
|
||||
interface RawAppRuleProps {
|
||||
val activityIds: List<String>?
|
||||
val excludeActivityIds: List<String>?
|
||||
}
|
||||
|
||||
interface RawGlobalRuleProps {
|
||||
val matchAnyApp: Boolean?
|
||||
val apps: List<RawGlobalApp>?
|
||||
}
|
||||
|
||||
|
||||
@Serializable
|
||||
data class RawGlobalApp(
|
||||
val id: String,
|
||||
val enable: Boolean?,
|
||||
val activityIds: List<String>?,
|
||||
val excludeActivityIds: List<String>?,
|
||||
)
|
||||
|
||||
|
||||
@Serializable
|
||||
data class RawGlobalGroup(
|
||||
override val name: String,
|
||||
override val key: Int,
|
||||
override val desc: String?,
|
||||
override val enable: Boolean?,
|
||||
override val actionCd: Long?,
|
||||
override val actionDelay: Long?,
|
||||
override val quickFind: Boolean?,
|
||||
override val matchDelay: Long?,
|
||||
override val matchTime: Long?,
|
||||
override val actionMaximum: Int?,
|
||||
override val resetMatch: String?,
|
||||
override val actionCdKey: Int?,
|
||||
override val actionMaximumKey: Int?,
|
||||
override val snapshotUrls: List<String>?,
|
||||
override val exampleUrls: List<String>?,
|
||||
override val matchAnyApp: Boolean?,
|
||||
override val apps: List<RawGlobalApp>?,
|
||||
override val rules: List<RawGlobalRule>,
|
||||
) : RawGroupProps, RawGlobalRuleProps {
|
||||
|
||||
@IgnoredOnParcel
|
||||
val valid by lazy {
|
||||
rules.all { r ->
|
||||
r.matches.all { s -> Selector.check(s) } && (r.excludeMatches
|
||||
?: emptyList()).all { s ->
|
||||
Selector.check(
|
||||
s
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@IgnoredOnParcel
|
||||
val allExampleUrls by lazy {
|
||||
mutableListOf<String>().apply {
|
||||
if (exampleUrls != null) {
|
||||
addAll(exampleUrls)
|
||||
}
|
||||
rules.forEach { r ->
|
||||
if (r.exampleUrls != null) {
|
||||
addAll(r.exampleUrls)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class RawGlobalRule(
|
||||
override val actionCd: Long?,
|
||||
override val actionDelay: Long?,
|
||||
override val quickFind: Boolean?,
|
||||
override val matchDelay: Long?,
|
||||
override val matchTime: Long?,
|
||||
override val actionMaximum: Int?,
|
||||
override val resetMatch: String?,
|
||||
override val actionCdKey: Int?,
|
||||
override val actionMaximumKey: Int?,
|
||||
override val snapshotUrls: List<String>?,
|
||||
override val exampleUrls: List<String>?,
|
||||
override val name: String?,
|
||||
override val key: Int?,
|
||||
override val preKeys: List<Int>?,
|
||||
override val action: String?,
|
||||
override val matches: List<String>,
|
||||
override val excludeMatches: List<String>?,
|
||||
override val matchAnyApp: Boolean?,
|
||||
override val apps: List<RawGlobalApp>?
|
||||
) : RawRuleProps, RawGlobalRuleProps
|
||||
|
||||
@Serializable
|
||||
data class RawAppGroup(
|
||||
override val name: String,
|
||||
override val key: Int,
|
||||
override val desc: String?,
|
||||
override val enable: Boolean?,
|
||||
override val actionCdKey: Int?,
|
||||
override val actionMaximumKey: Int?,
|
||||
override val actionCd: Long?,
|
||||
override val actionDelay: Long?,
|
||||
override val quickFind: Boolean?,
|
||||
override val actionMaximum: Int?,
|
||||
override val matchDelay: Long?,
|
||||
override val matchTime: Long?,
|
||||
override val resetMatch: String?,
|
||||
override val snapshotUrls: List<String>?,
|
||||
override val exampleUrls: List<String>?,
|
||||
override val activityIds: List<String>?,
|
||||
override val excludeActivityIds: List<String>?,
|
||||
override val rules: List<RawAppRule>,
|
||||
) : RawGroupProps, RawAppRuleProps {
|
||||
|
||||
@IgnoredOnParcel
|
||||
val valid by lazy {
|
||||
rules.all { r ->
|
||||
r.matches.all { s -> Selector.check(s) } && (r.excludeMatches
|
||||
?: emptyList()).all { s ->
|
||||
Selector.check(
|
||||
s
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@IgnoredOnParcel
|
||||
val allExampleUrls by lazy {
|
||||
mutableListOf<String>().apply {
|
||||
if (exampleUrls != null) {
|
||||
addAll(exampleUrls)
|
||||
}
|
||||
rules.forEach { r ->
|
||||
if (r.exampleUrls != null) {
|
||||
addAll(r.exampleUrls)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class RawAppRule(
|
||||
override val name: String?,
|
||||
override val key: Int?,
|
||||
override val preKeys: List<Int>?,
|
||||
override val action: String?,
|
||||
override val matches: List<String>,
|
||||
override val excludeMatches: List<String>?,
|
||||
|
||||
override val actionCdKey: Int?,
|
||||
override val actionMaximumKey: Int?,
|
||||
override val actionCd: Long?,
|
||||
override val actionDelay: Long?,
|
||||
override val quickFind: Boolean?,
|
||||
override val actionMaximum: Int?,
|
||||
override val matchDelay: Long?,
|
||||
override val matchTime: Long?,
|
||||
override val resetMatch: String?,
|
||||
override val snapshotUrls: List<String>?,
|
||||
override val exampleUrls: List<String>?,
|
||||
|
||||
override val activityIds: List<String>?,
|
||||
override val excludeActivityIds: List<String>?,
|
||||
) : RawRuleProps, RawAppRuleProps
|
||||
|
||||
companion object {
|
||||
|
||||
private fun getStringIArray(json: JsonObject? = null, name: String): List<String>? {
|
||||
return when (val element = json?.get(name)) {
|
||||
JsonNull, null -> null
|
||||
is JsonObject -> error("Element ${this::class} can not be object")
|
||||
is JsonArray -> element.map {
|
||||
when (it) {
|
||||
is JsonObject, is JsonArray, JsonNull -> error("Element ${this::class} is not a int")
|
||||
is JsonPrimitive -> it.content
|
||||
}
|
||||
}
|
||||
|
||||
is JsonPrimitive -> listOf(element.content)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("SameParameterValue")
|
||||
private fun getIntIArray(json: JsonObject? = null, name: String): List<Int>? {
|
||||
return when (val element = json?.get(name)) {
|
||||
JsonNull, null -> null
|
||||
is JsonArray -> element.map {
|
||||
when (it) {
|
||||
is JsonObject, is JsonArray, JsonNull -> error("Element $it is not a int")
|
||||
is JsonPrimitive -> it.int
|
||||
}
|
||||
}
|
||||
|
||||
is JsonPrimitive -> listOf(element.int)
|
||||
else -> error("Element $element is not a Array")
|
||||
}
|
||||
}
|
||||
|
||||
private fun getString(json: JsonObject? = null, key: String): String? =
|
||||
when (val p = json?.get(key)) {
|
||||
JsonNull, null -> null
|
||||
is JsonPrimitive -> {
|
||||
if (p.isString) {
|
||||
p.content
|
||||
} else {
|
||||
error("Element $p is not a string")
|
||||
}
|
||||
}
|
||||
|
||||
else -> error("Element $p is not a string")
|
||||
}
|
||||
|
||||
private fun getLong(json: JsonObject? = null, key: String): Long? =
|
||||
when (val p = json?.get(key)) {
|
||||
JsonNull, null -> null
|
||||
is JsonPrimitive -> {
|
||||
p.long
|
||||
}
|
||||
|
||||
else -> error("Element $p is not a long")
|
||||
}
|
||||
|
||||
private fun getInt(json: JsonObject? = null, key: String): Int? =
|
||||
when (val p = json?.get(key)) {
|
||||
JsonNull, null -> null
|
||||
is JsonPrimitive -> {
|
||||
p.int
|
||||
}
|
||||
|
||||
else -> error("Element $p is not a int")
|
||||
}
|
||||
|
||||
@Suppress("SameParameterValue")
|
||||
private fun getBoolean(json: JsonObject? = null, key: String): Boolean? =
|
||||
when (val p = json?.get(key)) {
|
||||
JsonNull, null -> null
|
||||
is JsonPrimitive -> {
|
||||
p.boolean
|
||||
}
|
||||
|
||||
else -> error("Element $p is not a boolean")
|
||||
}
|
||||
|
||||
private fun jsonToRuleRaw(rulesRawJson: JsonElement): RawAppRule {
|
||||
val rulesJson = when (rulesRawJson) {
|
||||
JsonNull -> error("miss current rule")
|
||||
is JsonObject -> rulesRawJson
|
||||
is JsonPrimitive, is JsonArray -> JsonObject(mapOf("matches" to rulesRawJson))
|
||||
}
|
||||
return RawAppRule(
|
||||
activityIds = getStringIArray(rulesJson, "activityIds"),
|
||||
excludeActivityIds = getStringIArray(rulesJson, "excludeActivityIds"),
|
||||
matches = (getStringIArray(
|
||||
rulesJson, "matches"
|
||||
) ?: emptyList()),
|
||||
excludeMatches = getStringIArray(rulesJson, "excludeMatches"),
|
||||
key = getInt(rulesJson, "key"),
|
||||
name = getString(rulesJson, "name"),
|
||||
actionCd = getLong(rulesJson, "actionCd") ?: getLong(rulesJson, "cd"),
|
||||
actionDelay = getLong(rulesJson, "actionDelay") ?: getLong(rulesJson, "delay"),
|
||||
preKeys = getIntIArray(rulesJson, "preKeys"),
|
||||
action = getString(rulesJson, "action"),
|
||||
quickFind = getBoolean(rulesJson, "quickFind"),
|
||||
actionMaximum = getInt(rulesJson, "actionMaximum"),
|
||||
matchDelay = getLong(rulesJson, "matchDelay"),
|
||||
matchTime = getLong(rulesJson, "matchTime"),
|
||||
resetMatch = getString(rulesJson, "resetMatch"),
|
||||
snapshotUrls = getStringIArray(rulesJson, "snapshotUrls"),
|
||||
exampleUrls = getStringIArray(rulesJson, "exampleUrls"),
|
||||
actionMaximumKey = getInt(rulesJson, "actionMaximumKey"),
|
||||
actionCdKey = getInt(rulesJson, "actionCdKey"),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
private fun jsonToGroupRaw(groupRawJson: JsonElement, groupIndex: Int): RawAppGroup {
|
||||
val groupJson = when (groupRawJson) {
|
||||
JsonNull -> error("group must not be null")
|
||||
is JsonObject -> groupRawJson
|
||||
is JsonPrimitive, is JsonArray -> JsonObject(mapOf("rules" to groupRawJson))
|
||||
}
|
||||
return RawAppGroup(
|
||||
activityIds = getStringIArray(groupJson, "activityIds"),
|
||||
excludeActivityIds = getStringIArray(groupJson, "excludeActivityIds"),
|
||||
actionCd = getLong(groupJson, "actionCd") ?: getLong(groupJson, "cd"),
|
||||
actionDelay = getLong(groupJson, "actionDelay") ?: getLong(groupJson, "delay"),
|
||||
name = getString(groupJson, "name") ?: error("miss group name"),
|
||||
desc = getString(groupJson, "desc"),
|
||||
enable = getBoolean(groupJson, "enable"),
|
||||
key = getInt(groupJson, "key") ?: groupIndex,
|
||||
rules = when (val rulesJson = groupJson["rules"]) {
|
||||
null, JsonNull -> emptyList()
|
||||
is JsonPrimitive, is JsonObject -> JsonArray(listOf(rulesJson))
|
||||
is JsonArray -> rulesJson
|
||||
}.map {
|
||||
jsonToRuleRaw(it)
|
||||
},
|
||||
quickFind = getBoolean(groupJson, "quickFind"),
|
||||
actionMaximum = getInt(groupJson, "actionMaximum"),
|
||||
matchDelay = getLong(groupJson, "matchDelay"),
|
||||
matchTime = getLong(groupJson, "matchTime"),
|
||||
resetMatch = getString(groupJson, "resetMatch"),
|
||||
snapshotUrls = getStringIArray(groupJson, "snapshotUrls"),
|
||||
exampleUrls = getStringIArray(groupJson, "exampleUrls"),
|
||||
actionMaximumKey = getInt(groupJson, "actionMaximumKey"),
|
||||
actionCdKey = getInt(groupJson, "actionCdKey"),
|
||||
)
|
||||
}
|
||||
|
||||
private fun jsonToAppRaw(appsJson: JsonObject, appIndex: Int): RawApp {
|
||||
return RawApp(
|
||||
id = getString(appsJson, "id") ?: error("miss subscription.apps[$appIndex].id"),
|
||||
name = getString(appsJson, "name"),
|
||||
groups = (when (val groupsJson = appsJson["groups"]) {
|
||||
null, JsonNull -> emptyList()
|
||||
is JsonPrimitive, is JsonObject -> JsonArray(listOf(groupsJson))
|
||||
is JsonArray -> groupsJson
|
||||
}).mapIndexed { index, jsonElement ->
|
||||
jsonToGroupRaw(jsonElement, index)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
private fun jsonToGlobalApp(jsonObject: JsonObject, index: Int): RawGlobalApp {
|
||||
return RawGlobalApp(
|
||||
id = getString(jsonObject, "id") ?: error("miss apps[$index].id"),
|
||||
enable = getBoolean(jsonObject, "enable"),
|
||||
activityIds = getStringIArray(jsonObject, "activityIds"),
|
||||
excludeActivityIds = getStringIArray(jsonObject, "excludeActivityIds"),
|
||||
)
|
||||
}
|
||||
|
||||
private fun jsonToGlobalRule(jsonObject: JsonObject): RawGlobalRule {
|
||||
return RawGlobalRule(
|
||||
key = getInt(jsonObject, "key"),
|
||||
name = getString(jsonObject, "name"),
|
||||
actionCd = getLong(jsonObject, "actionCd"),
|
||||
actionDelay = getLong(jsonObject, "actionDelay"),
|
||||
quickFind = getBoolean(jsonObject, "quickFind"),
|
||||
actionMaximum = getInt(jsonObject, "actionMaximum"),
|
||||
matchDelay = getLong(jsonObject, "matchDelay"),
|
||||
matchTime = getLong(jsonObject, "matchTime"),
|
||||
resetMatch = getString(jsonObject, "resetMatch"),
|
||||
snapshotUrls = getStringIArray(jsonObject, "snapshotUrls"),
|
||||
exampleUrls = getStringIArray(jsonObject, "exampleUrls"),
|
||||
actionMaximumKey = getInt(jsonObject, "actionMaximumKey"),
|
||||
actionCdKey = getInt(jsonObject, "actionCdKey"),
|
||||
matchAnyApp = getBoolean(jsonObject, "matchAnyApp"),
|
||||
apps = jsonObject["apps"]?.jsonArray?.mapIndexed { index, jsonElement ->
|
||||
jsonToGlobalApp(
|
||||
jsonElement.jsonObject, index
|
||||
)
|
||||
},
|
||||
action = getString(jsonObject, "action"),
|
||||
preKeys = getIntIArray(jsonObject, "preKeys"),
|
||||
excludeMatches = getStringIArray(jsonObject, "excludeMatches"),
|
||||
matches = getStringIArray(jsonObject, "matches") ?: error("miss matches"),
|
||||
)
|
||||
}
|
||||
|
||||
private fun jsonToGlobalGroups(jsonObject: JsonObject, groupIndex: Int): RawGlobalGroup {
|
||||
return RawGlobalGroup(key = getInt(jsonObject, "key")
|
||||
?: error("miss group[$groupIndex].key"),
|
||||
name = getString(jsonObject, "name") ?: error("miss group[$groupIndex].name"),
|
||||
desc = getString(jsonObject, "desc"),
|
||||
enable = getBoolean(jsonObject, "enable"),
|
||||
actionCd = getLong(jsonObject, "actionCd"),
|
||||
actionDelay = getLong(jsonObject, "actionDelay"),
|
||||
quickFind = getBoolean(jsonObject, "quickFind"),
|
||||
actionMaximum = getInt(jsonObject, "actionMaximum"),
|
||||
matchDelay = getLong(jsonObject, "matchDelay"),
|
||||
matchTime = getLong(jsonObject, "matchTime"),
|
||||
resetMatch = getString(jsonObject, "resetMatch"),
|
||||
snapshotUrls = getStringIArray(jsonObject, "snapshotUrls"),
|
||||
exampleUrls = getStringIArray(jsonObject, "exampleUrls"),
|
||||
actionMaximumKey = getInt(jsonObject, "actionMaximumKey"),
|
||||
actionCdKey = getInt(jsonObject, "actionCdKey"),
|
||||
matchAnyApp = getBoolean(jsonObject, "matchAnyApp"),
|
||||
apps = jsonObject["apps"]?.jsonArray?.mapIndexed { index, jsonElement ->
|
||||
jsonToGlobalApp(
|
||||
jsonElement.jsonObject, index
|
||||
)
|
||||
},
|
||||
rules = jsonObject["rules"]?.jsonArray?.map { jsonElement ->
|
||||
jsonToGlobalRule(jsonElement.jsonObject)
|
||||
} ?: emptyList()
|
||||
)
|
||||
}
|
||||
|
||||
private fun jsonToSubscriptionRaw(rootJson: JsonObject): RawSubscription {
|
||||
return RawSubscription(id = getLong(rootJson, "id") ?: error("miss subscription.id"),
|
||||
name = getString(rootJson, "name") ?: error("miss subscription.name"),
|
||||
version = getInt(rootJson, "version") ?: error("miss subscription.version"),
|
||||
author = getString(rootJson, "author"),
|
||||
updateUrl = getString(rootJson, "updateUrl"),
|
||||
supportUri = getString(rootJson, "supportUri"),
|
||||
checkUpdateUrl = getString(rootJson, "checkUpdateUrl"),
|
||||
apps = rootJson["apps"]?.jsonArray?.mapIndexed { index, jsonElement ->
|
||||
jsonToAppRaw(
|
||||
jsonElement.jsonObject, index
|
||||
)
|
||||
} ?: emptyList(),
|
||||
categories = rootJson["categories"]?.jsonArray?.mapIndexed { index, jsonElement ->
|
||||
RawCategory(
|
||||
key = getInt(jsonElement.jsonObject, "key")
|
||||
?: error("miss categories[$index].key"),
|
||||
name = getString(jsonElement.jsonObject, "name")
|
||||
?: error("miss categories[$index].name"),
|
||||
enable = getBoolean(jsonElement.jsonObject, "enable"),
|
||||
)
|
||||
} ?: emptyList(),
|
||||
globalGroups = rootJson["globalGroups"]?.jsonArray?.mapIndexed { index, jsonElement ->
|
||||
jsonToGlobalGroups(jsonElement.jsonObject, index)
|
||||
} ?: emptyList()
|
||||
)
|
||||
}
|
||||
|
||||
private fun <T> List<T>.findDuplicatedItem(predicate: (T) -> Any?): T? {
|
||||
forEach { v ->
|
||||
val key = predicate(v)
|
||||
if (key != null && any { v2 -> v2 !== v && predicate(v2) == key }) {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun parse(source: String, json5: Boolean = true): RawSubscription {
|
||||
val text = if (json5) json5ToJson(source) else source
|
||||
val subscription = jsonToSubscriptionRaw(json.parseToJsonElement(text).jsonObject)
|
||||
subscription.categories.findDuplicatedItem { v -> v.key }?.let { v ->
|
||||
error("duplicated category: key=${v.key}")
|
||||
}
|
||||
subscription.globalGroups.findDuplicatedItem { v -> v.key }?.let { v ->
|
||||
error("duplicated global group: key=${v.key}")
|
||||
}
|
||||
subscription.globalGroups.forEach { g ->
|
||||
g.rules.findDuplicatedItem { v -> v.key }?.let { v ->
|
||||
error("duplicated global rule: key=${v.key}, groupKey=${g.key}")
|
||||
}
|
||||
}
|
||||
subscription.apps.findDuplicatedItem { v -> v.id }?.let { v ->
|
||||
error("duplicated app: ${v.id}")
|
||||
}
|
||||
subscription.apps.forEach { a ->
|
||||
a.groups.findDuplicatedItem { v -> v.key }?.let { v ->
|
||||
error("duplicated app group: key=${v.key}")
|
||||
}
|
||||
a.groups.forEach { g ->
|
||||
g.rules.findDuplicatedItem { v -> v.key }?.let { v ->
|
||||
error("duplicated app rule: key=${v.key}, groupKey=${g.key}, appId=${a.id} ")
|
||||
}
|
||||
}
|
||||
}
|
||||
return subscription
|
||||
}
|
||||
|
||||
fun parseRawApp(source: String, json5: Boolean = true): RawApp {
|
||||
val text = if (json5) json5ToJson(source) else source
|
||||
val a = jsonToAppRaw(json.parseToJsonElement(text).jsonObject, 0)
|
||||
a.groups.findDuplicatedItem { v -> v.key }?.let { v ->
|
||||
error("duplicated app group: key=${v.key}")
|
||||
}
|
||||
a.groups.forEach { g ->
|
||||
g.rules.findDuplicatedItem { v -> v.key }?.let { v ->
|
||||
error("duplicated app rule: key=${v.key}, groupKey=${g.key}")
|
||||
}
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
fun parseRawGroup(source: String, json5: Boolean = true): RawAppGroup {
|
||||
val text = if (json5) json5ToJson(source) else source
|
||||
val g = jsonToGroupRaw(json.parseToJsonElement(text).jsonObject, 0)
|
||||
g.rules.findDuplicatedItem { v -> v.key }?.let { v ->
|
||||
error("duplicated app rule: key=${v.key}")
|
||||
}
|
||||
return g
|
||||
}
|
||||
|
||||
fun parseRawGlobalGroup(source: String, json5: Boolean = true): RawGlobalGroup {
|
||||
val text = if (json5) json5ToJson(source) else source
|
||||
val g = jsonToGlobalGroups(json.parseToJsonElement(text).jsonObject, 0)
|
||||
g.rules.findDuplicatedItem { v -> v.key }?.let { v ->
|
||||
error("duplicated global rule: key=${v.key}")
|
||||
}
|
||||
return g
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
129
app/src/main/kotlin/li/songe/gkd/data/ResolvedRule.kt
Normal file
129
app/src/main/kotlin/li/songe/gkd/data/ResolvedRule.kt
Normal file
|
@ -0,0 +1,129 @@
|
|||
package li.songe.gkd.data
|
||||
|
||||
import android.view.accessibility.AccessibilityNodeInfo
|
||||
import kotlinx.coroutines.Job
|
||||
import li.songe.gkd.service.TopActivity
|
||||
import li.songe.gkd.service.lastTriggerAppRule
|
||||
import li.songe.gkd.service.querySelector
|
||||
import li.songe.selector.Selector
|
||||
|
||||
sealed class ResolvedRule(
|
||||
val matches: List<Selector>,
|
||||
val excludeMatches: List<Selector>,
|
||||
|
||||
val actionDelay: Long,
|
||||
val quickFind: Boolean,
|
||||
|
||||
val matchDelay: Long,
|
||||
val matchTime: Long?,
|
||||
val resetMatch: String?,
|
||||
|
||||
val key: Int?,
|
||||
val preKeys: Set<Int>,
|
||||
|
||||
val index: Int,
|
||||
val rule: RawSubscription.RawRuleProps,
|
||||
val group: RawSubscription.RawGroupProps,
|
||||
val rawSubs: RawSubscription,
|
||||
val subsItem: SubsItem,
|
||||
) {
|
||||
var preAppRules: Set<ResolvedRule> = emptySet()
|
||||
var actionDelayTriggerTime = 0L
|
||||
var actionDelayJob: Job? = null
|
||||
fun checkDelay(): Boolean {
|
||||
if (actionDelay > 0 && actionDelayTriggerTime == 0L) {
|
||||
actionDelayTriggerTime = System.currentTimeMillis()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
val actionCd = (if (rule.actionCdKey != null) {
|
||||
group.rules.find { r -> r.key == rule.actionCdKey }?.actionCd ?: group.actionCd
|
||||
} else {
|
||||
null
|
||||
} ?: rule.actionCd ?: group.actionCd ?: 1000L)
|
||||
|
||||
var actionTriggerTime = Value(0L)
|
||||
fun trigger() {
|
||||
actionTriggerTime.value = System.currentTimeMillis()
|
||||
// 重置延迟点
|
||||
actionDelayTriggerTime = 0L
|
||||
actionCount.value++
|
||||
lastTriggerAppRule = this
|
||||
}
|
||||
|
||||
val actionMaximum = ((if (rule.actionMaximumKey != null) {
|
||||
group.rules.find { r -> r.key == rule.actionMaximumKey }?.actionMaximum
|
||||
?: group.actionMaximum
|
||||
} else {
|
||||
null
|
||||
}) ?: rule.actionMaximum ?: group.actionMaximum)
|
||||
|
||||
var actionCount = Value(0)
|
||||
|
||||
var matchChangedTime = 0L
|
||||
|
||||
val matchLimitTime = (matchTime ?: 0) + matchDelay
|
||||
|
||||
val resetMatchTypeWhenActivity = when (resetMatch) {
|
||||
"app" -> false
|
||||
"activity" -> true
|
||||
else -> true
|
||||
}
|
||||
|
||||
fun query(nodeInfo: AccessibilityNodeInfo?): AccessibilityNodeInfo? {
|
||||
if (nodeInfo == null) return null
|
||||
var target: AccessibilityNodeInfo? = null
|
||||
for (selector in matches) {
|
||||
target = nodeInfo.querySelector(selector, quickFind) ?: return null
|
||||
}
|
||||
for (selector in excludeMatches) {
|
||||
if (nodeInfo.querySelector(selector, quickFind) != null) return null
|
||||
}
|
||||
return target
|
||||
}
|
||||
|
||||
val performAction = getActionFc(rule.action)
|
||||
|
||||
var matchDelayJob: Job? = null
|
||||
|
||||
val statusCode: Int
|
||||
get() {
|
||||
if (actionMaximum != null) {
|
||||
if (actionCount.value >= actionMaximum) {
|
||||
return 1 // 达到最大执行次数
|
||||
}
|
||||
}
|
||||
if (preAppRules.isNotEmpty()) { // 需要提前点击某个规则
|
||||
lastTriggerAppRule ?: return 2
|
||||
return if (preAppRules.any { it === lastTriggerAppRule }) {
|
||||
0
|
||||
} else {
|
||||
3 // 上一个点击的规则不在当前需要点击的列表
|
||||
}
|
||||
}
|
||||
val t = System.currentTimeMillis()
|
||||
if (matchDelay > 0 && t - matchChangedTime < matchDelay) {
|
||||
return 4 // 处于匹配延迟中
|
||||
}
|
||||
if (matchTime != null && t - matchChangedTime > matchLimitTime) {
|
||||
return 5 // 超出匹配时间
|
||||
}
|
||||
if (actionTriggerTime.value + actionCd > t) {
|
||||
return 6 // 处于冷却时间
|
||||
}
|
||||
if (actionDelayTriggerTime > 0) {
|
||||
if (actionDelayTriggerTime + actionDelay > t) {
|
||||
return 7 // 处于点击延迟中
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
abstract fun matchActivity(topActivity: TopActivity?): Boolean
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -1,243 +0,0 @@
|
|||
package li.songe.gkd.data
|
||||
|
||||
import android.accessibilityservice.AccessibilityService
|
||||
import android.accessibilityservice.GestureDescription
|
||||
import android.graphics.Path
|
||||
import android.graphics.Rect
|
||||
import android.view.ViewConfiguration
|
||||
import android.view.accessibility.AccessibilityNodeInfo
|
||||
import com.blankj.utilcode.util.LogUtils
|
||||
import com.blankj.utilcode.util.ScreenUtils
|
||||
import kotlinx.serialization.Serializable
|
||||
import li.songe.gkd.service.lastTriggerRule
|
||||
import li.songe.gkd.service.openAdOptimized
|
||||
import li.songe.gkd.service.querySelector
|
||||
import li.songe.selector.Selector
|
||||
|
||||
class Value<T>(var value: T)
|
||||
|
||||
data class Rule(
|
||||
/**
|
||||
* length>0
|
||||
*/
|
||||
val matches: List<Selector> = emptyList(),
|
||||
val excludeMatches: List<Selector> = emptyList(),
|
||||
|
||||
val actionDelay: Long = 0,
|
||||
val quickFind: Boolean = false,
|
||||
|
||||
val matchDelay: Long?,
|
||||
val matchTime: Long?,
|
||||
val resetMatch: String?,
|
||||
|
||||
val appId: String,
|
||||
val activityIds: Set<String> = emptySet(),
|
||||
val excludeActivityIds: Set<String> = emptySet(),
|
||||
|
||||
val key: Int? = null,
|
||||
val preKeys: Set<Int> = emptySet(),
|
||||
|
||||
val index: Int = 0,
|
||||
val rule: SubscriptionRaw.RuleRaw,
|
||||
val group: SubscriptionRaw.GroupRaw,
|
||||
val app: SubscriptionRaw.AppRaw,
|
||||
val subsItem: SubsItem,
|
||||
) {
|
||||
|
||||
/**
|
||||
* 优化: 切换 APP 后短时间内, 如果存在开屏广告的规则并且没有一次触发, 则尽量使开屏广告运行
|
||||
*/
|
||||
val isOpenAd = group.name.startsWith("开屏广告")
|
||||
|
||||
/**
|
||||
* 任意一个元素是上次点击过的
|
||||
*/
|
||||
var preRules: Set<Rule> = emptySet()
|
||||
|
||||
var actionDelayTriggerTime = 0L
|
||||
fun triggerDelay() {
|
||||
// 触发延迟, 一段时间内此规则不可利用
|
||||
actionDelayTriggerTime = System.currentTimeMillis()
|
||||
LogUtils.d(
|
||||
"触发延迟",
|
||||
"subsId:${subsItem.id}, gKey=${group.key}, gName:${group.name}, ruleIndex:${index}, rKey:${key}, delay:${actionDelay}"
|
||||
)
|
||||
}
|
||||
|
||||
val actionCd = defaultMiniCd.coerceAtLeast(
|
||||
((if (rule.actionCdKey != null) {
|
||||
group.rules.find { r -> r.key == rule.actionCdKey }?.actionCd ?: group.actionCd
|
||||
?: app.actionCd
|
||||
} else {
|
||||
null
|
||||
}) ?: rule.actionCd ?: group.actionCd ?: app.actionCd ?: defaultMiniCd)
|
||||
)
|
||||
var actionTriggerTime = Value(0L)
|
||||
fun trigger() {
|
||||
actionTriggerTime.value = System.currentTimeMillis()
|
||||
// 重置延迟点
|
||||
actionDelayTriggerTime = 0L
|
||||
actionCount.value++
|
||||
lastTriggerRule = this
|
||||
if (isOpenAd && openAdOptimized == true) {
|
||||
openAdOptimized = false
|
||||
}
|
||||
}
|
||||
|
||||
val actionMaximum = ((if (rule.actionMaximumKey != null) {
|
||||
group.rules.find { r -> r.key == rule.actionMaximumKey }?.actionMaximum
|
||||
?: group.actionMaximum ?: app.actionMaximum
|
||||
} else {
|
||||
null
|
||||
}) ?: rule.actionMaximum ?: group.actionMaximum ?: app.actionMaximum)
|
||||
|
||||
var actionCount = Value(0)
|
||||
|
||||
var matchChangeTime = 0L
|
||||
|
||||
val matchAllTime = (matchTime ?: 0) + (matchDelay ?: 0)
|
||||
|
||||
val resetMatchTypeWhenActivity = when (resetMatch) {
|
||||
"app" -> false
|
||||
"activity" -> true
|
||||
else -> true
|
||||
}
|
||||
|
||||
|
||||
fun query(nodeInfo: AccessibilityNodeInfo?): AccessibilityNodeInfo? {
|
||||
if (nodeInfo == null) return null
|
||||
var target: AccessibilityNodeInfo? = null
|
||||
for (selector in matches) {
|
||||
target = nodeInfo.querySelector(selector, quickFind) ?: return null
|
||||
}
|
||||
for (selector in excludeMatches) {
|
||||
if (nodeInfo.querySelector(selector, quickFind) != null) return null
|
||||
}
|
||||
return target
|
||||
}
|
||||
|
||||
private val matchAnyActivity = activityIds.isEmpty() && excludeActivityIds.isEmpty()
|
||||
|
||||
fun matchActivityId(activityId: String?): Boolean {
|
||||
if (matchAnyActivity || activityId == null) return true
|
||||
if (excludeActivityIds.any { activityId.startsWith(it) }) return false
|
||||
if (activityIds.isEmpty()) return true
|
||||
|
||||
return activityIds.any { activityId.startsWith(it) }
|
||||
}
|
||||
|
||||
val performAction = getActionFc(rule.action)
|
||||
|
||||
companion object {
|
||||
const val defaultMiniCd = 1000L
|
||||
}
|
||||
}
|
||||
|
||||
typealias ActionFc = (context: AccessibilityService, node: AccessibilityNodeInfo) -> ActionResult
|
||||
|
||||
|
||||
@Serializable
|
||||
data class GkdAction(
|
||||
val selector: String,
|
||||
val quickFind: Boolean = false,
|
||||
val action: String? = null,
|
||||
)
|
||||
|
||||
|
||||
@Serializable
|
||||
data class ActionResult(
|
||||
val action: String,
|
||||
val result: Boolean,
|
||||
)
|
||||
|
||||
val clickNode: ActionFc = { _, node ->
|
||||
ActionResult(
|
||||
action = "clickNode", result = node.performAction(AccessibilityNodeInfo.ACTION_CLICK)
|
||||
)
|
||||
}
|
||||
|
||||
val clickCenter: ActionFc = { context, node ->
|
||||
val react = Rect()
|
||||
node.getBoundsInScreen(react)
|
||||
val x = (react.right + react.left) / 2f
|
||||
val y = (react.bottom + react.top) / 2f
|
||||
ActionResult(
|
||||
action = "clickCenter",
|
||||
result = if (0 <= x && 0 <= y && x <= ScreenUtils.getScreenWidth() && y <= ScreenUtils.getScreenHeight()) {
|
||||
val gestureDescription = GestureDescription.Builder()
|
||||
val path = Path()
|
||||
path.moveTo(x, y)
|
||||
gestureDescription.addStroke(
|
||||
GestureDescription.StrokeDescription(
|
||||
path, 0, ViewConfiguration.getTapTimeout().toLong()
|
||||
)
|
||||
)
|
||||
context.dispatchGesture(gestureDescription.build(), null, null)
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
val click: ActionFc = { context, node ->
|
||||
if (node.isClickable) clickNode(context, node) else clickCenter(context, node)
|
||||
}
|
||||
|
||||
val longClickNode: ActionFc = { _, node ->
|
||||
ActionResult(
|
||||
action = "longClickNode",
|
||||
result = node.performAction(AccessibilityNodeInfo.ACTION_LONG_CLICK)
|
||||
)
|
||||
}
|
||||
|
||||
val longClickCenter: ActionFc = { context, node ->
|
||||
val react = Rect()
|
||||
node.getBoundsInScreen(react)
|
||||
val x = (react.right + react.left) / 2f
|
||||
val y = (react.bottom + react.top) / 2f
|
||||
// 内部的 DEFAULT_LONG_PRESS_TIMEOUT 常量是 400
|
||||
// 而 ViewConfiguration.getLongPressTimeout() 返回 300, 这将导致触发普通的 click 事件
|
||||
ActionResult(
|
||||
action = "longClickCenter",
|
||||
result = if (0 <= x && 0 <= y && x <= ScreenUtils.getScreenWidth() && y <= ScreenUtils.getScreenHeight()) {
|
||||
val gestureDescription = GestureDescription.Builder()
|
||||
val path = Path()
|
||||
path.moveTo(x, y)
|
||||
gestureDescription.addStroke(
|
||||
GestureDescription.StrokeDescription(
|
||||
path, 0, 400L
|
||||
)
|
||||
)
|
||||
// TODO 传入处理 callback
|
||||
context.dispatchGesture(gestureDescription.build(), null, null)
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
val longClick: ActionFc = { context, node ->
|
||||
if (node.isLongClickable) longClickNode(context, node) else longClickCenter(context, node)
|
||||
}
|
||||
|
||||
val backFc: ActionFc = { context, _ ->
|
||||
ActionResult(
|
||||
action = "back",
|
||||
result = context.performGlobalAction(AccessibilityService.GLOBAL_ACTION_BACK)
|
||||
)
|
||||
}
|
||||
|
||||
fun getActionFc(action: String?): ActionFc {
|
||||
return when (action) {
|
||||
"clickNode" -> clickNode
|
||||
"clickCenter" -> clickCenter
|
||||
"back" -> backFc
|
||||
"longClick" -> longClick
|
||||
"longClickNode" -> longClickNode
|
||||
"longClickCenter" -> longClickCenter
|
||||
else -> click
|
||||
}
|
||||
}
|
|
@ -29,7 +29,8 @@ data class SubsConfig(
|
|||
|
||||
companion object {
|
||||
const val AppType = 1
|
||||
const val GroupType = 2
|
||||
const val AppGroupType = 2
|
||||
const val GlobalGroupType = 3
|
||||
}
|
||||
|
||||
@Dao
|
||||
|
@ -57,14 +58,17 @@ data class SubsConfig(
|
|||
fun query(): Flow<List<SubsConfig>>
|
||||
|
||||
|
||||
@Query("SELECT * FROM subs_config WHERE type=${AppType} and subs_item_id=:subsItemId")
|
||||
@Query("SELECT * FROM subs_config WHERE type=${AppType} AND subs_item_id=:subsItemId")
|
||||
fun queryAppTypeConfig(subsItemId: Long): Flow<List<SubsConfig>>
|
||||
|
||||
@Query("SELECT * FROM subs_config WHERE type=${GroupType} and subs_item_id=:subsItemId")
|
||||
@Query("SELECT * FROM subs_config WHERE type=${AppGroupType} AND subs_item_id=:subsItemId")
|
||||
fun querySubsGroupTypeConfig(subsItemId: Long): Flow<List<SubsConfig>>
|
||||
|
||||
@Query("SELECT * FROM subs_config WHERE type=${GroupType} and subs_item_id=:subsItemId and app_id=:appId")
|
||||
fun queryGroupTypeConfig(subsItemId: Long, appId: String): Flow<List<SubsConfig>>
|
||||
@Query("SELECT * FROM subs_config WHERE type=${AppGroupType} AND subs_item_id=:subsItemId AND app_id=:appId")
|
||||
fun queryAppGroupTypeConfig(subsItemId: Long, appId: String): Flow<List<SubsConfig>>
|
||||
|
||||
@Query("SELECT * FROM subs_config WHERE type=${GlobalGroupType} AND subs_item_id=:subsItemId")
|
||||
fun queryGlobalGroupTypeConfig(subsItemId: Long): Flow<List<SubsConfig>>
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -9,12 +9,9 @@ import androidx.room.OnConflictStrategy
|
|||
import androidx.room.PrimaryKey
|
||||
import androidx.room.Query
|
||||
import androidx.room.Update
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.withContext
|
||||
import li.songe.gkd.db.DbSet
|
||||
import li.songe.gkd.util.subsFolder
|
||||
import java.io.File
|
||||
import li.songe.gkd.util.deleteSubscription
|
||||
|
||||
@Entity(
|
||||
tableName = "subs_item",
|
||||
|
@ -31,37 +28,14 @@ data class SubsItem(
|
|||
|
||||
) {
|
||||
|
||||
val subsFile by lazy {
|
||||
File(subsFolder.absolutePath.plus("/${id}.json"))
|
||||
}
|
||||
|
||||
suspend fun removeAssets() {
|
||||
withContext(IO) {
|
||||
subsFile.exists() && subsFile.delete()
|
||||
}
|
||||
deleteSubscription(id)
|
||||
DbSet.subsItemDao.delete(this)
|
||||
DbSet.subsConfigDao.delete(id)
|
||||
DbSet.clickLogDao.deleteBySubsId(id)
|
||||
DbSet.categoryConfigDao.deleteBySubsItemId(id)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
fun getSubscriptionRaw(subsItemId: Long): SubscriptionRaw? {
|
||||
return try {
|
||||
val file = File(subsFolder.absolutePath.plus("/${subsItemId}.json"))
|
||||
if (!file.exists()) {
|
||||
return null
|
||||
}
|
||||
return SubscriptionRaw.parse(file.readText())
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Dao
|
||||
interface SubsItemDao {
|
||||
@Update
|
||||
|
@ -74,7 +48,7 @@ data class SubsItem(
|
|||
suspend fun delete(vararg users: SubsItem): Int
|
||||
|
||||
@Query("UPDATE subs_item SET mtime=:mtime WHERE id=:id")
|
||||
suspend fun updateMtime(id: Long, mtime: Long): Int
|
||||
suspend fun updateMtime(id: Long, mtime: Long = System.currentTimeMillis()): Int
|
||||
|
||||
@Query("SELECT * FROM subs_item ORDER BY `order`")
|
||||
fun query(): Flow<List<SubsItem>>
|
||||
|
|
|
@ -1,406 +0,0 @@
|
|||
package li.songe.gkd.data
|
||||
|
||||
import blue.endless.jankson.Jankson
|
||||
import kotlinx.parcelize.IgnoredOnParcel
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.JsonArray
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
import kotlinx.serialization.json.JsonNull
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
import kotlinx.serialization.json.boolean
|
||||
import kotlinx.serialization.json.int
|
||||
import kotlinx.serialization.json.jsonArray
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.long
|
||||
import li.songe.gkd.util.json
|
||||
import li.songe.selector.Selector
|
||||
|
||||
|
||||
@Serializable
|
||||
data class SubscriptionRaw(
|
||||
val id: Long,
|
||||
val name: String,
|
||||
val version: Int,
|
||||
val author: String? = null,
|
||||
val updateUrl: String? = null,
|
||||
val supportUri: String? = null,
|
||||
val checkUpdateUrl: String? = null,
|
||||
val apps: List<AppRaw> = emptyList(),
|
||||
val categories: List<Category> = emptyList(),
|
||||
) {
|
||||
|
||||
@IgnoredOnParcel
|
||||
val categoriesGroups by lazy {
|
||||
val allAppGroups =
|
||||
apps.flatMap { a -> a.groups.map { g -> g to a } }
|
||||
allAppGroups.groupBy { g ->
|
||||
categories.find { c -> g.first.name.startsWith(c.name) }
|
||||
}
|
||||
}
|
||||
|
||||
@IgnoredOnParcel
|
||||
val groupToCategoryMap by lazy {
|
||||
val map = mutableMapOf<GroupRaw, Category>()
|
||||
categoriesGroups.forEach { (key, value) ->
|
||||
value.forEach { (g) ->
|
||||
if (key != null) {
|
||||
map[g] = key
|
||||
}
|
||||
}
|
||||
}
|
||||
map
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class Category(val key: Int, val name: String, val enable: Boolean?)
|
||||
|
||||
interface CommonProps {
|
||||
val activityIds: List<String>?
|
||||
val excludeActivityIds: List<String>?
|
||||
val actionCd: Long?
|
||||
val actionDelay: Long?
|
||||
val quickFind: Boolean?
|
||||
val matchDelay: Long?
|
||||
val matchTime: Long?
|
||||
val actionMaximum: Int?
|
||||
val resetMatch: String?
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class AppRaw(
|
||||
val id: String,
|
||||
val name: String?,
|
||||
override val actionCd: Long?,
|
||||
override val actionDelay: Long?,
|
||||
override val quickFind: Boolean?,
|
||||
override val actionMaximum: Int?,
|
||||
override val matchDelay: Long?,
|
||||
override val matchTime: Long?,
|
||||
override val resetMatch: String?,
|
||||
override val activityIds: List<String>?,
|
||||
override val excludeActivityIds: List<String>?,
|
||||
val groups: List<GroupRaw> = emptyList(),
|
||||
) : CommonProps
|
||||
|
||||
@Serializable
|
||||
data class GroupRaw(
|
||||
val name: String,
|
||||
val key: Int,
|
||||
val desc: String?,
|
||||
val enable: Boolean?,
|
||||
override val actionCd: Long?,
|
||||
override val actionDelay: Long?,
|
||||
override val quickFind: Boolean?,
|
||||
override val actionMaximum: Int?,
|
||||
override val matchDelay: Long?,
|
||||
override val matchTime: Long?,
|
||||
override val resetMatch: String?,
|
||||
override val activityIds: List<String>?,
|
||||
override val excludeActivityIds: List<String>?,
|
||||
val rules: List<RuleRaw>,
|
||||
val snapshotUrls: List<String>?,
|
||||
val exampleUrls: List<String>?,
|
||||
) : CommonProps {
|
||||
|
||||
@IgnoredOnParcel
|
||||
val valid by lazy {
|
||||
rules.all { r ->
|
||||
r.matches.all { s -> Selector.check(s) } && (r.excludeMatches
|
||||
?: emptyList()).all { s ->
|
||||
Selector.check(
|
||||
s
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@IgnoredOnParcel
|
||||
val allExampleUrls by lazy {
|
||||
mutableListOf<String>().apply {
|
||||
if (exampleUrls != null) {
|
||||
addAll(exampleUrls)
|
||||
}
|
||||
rules.forEach { r ->
|
||||
if (r.exampleUrls != null) {
|
||||
addAll(r.exampleUrls)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class RuleRaw(
|
||||
val name: String?,
|
||||
val key: Int?,
|
||||
val preKeys: List<Int>?,
|
||||
val action: String?,
|
||||
val actionCdKey: Int?,
|
||||
val actionMaximumKey: Int?,
|
||||
override val actionCd: Long?,
|
||||
override val actionDelay: Long?,
|
||||
override val quickFind: Boolean?,
|
||||
override val actionMaximum: Int?,
|
||||
override val matchDelay: Long?,
|
||||
override val matchTime: Long?,
|
||||
override val resetMatch: String?,
|
||||
override val activityIds: List<String>?,
|
||||
override val excludeActivityIds: List<String>?,
|
||||
val matches: List<String>,
|
||||
val excludeMatches: List<String>?,
|
||||
val snapshotUrls: List<String>?,
|
||||
val exampleUrls: List<String>?,
|
||||
) : CommonProps
|
||||
|
||||
companion object {
|
||||
|
||||
|
||||
private fun getStringIArray(json: JsonObject? = null, name: String): List<String>? {
|
||||
return when (val element = json?.get(name)) {
|
||||
JsonNull, null -> null
|
||||
is JsonObject -> error("Element ${this::class} can not be object")
|
||||
is JsonArray -> element.map {
|
||||
when (it) {
|
||||
is JsonObject, is JsonArray, JsonNull -> error("Element ${this::class} is not a int")
|
||||
is JsonPrimitive -> it.content
|
||||
}
|
||||
}
|
||||
|
||||
is JsonPrimitive -> listOf(element.content)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("SameParameterValue")
|
||||
private fun getIntIArray(json: JsonObject? = null, name: String): List<Int>? {
|
||||
return when (val element = json?.get(name)) {
|
||||
JsonNull, null -> null
|
||||
is JsonArray -> element.map {
|
||||
when (it) {
|
||||
is JsonObject, is JsonArray, JsonNull -> error("Element $it is not a int")
|
||||
is JsonPrimitive -> it.int
|
||||
}
|
||||
}
|
||||
|
||||
is JsonPrimitive -> listOf(element.int)
|
||||
else -> error("Element $element is not a Array")
|
||||
}
|
||||
}
|
||||
|
||||
private fun getString(json: JsonObject? = null, key: String): String? =
|
||||
when (val p = json?.get(key)) {
|
||||
JsonNull, null -> null
|
||||
is JsonPrimitive -> {
|
||||
if (p.isString) {
|
||||
p.content
|
||||
} else {
|
||||
error("Element $p is not a string")
|
||||
}
|
||||
}
|
||||
|
||||
else -> error("Element $p is not a string")
|
||||
}
|
||||
|
||||
private fun getLong(json: JsonObject? = null, key: String): Long? =
|
||||
when (val p = json?.get(key)) {
|
||||
JsonNull, null -> null
|
||||
is JsonPrimitive -> {
|
||||
p.long
|
||||
}
|
||||
|
||||
else -> error("Element $p is not a long")
|
||||
}
|
||||
|
||||
private fun getInt(json: JsonObject? = null, key: String): Int? =
|
||||
when (val p = json?.get(key)) {
|
||||
JsonNull, null -> null
|
||||
is JsonPrimitive -> {
|
||||
p.int
|
||||
}
|
||||
|
||||
else -> error("Element $p is not a int")
|
||||
}
|
||||
|
||||
@Suppress("SameParameterValue")
|
||||
private fun getBoolean(json: JsonObject? = null, key: String): Boolean? =
|
||||
when (val p = json?.get(key)) {
|
||||
JsonNull, null -> null
|
||||
is JsonPrimitive -> {
|
||||
p.boolean
|
||||
}
|
||||
|
||||
else -> error("Element $p is not a boolean")
|
||||
}
|
||||
|
||||
private fun jsonToRuleRaw(rulesRawJson: JsonElement): RuleRaw {
|
||||
val rulesJson = when (rulesRawJson) {
|
||||
JsonNull -> error("miss current rule")
|
||||
is JsonObject -> rulesRawJson
|
||||
is JsonPrimitive, is JsonArray -> JsonObject(mapOf("matches" to rulesRawJson))
|
||||
}
|
||||
return RuleRaw(
|
||||
activityIds = getStringIArray(rulesJson, "activityIds"),
|
||||
excludeActivityIds = getStringIArray(rulesJson, "excludeActivityIds"),
|
||||
actionCd = getLong(rulesJson, "actionCd") ?: getLong(rulesJson, "cd"),
|
||||
actionDelay = getLong(rulesJson, "actionDelay") ?: getLong(rulesJson, "delay"),
|
||||
matches = (getStringIArray(
|
||||
rulesJson, "matches"
|
||||
) ?: emptyList()),
|
||||
excludeMatches = getStringIArray(rulesJson, "excludeMatches"),
|
||||
key = getInt(rulesJson, "key"),
|
||||
name = getString(rulesJson, "name"),
|
||||
preKeys = getIntIArray(rulesJson, "preKeys"),
|
||||
action = getString(rulesJson, "action"),
|
||||
quickFind = getBoolean(rulesJson, "quickFind"),
|
||||
actionMaximum = getInt(rulesJson, "actionMaximum"),
|
||||
matchDelay = getLong(rulesJson, "matchDelay"),
|
||||
matchTime = getLong(rulesJson, "matchTime"),
|
||||
resetMatch = getString(rulesJson, "resetMatch"),
|
||||
snapshotUrls = getStringIArray(rulesJson, "snapshotUrls"),
|
||||
exampleUrls = getStringIArray(rulesJson, "exampleUrls"),
|
||||
actionMaximumKey = getInt(rulesJson, "actionMaximumKey"),
|
||||
actionCdKey = getInt(rulesJson, "actionCdKey"),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
private fun jsonToGroupRaw(groupsRawJson: JsonElement, groupIndex: Int): GroupRaw {
|
||||
val groupsJson = when (groupsRawJson) {
|
||||
JsonNull -> error("")
|
||||
is JsonObject -> groupsRawJson
|
||||
is JsonPrimitive, is JsonArray -> JsonObject(mapOf("rules" to groupsRawJson))
|
||||
}
|
||||
return GroupRaw(
|
||||
activityIds = getStringIArray(groupsJson, "activityIds"),
|
||||
excludeActivityIds = getStringIArray(groupsJson, "excludeActivityIds"),
|
||||
actionCd = getLong(groupsJson, "actionCd") ?: getLong(groupsJson, "cd"),
|
||||
actionDelay = getLong(groupsJson, "actionDelay") ?: getLong(groupsJson, "delay"),
|
||||
name = getString(groupsJson, "name") ?: error("miss group name"),
|
||||
desc = getString(groupsJson, "desc"),
|
||||
enable = getBoolean(groupsJson, "enable"),
|
||||
key = getInt(groupsJson, "key") ?: groupIndex,
|
||||
rules = when (val rulesJson = groupsJson["rules"]) {
|
||||
null, JsonNull -> emptyList()
|
||||
is JsonPrimitive, is JsonObject -> JsonArray(listOf(rulesJson))
|
||||
is JsonArray -> rulesJson
|
||||
}.map {
|
||||
jsonToRuleRaw(it)
|
||||
},
|
||||
quickFind = getBoolean(groupsJson, "quickFind"),
|
||||
actionMaximum = getInt(groupsJson, "actionMaximum"),
|
||||
matchDelay = getLong(groupsJson, "matchDelay"),
|
||||
matchTime = getLong(groupsJson, "matchTime"),
|
||||
resetMatch = getString(groupsJson, "resetMatch"),
|
||||
snapshotUrls = getStringIArray(groupsJson, "snapshotUrls"),
|
||||
exampleUrls = getStringIArray(groupsJson, "exampleUrls"),
|
||||
)
|
||||
}
|
||||
|
||||
private fun jsonToAppRaw(appsJson: JsonObject, appIndex: Int): AppRaw {
|
||||
return AppRaw(
|
||||
activityIds = getStringIArray(appsJson, "activityIds"),
|
||||
excludeActivityIds = getStringIArray(appsJson, "excludeActivityIds"),
|
||||
actionCd = getLong(appsJson, "actionCd") ?: getLong(appsJson, "cd"),
|
||||
actionDelay = getLong(appsJson, "actionDelay") ?: getLong(appsJson, "delay"),
|
||||
id = getString(appsJson, "id") ?: error("miss subscription.apps[$appIndex].id"),
|
||||
name = getString(appsJson, "name"),
|
||||
groups = (when (val groupsJson = appsJson["groups"]) {
|
||||
null, JsonNull -> emptyList()
|
||||
is JsonPrimitive, is JsonObject -> JsonArray(listOf(groupsJson))
|
||||
is JsonArray -> groupsJson
|
||||
}).mapIndexed { index, jsonElement ->
|
||||
jsonToGroupRaw(jsonElement, index)
|
||||
},
|
||||
quickFind = getBoolean(appsJson, "quickFind"),
|
||||
actionMaximum = getInt(appsJson, "actionMaximum"),
|
||||
matchDelay = getLong(appsJson, "matchDelay"),
|
||||
matchTime = getLong(appsJson, "matchTime"),
|
||||
resetMatch = getString(appsJson, "resetMatch")
|
||||
)
|
||||
}
|
||||
|
||||
private fun jsonToSubscriptionRaw(rootJson: JsonObject): SubscriptionRaw {
|
||||
return SubscriptionRaw(id = getLong(rootJson, "id") ?: error("miss subscription.id"),
|
||||
name = getString(rootJson, "name") ?: error("miss subscription.name"),
|
||||
version = getInt(rootJson, "version") ?: error("miss subscription.version"),
|
||||
author = getString(rootJson, "author"),
|
||||
updateUrl = getString(rootJson, "updateUrl"),
|
||||
supportUri = getString(rootJson, "supportUri"),
|
||||
checkUpdateUrl = getString(rootJson, "checkUpdateUrl"),
|
||||
apps = rootJson["apps"]?.jsonArray?.mapIndexed { index, jsonElement ->
|
||||
jsonToAppRaw(
|
||||
jsonElement.jsonObject, index
|
||||
)
|
||||
} ?: emptyList(),
|
||||
categories = rootJson["categories"]?.jsonArray?.mapIndexed { index, jsonElement ->
|
||||
Category(
|
||||
key = getInt(jsonElement.jsonObject, "key")
|
||||
?: error("miss categories[$index].key"),
|
||||
name = getString(jsonElement.jsonObject, "name")
|
||||
?: error("miss categories[$index].name"),
|
||||
enable = getBoolean(jsonElement.jsonObject, "enable"),
|
||||
)
|
||||
} ?: emptyList()
|
||||
)
|
||||
}
|
||||
|
||||
// 订阅文件状态: 文件不存在, 文件正常, 文件损坏(损坏原因)
|
||||
fun stringify(source: SubscriptionRaw) = json.encodeToString(source)
|
||||
|
||||
fun parse(source: String, json5: Boolean = true): SubscriptionRaw {
|
||||
val text = if (json5) Jankson.builder().build().load(source).toJson() else source
|
||||
val obj = jsonToSubscriptionRaw(json.parseToJsonElement(text).jsonObject)
|
||||
// 校验 category 不重复
|
||||
obj.categories.forEach { c ->
|
||||
if (obj.categories.find { c2 -> c2 !== c && c2.key == c.key } != null) {
|
||||
error("duplicated category: key:${c.key} ")
|
||||
}
|
||||
}
|
||||
// 校验 appId 不重复
|
||||
val duplicatedApps = obj.apps.groupingBy { it }.eachCount().filter { it.value > 1 }.keys
|
||||
if (duplicatedApps.isNotEmpty()) {
|
||||
error("duplicated app: ${duplicatedApps.map { it.id }}")
|
||||
}
|
||||
obj.apps.forEach { appRaw ->
|
||||
// 校验 group key 不重复
|
||||
val duplicatedGroups =
|
||||
appRaw.groups.groupingBy { it }.eachCount().filter { it.value > 1 }.keys
|
||||
if (duplicatedGroups.isNotEmpty()) {
|
||||
error("app:${appRaw.id}, duplicated group: ${duplicatedGroups.map { it.key }}")
|
||||
}
|
||||
// 校验 rule key 不重复
|
||||
appRaw.groups.forEach { groupRaw ->
|
||||
val duplicatedRules =
|
||||
groupRaw.rules.mapNotNull { r -> r.key }.groupingBy { it }.eachCount()
|
||||
.filter { it.value > 1 }.keys
|
||||
if (duplicatedRules.isNotEmpty()) {
|
||||
error("app:${appRaw.id}, group:${groupRaw.key}, duplicated rule: $duplicatedRules")
|
||||
}
|
||||
}
|
||||
}
|
||||
return obj
|
||||
}
|
||||
|
||||
fun parseAppRaw(source: String, json5: Boolean = true): AppRaw {
|
||||
val text = if (json5) Jankson.builder().build().load(source).toJson() else source
|
||||
return jsonToAppRaw(json.parseToJsonElement(text).jsonObject, 0)
|
||||
}
|
||||
|
||||
fun parseGroupRaw(source: String, json5: Boolean = true): GroupRaw {
|
||||
val text = if (json5) Jankson.builder().build().load(source).toJson() else source
|
||||
return jsonToGroupRaw(json.parseToJsonElement(text).jsonObject, 0)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
3
app/src/main/kotlin/li/songe/gkd/data/Value.kt
Normal file
3
app/src/main/kotlin/li/songe/gkd/data/Value.kt
Normal file
|
@ -0,0 +1,3 @@
|
|||
package li.songe.gkd.data
|
||||
|
||||
class Value<T>(var value: T)
|
|
@ -10,9 +10,13 @@ import li.songe.gkd.data.SubsConfig
|
|||
import li.songe.gkd.data.SubsItem
|
||||
|
||||
@Database(
|
||||
version = 3,
|
||||
version = 4,
|
||||
entities = [SubsItem::class, Snapshot::class, SubsConfig::class, ClickLog::class, CategoryConfig::class],
|
||||
autoMigrations = [AutoMigration(from = 1, to = 2), AutoMigration(from = 2, to = 3)]
|
||||
autoMigrations = [
|
||||
AutoMigration(from = 1, to = 2),
|
||||
AutoMigration(from = 2, to = 3),
|
||||
AutoMigration(from = 3, to = 4),
|
||||
]
|
||||
)
|
||||
abstract class AppDb : RoomDatabase() {
|
||||
abstract fun subsItemDao(): SubsItem.SubsItemDao
|
||||
|
|
|
@ -12,12 +12,13 @@ import kotlinx.coroutines.flow.firstOrNull
|
|||
import kotlinx.coroutines.withTimeout
|
||||
import li.songe.gkd.app
|
||||
import li.songe.gkd.appScope
|
||||
import li.songe.gkd.data.RawSubscription
|
||||
import li.songe.gkd.data.SubsItem
|
||||
import li.songe.gkd.data.SubscriptionRaw
|
||||
import li.songe.gkd.util.DEFAULT_SUBS_UPDATE_URL
|
||||
import li.songe.gkd.util.client
|
||||
import li.songe.gkd.util.dbFolder
|
||||
import li.songe.gkd.util.launchTry
|
||||
import li.songe.gkd.util.updateSubscription
|
||||
import java.io.File
|
||||
|
||||
object DbSet {
|
||||
|
@ -50,7 +51,7 @@ object DbSet {
|
|||
subsItemDao.insert(defaultSubsItem)
|
||||
val newSubsRaw = try {
|
||||
withTimeout(3000) {
|
||||
SubscriptionRaw.parse(
|
||||
RawSubscription.parse(
|
||||
client.get(defaultSubsItem.updateUrl!!).bodyAsText()
|
||||
)
|
||||
}
|
||||
|
@ -59,7 +60,7 @@ object DbSet {
|
|||
LogUtils.d(e)
|
||||
return@launchTry
|
||||
}
|
||||
defaultSubsItem.subsFile.writeText(SubscriptionRaw.stringify(newSubsRaw))
|
||||
updateSubscription(newSubsRaw)
|
||||
subsItemDao.update(defaultSubsItem.copy(mtime = System.currentTimeMillis()))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,11 +9,11 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.blankj.utilcode.util.ServiceUtils
|
||||
import com.blankj.utilcode.util.ToastUtils
|
||||
import com.torrydo.floatingbubbleview.FloatingBubbleListener
|
||||
import com.torrydo.floatingbubbleview.service.expandable.BubbleBuilder
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import li.songe.gkd.app
|
||||
import li.songe.gkd.appScope
|
||||
import li.songe.gkd.composition.CompositionExt.useLifeCycleLog
|
||||
|
@ -74,6 +74,11 @@ class FloatingService : CompositionFbService({
|
|||
})
|
||||
resolve(builder)
|
||||
}
|
||||
|
||||
isRunning.value = true
|
||||
onDestroy {
|
||||
isRunning.value = false
|
||||
}
|
||||
}) {
|
||||
|
||||
override fun onCreate() {
|
||||
|
@ -86,11 +91,9 @@ class FloatingService : CompositionFbService({
|
|||
}
|
||||
|
||||
companion object {
|
||||
fun isRunning() = ServiceUtils.isServiceRunning(FloatingService::class.java)
|
||||
val isRunning = MutableStateFlow(false)
|
||||
fun stop(context: Context = app) {
|
||||
if (isRunning()) {
|
||||
context.stopService(Intent(context, FloatingService::class.java))
|
||||
}
|
||||
context.stopService(Intent(context, FloatingService::class.java))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -3,7 +3,7 @@ package li.songe.gkd.debug
|
|||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import com.blankj.utilcode.util.LogUtils
|
||||
import com.blankj.utilcode.util.ServiceUtils
|
||||
import com.blankj.utilcode.util.ToastUtils
|
||||
import io.ktor.http.ContentType
|
||||
import io.ktor.serialization.kotlinx.json.json
|
||||
import io.ktor.server.application.call
|
||||
|
@ -33,9 +33,9 @@ import li.songe.gkd.appScope
|
|||
import li.songe.gkd.composition.CompositionService
|
||||
import li.songe.gkd.data.DeviceInfo
|
||||
import li.songe.gkd.data.GkdAction
|
||||
import li.songe.gkd.data.RawSubscription
|
||||
import li.songe.gkd.data.RpcError
|
||||
import li.songe.gkd.data.SubsItem
|
||||
import li.songe.gkd.data.SubscriptionRaw
|
||||
import li.songe.gkd.db.DbSet
|
||||
import li.songe.gkd.debug.SnapshotExt.captureSnapshot
|
||||
import li.songe.gkd.notif.createNotif
|
||||
|
@ -49,6 +49,7 @@ import li.songe.gkd.util.launchTry
|
|||
import li.songe.gkd.util.map
|
||||
import li.songe.gkd.util.storeFlow
|
||||
import li.songe.gkd.util.subsItemsFlow
|
||||
import li.songe.gkd.util.updateSubscription
|
||||
import java.io.File
|
||||
|
||||
|
||||
|
@ -56,14 +57,13 @@ class HttpService : CompositionService({
|
|||
val context = this
|
||||
val scope = CoroutineScope(Dispatchers.IO)
|
||||
|
||||
|
||||
val httpSubsItem = SubsItem(
|
||||
id = -1L,
|
||||
order = -1,
|
||||
enableUpdate = false,
|
||||
)
|
||||
|
||||
val httpSubsRawFlow = MutableStateFlow<SubscriptionRaw?>(null)
|
||||
val httpSubsRawFlow = MutableStateFlow<RawSubscription?>(null)
|
||||
fun createServer(port: Int): CIOApplicationEngine {
|
||||
return embeddedServer(CIO, port) {
|
||||
install(KtorCorsPlugin)
|
||||
|
@ -106,8 +106,8 @@ class HttpService : CompositionService({
|
|||
val subsStr =
|
||||
"""{"name":"内存订阅","id":-1,"version":0,"author":"@gkd-kit/inspect","apps":${call.receiveText()}}"""
|
||||
try {
|
||||
val httpSubsRaw = SubscriptionRaw.parse(subsStr)
|
||||
httpSubsItem.subsFile.writeText(SubscriptionRaw.stringify(httpSubsRaw))
|
||||
val httpSubsRaw = RawSubscription.parse(subsStr)
|
||||
updateSubscription(httpSubsRaw)
|
||||
DbSet.subsItemDao.insert((subsItemsFlow.value.find { s -> s.id == httpSubsItem.id }
|
||||
?: httpSubsItem).copy(mtime = System.currentTimeMillis()))
|
||||
} catch (e: Exception) {
|
||||
|
@ -116,7 +116,7 @@ class HttpService : CompositionService({
|
|||
call.respond(RpcOk())
|
||||
}
|
||||
post("/execSelector") {
|
||||
if (!GkdAbService.isRunning()) {
|
||||
if (!GkdAbService.isRunning.value) {
|
||||
throw RpcError("无障碍没有运行")
|
||||
}
|
||||
val gkdAction = call.receive<GkdAction>()
|
||||
|
@ -132,7 +132,17 @@ class HttpService : CompositionService({
|
|||
scope.launchTry(Dispatchers.IO) {
|
||||
storeFlow.map(scope) { s -> s.httpServerPort }.collect { port ->
|
||||
server?.stop()
|
||||
server = createServer(port).apply { start() }
|
||||
server = try {
|
||||
createServer(port).apply { start() }
|
||||
} catch (e: Exception) {
|
||||
LogUtils.d("HTTP服务启动失败", e)
|
||||
null
|
||||
}
|
||||
if (server == null) {
|
||||
ToastUtils.showShort("HTTP服务启动失败,您可以尝试切换端口后重新启动")
|
||||
stopSelf()
|
||||
return@collect
|
||||
}
|
||||
createNotif(
|
||||
context, httpChannel.id, httpNotif.copy(text = "HTTP服务正在运行-端口$port")
|
||||
)
|
||||
|
@ -153,14 +163,16 @@ class HttpService : CompositionService({
|
|||
scope.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
isRunning.value = true
|
||||
onDestroy {
|
||||
isRunning.value = false
|
||||
}
|
||||
}) {
|
||||
companion object {
|
||||
|
||||
fun isRunning() = ServiceUtils.isServiceRunning(HttpService::class.java)
|
||||
val isRunning = MutableStateFlow(false)
|
||||
fun stop(context: Context = app) {
|
||||
if (isRunning()) {
|
||||
context.stopService(Intent(context, HttpService::class.java))
|
||||
}
|
||||
context.stopService(Intent(context, HttpService::class.java))
|
||||
}
|
||||
|
||||
fun start(context: Context = app) {
|
||||
|
@ -177,7 +189,7 @@ data class RpcOk(
|
|||
|
||||
fun clearHttpSubs() {
|
||||
// 如果 app 被直接在任务列表划掉, HTTP订阅会没有清除, 所以在后续的第一次启动时清除
|
||||
if (HttpService.isRunning()) return
|
||||
if (HttpService.isRunning.value) return
|
||||
appScope.launchTry(Dispatchers.IO) {
|
||||
delay(1000)
|
||||
if (storeFlow.value.autoClearMemorySubs) {
|
||||
|
|
|
@ -5,7 +5,7 @@ import android.content.ComponentName
|
|||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import com.blankj.utilcode.util.LogUtils
|
||||
import com.blankj.utilcode.util.ServiceUtils
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import li.songe.gkd.app
|
||||
import li.songe.gkd.composition.CompositionExt.useLifeCycleLog
|
||||
import li.songe.gkd.composition.CompositionService
|
||||
|
@ -28,6 +28,11 @@ class ScreenshotService : CompositionService({
|
|||
screenshotUtil?.destroy()
|
||||
screenshotUtil = null
|
||||
}
|
||||
|
||||
isRunning.value = true
|
||||
onDestroy {
|
||||
isRunning.value = false
|
||||
}
|
||||
}) {
|
||||
companion object {
|
||||
suspend fun screenshot() = screenshotUtil?.execute()
|
||||
|
@ -40,11 +45,9 @@ class ScreenshotService : CompositionService({
|
|||
context.startForegroundService(intent)
|
||||
}
|
||||
|
||||
fun isRunning() = ServiceUtils.isServiceRunning(ScreenshotService::class.java)
|
||||
val isRunning = MutableStateFlow(false)
|
||||
fun stop(context: Context = app) {
|
||||
if (isRunning()) {
|
||||
context.stopService(Intent(context, ScreenshotService::class.java))
|
||||
}
|
||||
context.stopService(Intent(context, ScreenshotService::class.java))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -14,7 +14,6 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
|||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import kotlinx.serialization.encodeToString
|
||||
import li.songe.gkd.app
|
||||
import li.songe.gkd.data.ComplexSnapshot
|
||||
import li.songe.gkd.data.RpcError
|
||||
import li.songe.gkd.data.createComplexSnapshot
|
||||
|
@ -22,18 +21,16 @@ import li.songe.gkd.data.toSnapshot
|
|||
import li.songe.gkd.db.DbSet
|
||||
import li.songe.gkd.service.GkdAbService
|
||||
import li.songe.gkd.util.keepNullJson
|
||||
import li.songe.gkd.util.snapshotFolder
|
||||
import li.songe.gkd.util.snapshotZipDir
|
||||
import li.songe.gkd.util.storeFlow
|
||||
import java.io.File
|
||||
import kotlin.math.min
|
||||
|
||||
object SnapshotExt {
|
||||
val snapshotDir by lazy {
|
||||
app.getExternalFilesDir("snapshot")!!.apply { if (!exists()) mkdir() }
|
||||
}
|
||||
|
||||
private fun getSnapshotParentPath(snapshotId: Long) =
|
||||
"${snapshotDir.absolutePath}/${snapshotId}"
|
||||
"${snapshotFolder.absolutePath}/${snapshotId}"
|
||||
|
||||
fun getSnapshotPath(snapshotId: Long) =
|
||||
"${getSnapshotParentPath(snapshotId)}/${snapshotId}.json"
|
||||
|
@ -67,7 +64,7 @@ object SnapshotExt {
|
|||
private val captureLoading = MutableStateFlow(false)
|
||||
|
||||
suspend fun captureSnapshot(skipScreenshot: Boolean = false): ComplexSnapshot {
|
||||
if (!GkdAbService.isRunning()) {
|
||||
if (!GkdAbService.isRunning.value) {
|
||||
throw RpcError("无障碍不可用")
|
||||
}
|
||||
if (captureLoading.value) {
|
||||
|
@ -89,7 +86,7 @@ object SnapshotExt {
|
|||
)
|
||||
} else {
|
||||
GkdAbService.currentScreenshot() ?: withTimeoutOrNull(3_000) {
|
||||
if (!ScreenshotService.isRunning()) {
|
||||
if (!ScreenshotService.isRunning.value) {
|
||||
return@withTimeoutOrNull null
|
||||
}
|
||||
ScreenshotService.screenshot()
|
||||
|
|
|
@ -19,7 +19,7 @@ val abNotif by lazy {
|
|||
title = "GKD",
|
||||
text = "无障碍正在运行",
|
||||
ongoing = true,
|
||||
autoCancel = false
|
||||
autoCancel = false,
|
||||
)
|
||||
}
|
||||
val screenshotNotif by lazy {
|
||||
|
@ -29,7 +29,7 @@ val screenshotNotif by lazy {
|
|||
title = "GKD",
|
||||
text = "截屏服务正在运行",
|
||||
ongoing = true,
|
||||
autoCancel = false
|
||||
autoCancel = false,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -40,9 +40,10 @@ val floatingNotif by lazy {
|
|||
title = "GKD",
|
||||
text = "悬浮窗按钮正在显示",
|
||||
ongoing = true,
|
||||
autoCancel = false
|
||||
autoCancel = false,
|
||||
)
|
||||
}
|
||||
|
||||
val httpNotif by lazy {
|
||||
Notif(
|
||||
id = 103,
|
||||
|
@ -50,6 +51,6 @@ val httpNotif by lazy {
|
|||
title = "GKD",
|
||||
text = "HTTP服务正在运行",
|
||||
ongoing = true,
|
||||
autoCancel = false
|
||||
autoCancel = false,
|
||||
)
|
||||
}
|
|
@ -13,7 +13,7 @@ import androidx.core.app.NotificationManagerCompat
|
|||
import li.songe.gkd.MainActivity
|
||||
|
||||
fun createChannel(context: Context, notifChannel: NotifChannel) {
|
||||
val importance = NotificationManager.IMPORTANCE_DEFAULT
|
||||
val importance = NotificationManager.IMPORTANCE_LOW
|
||||
val channel = NotificationChannel(notifChannel.id, notifChannel.name, importance)
|
||||
channel.description = notifChannel.desc
|
||||
val notificationManager = NotificationManagerCompat.from(context)
|
||||
|
@ -24,8 +24,8 @@ fun createNotif(context: Service, channelId: String, notif: Notif) {
|
|||
val intent = Intent(context, MainActivity::class.java).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
}
|
||||
val pendingIntent: PendingIntent = PendingIntent.getActivity(
|
||||
context, 0, intent, PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||
val pendingIntent = PendingIntent.getActivity(
|
||||
context, notif.id, intent, PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||
)
|
||||
|
||||
val builder = NotificationCompat.Builder(context, channelId).setSmallIcon(notif.icon)
|
||||
|
|
|
@ -12,6 +12,7 @@ val AccessibilityService.safeActiveWindow: AccessibilityNodeInfo?
|
|||
get() = try {
|
||||
// java.lang.SecurityException: Call from user 0 as user -2 without permission INTERACT_ACROSS_USERS or INTERACT_ACROSS_USERS_FULL not allowed.
|
||||
rootInActiveWindow
|
||||
// 在主线程调用会阻塞界面导致卡顿
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
null
|
||||
|
@ -132,9 +133,10 @@ private val getAttr: (AccessibilityNodeInfo, String) -> Any? = { node, name ->
|
|||
"desc.length" -> node.contentDescription?.length
|
||||
|
||||
"clickable" -> node.isClickable
|
||||
"focusable" -> node.isFocusable
|
||||
"checkable" -> node.isCheckable
|
||||
"checked" -> node.isChecked
|
||||
"focusable" -> node.isFocusable
|
||||
"editable" -> node.isEditable
|
||||
"longClickable" -> node.isLongClickable
|
||||
"visibleToUser" -> node.isVisibleToUser
|
||||
|
||||
|
|
|
@ -3,38 +3,43 @@ package li.songe.gkd.service
|
|||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import li.songe.gkd.appScope
|
||||
import li.songe.gkd.data.AppRule
|
||||
import li.songe.gkd.data.ClickLog
|
||||
import li.songe.gkd.data.Rule
|
||||
import li.songe.gkd.data.GlobalRule
|
||||
import li.songe.gkd.data.ResolvedRule
|
||||
import li.songe.gkd.data.SubsConfig
|
||||
import li.songe.gkd.db.DbSet
|
||||
import li.songe.gkd.util.appRuleFlow
|
||||
import li.songe.gkd.util.AllRules
|
||||
import li.songe.gkd.util.allRulesFlow
|
||||
import li.songe.gkd.util.increaseClickCount
|
||||
import li.songe.gkd.util.launchTry
|
||||
import li.songe.gkd.util.recordStoreFlow
|
||||
import li.songe.gkd.util.storeFlow
|
||||
|
||||
data class TopActivity(
|
||||
val appId: String,
|
||||
val appId: String = "",
|
||||
val activityId: String? = null,
|
||||
val number: Int = 0
|
||||
)
|
||||
|
||||
val topActivityFlow by lazy {
|
||||
MutableStateFlow<TopActivity?>(null)
|
||||
}
|
||||
val topActivityFlow = MutableStateFlow(TopActivity())
|
||||
|
||||
data class ActivityRule(
|
||||
val rules: List<Rule> = emptyList(),
|
||||
val topActivity: TopActivity? = null,
|
||||
val appIdToRules: Map<String, List<Rule>> = emptyMap(),
|
||||
)
|
||||
private val appRules: List<AppRule> = emptyList(),
|
||||
private val globalRules: List<GlobalRule> = emptyList(),
|
||||
val topActivity: TopActivity = TopActivity(),
|
||||
val allRules: AllRules = AllRules(),
|
||||
) {
|
||||
val currentRules = appRules + globalRules
|
||||
}
|
||||
|
||||
val activityRuleFlow by lazy { MutableStateFlow(ActivityRule()) }
|
||||
|
||||
private var lastTopActivity: TopActivity? = null
|
||||
private var lastTopActivity: TopActivity = topActivityFlow.value
|
||||
|
||||
private fun getFixTopActivity(): TopActivity? {
|
||||
val top = topActivityFlow.value ?: return null
|
||||
private fun getFixTopActivity(): TopActivity {
|
||||
val top = topActivityFlow.value
|
||||
if (top.activityId == null) {
|
||||
if (lastTopActivity?.appId == top.appId) {
|
||||
if (lastTopActivity.appId == top.appId) {
|
||||
// 当从通知栏上拉返回应用等时, activityId 的无障碍事件不会触发, 此时复用上一次获得的 activityId 填充
|
||||
topActivityFlow.value = lastTopActivity
|
||||
}
|
||||
|
@ -47,121 +52,71 @@ private fun getFixTopActivity(): TopActivity? {
|
|||
|
||||
fun getCurrentRules(): ActivityRule {
|
||||
val topActivity = getFixTopActivity()
|
||||
val activityRule = activityRuleFlow.value
|
||||
val appIdToRules = if (storeFlow.value.matchUnknownApp) {
|
||||
appRuleFlow.value.allMap
|
||||
} else {
|
||||
appRuleFlow.value.visibleMap
|
||||
}
|
||||
val idChanged = topActivity?.appId != activityRule.topActivity?.appId
|
||||
val topChanged = activityRule.topActivity != topActivity
|
||||
if (topChanged || activityRule.appIdToRules !== appIdToRules) {
|
||||
activityRuleFlow.value =
|
||||
ActivityRule(rules = (appIdToRules[topActivity?.appId] ?: emptyList()).filter { rule ->
|
||||
rule.matchActivityId(topActivity?.activityId)
|
||||
}, topActivity = topActivity, appIdToRules = appIdToRules)
|
||||
}
|
||||
if (topChanged) {
|
||||
val t = System.currentTimeMillis()
|
||||
if (idChanged) {
|
||||
appChangeTime = t
|
||||
openAdOptimized = null
|
||||
}
|
||||
activityChangeTime = t
|
||||
if (openAdOptimized == null && t - appChangeTime < openAdOptimizedTime) {
|
||||
openAdOptimized = activityRuleFlow.value.rules.any { r -> r.isOpenAd }
|
||||
val oldActivityRule = activityRuleFlow.value
|
||||
val allRules = allRulesFlow.value
|
||||
val idChanged = topActivity.appId != oldActivityRule.topActivity.appId
|
||||
val topChanged = idChanged || oldActivityRule.topActivity != topActivity
|
||||
if (topChanged || oldActivityRule.allRules !== allRules) {
|
||||
val newActivityRule = ActivityRule(
|
||||
allRules = allRules,
|
||||
topActivity = topActivity,
|
||||
appRules = (allRules.appIdToRules[topActivity.appId]
|
||||
?: emptyList()).filter { rule ->
|
||||
rule.matchActivity(topActivity)
|
||||
},
|
||||
globalRules = allRulesFlow.value.globalRules.filter { r ->
|
||||
r.matchActivity(
|
||||
topActivity
|
||||
)
|
||||
},
|
||||
)
|
||||
activityRuleFlow.value = newActivityRule
|
||||
if (newActivityRule.currentRules.isNotEmpty()) {
|
||||
val newRules =
|
||||
newActivityRule.currentRules.filter { r -> !oldActivityRule.currentRules.any { r2 -> r2 == r } }
|
||||
val t = System.currentTimeMillis()
|
||||
if (newRules.isNotEmpty()) {
|
||||
newRules.forEach { r ->
|
||||
r.matchChangedTime = t
|
||||
}
|
||||
}
|
||||
newActivityRule.currentRules.forEach { r ->
|
||||
if (r.resetMatchTypeWhenActivity || idChanged) {
|
||||
r.actionDelayTriggerTime = 0
|
||||
r.actionCount.value = 0
|
||||
}
|
||||
if (idChanged) {
|
||||
// 重置全局规则的匹配时间
|
||||
r.matchChangedTime = t
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (idChanged) {
|
||||
appChangeTime = System.currentTimeMillis()
|
||||
}
|
||||
return activityRuleFlow.value
|
||||
}
|
||||
|
||||
var lastTriggerRule: Rule? = null
|
||||
var lastTriggerAppRule: ResolvedRule? = null
|
||||
var appChangeTime = 0L
|
||||
var activityChangeTime = 0L
|
||||
|
||||
// null: app 切换过
|
||||
// true: 需要执行优化(此界面组需要存在开屏广告)
|
||||
// false: 执行优化过了/已经过了优化时间
|
||||
@Volatile
|
||||
var openAdOptimized: Boolean? = null
|
||||
const val openAdOptimizedTime = 5000L
|
||||
|
||||
fun isAvailableRule(rule: Rule): Boolean {
|
||||
val t = System.currentTimeMillis()
|
||||
if (openAdOptimized == true) {
|
||||
if (t - appChangeTime > openAdOptimizedTime) {
|
||||
openAdOptimized = false
|
||||
}
|
||||
}
|
||||
if (rule.resetMatchTypeWhenActivity) {
|
||||
if (activityChangeTime != rule.matchChangeTime) {
|
||||
// 当 界面 更新时, 重置操作延迟点, 重置点击次数
|
||||
rule.actionDelayTriggerTime = 0
|
||||
rule.actionCount.value = 0
|
||||
rule.matchChangeTime = activityChangeTime
|
||||
}
|
||||
} else {
|
||||
if (appChangeTime != rule.matchChangeTime) {
|
||||
// 当 切换APP 时, 重置点击次数
|
||||
rule.actionDelayTriggerTime = 0
|
||||
rule.actionCount.value = 0
|
||||
rule.matchChangeTime = appChangeTime
|
||||
}
|
||||
}
|
||||
if (rule.actionMaximum != null) {
|
||||
if (rule.actionCount.value >= rule.actionMaximum) {
|
||||
return false // 达到最大执行次数
|
||||
}
|
||||
}
|
||||
if (rule.matchDelay != null) {
|
||||
// 处于匹配延迟中
|
||||
if (rule.resetMatchTypeWhenActivity) {
|
||||
if (t - activityChangeTime < rule.matchDelay) {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
if (t - appChangeTime < rule.matchDelay) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
if (rule.matchTime != null) {
|
||||
// 超出了匹配时间
|
||||
if (rule.resetMatchTypeWhenActivity) {
|
||||
if (t - activityChangeTime > rule.matchAllTime) {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
if (t - appChangeTime > rule.matchAllTime) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
if (rule.actionTriggerTime.value + rule.actionCd > t) return false // 处于冷却时间
|
||||
if (rule.preRules.isNotEmpty()) { // 需要提前点击某个规则
|
||||
lastTriggerRule ?: return false
|
||||
// 上一个点击的规则不在当前需要点击的列表
|
||||
return rule.preRules.any { it === lastTriggerRule }
|
||||
}
|
||||
if (rule.actionDelayTriggerTime > 0) {
|
||||
if (rule.actionDelayTriggerTime + rule.actionDelay > t) {
|
||||
return false // 没有延迟完毕
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
fun insertClickLog(rule: Rule) {
|
||||
rule.trigger()
|
||||
fun insertClickLog(appRule: ResolvedRule) {
|
||||
appRule.trigger()
|
||||
toastClickTip()
|
||||
appScope.launchTry(Dispatchers.IO) {
|
||||
val clickLog = ClickLog(
|
||||
appId = topActivityFlow.value?.appId,
|
||||
activityId = topActivityFlow.value?.activityId,
|
||||
subsId = rule.subsItem.id,
|
||||
groupKey = rule.group.key,
|
||||
ruleIndex = rule.index,
|
||||
ruleKey = rule.key
|
||||
appId = topActivityFlow.value.appId,
|
||||
activityId = topActivityFlow.value.activityId,
|
||||
subsId = appRule.subsItem.id,
|
||||
subsVersion = appRule.rawSubs.version,
|
||||
groupKey = appRule.group.key,
|
||||
groupType = when (appRule) {
|
||||
is AppRule -> SubsConfig.AppGroupType
|
||||
is GlobalRule -> SubsConfig.GlobalGroupType
|
||||
},
|
||||
ruleIndex = appRule.index,
|
||||
ruleKey = appRule.key,
|
||||
)
|
||||
DbSet.clickLogDao.insert(clickLog)
|
||||
increaseClickCount()
|
||||
|
|
|
@ -1,17 +1,20 @@
|
|||
package li.songe.gkd.service
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.pm.PackageManager
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.PixelFormat
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import android.view.Display
|
||||
import android.view.View
|
||||
import android.view.WindowManager
|
||||
import android.view.accessibility.AccessibilityEvent
|
||||
import com.blankj.utilcode.util.LogUtils
|
||||
import com.blankj.utilcode.util.ServiceUtils
|
||||
import com.blankj.utilcode.util.ScreenUtils
|
||||
import com.blankj.utilcode.util.ToastUtils
|
||||
import io.ktor.client.call.body
|
||||
import io.ktor.client.request.get
|
||||
|
@ -19,12 +22,10 @@ import io.ktor.client.statement.bodyAsText
|
|||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import li.songe.gkd.composition.CompositionAbService
|
||||
|
@ -33,43 +34,35 @@ import li.songe.gkd.composition.CompositionExt.useScope
|
|||
import li.songe.gkd.data.ActionResult
|
||||
import li.songe.gkd.data.GkdAction
|
||||
import li.songe.gkd.data.NodeInfo
|
||||
import li.songe.gkd.data.RawSubscription
|
||||
import li.songe.gkd.data.RpcError
|
||||
import li.songe.gkd.data.SubsVersion
|
||||
import li.songe.gkd.data.SubscriptionRaw
|
||||
import li.songe.gkd.data.getActionFc
|
||||
import li.songe.gkd.db.DbSet
|
||||
import li.songe.gkd.debug.SnapshotExt
|
||||
import li.songe.gkd.shizuku.useSafeGetTasksFc
|
||||
import li.songe.gkd.util.VOLUME_CHANGED_ACTION
|
||||
import li.songe.gkd.util.client
|
||||
import li.songe.gkd.util.launchTry
|
||||
import li.songe.gkd.util.map
|
||||
import li.songe.gkd.util.storeFlow
|
||||
import li.songe.gkd.util.subsIdToRawFlow
|
||||
import li.songe.gkd.util.subsItemsFlow
|
||||
import li.songe.gkd.util.updateSubscription
|
||||
import li.songe.selector.Selector
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
|
||||
@OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class)
|
||||
class GkdAbService : CompositionAbService({
|
||||
useLifeCycleLog()
|
||||
|
||||
val context = this as GkdAbService
|
||||
|
||||
context.resources
|
||||
|
||||
val scope = useScope()
|
||||
|
||||
service = context
|
||||
onDestroy {
|
||||
service = null
|
||||
topActivityFlow.value = null
|
||||
}
|
||||
|
||||
ManageService.start(context)
|
||||
onDestroy {
|
||||
ManageService.stop(context)
|
||||
}
|
||||
|
||||
val safeGetTasksFc = useSafeGetTasksFc(scope)
|
||||
|
@ -85,7 +78,7 @@ class GkdAbService : CompositionAbService({
|
|||
appId: String,
|
||||
activityId: String,
|
||||
): Boolean {
|
||||
if (appId == topActivityFlow.value?.appId && activityId == topActivityFlow.value?.activityId) return true
|
||||
if (appId == topActivityFlow.value.appId && activityId == topActivityFlow.value.activityId) return true
|
||||
val r = (try {
|
||||
packageManager.getActivityInfo(
|
||||
ComponentName(
|
||||
|
@ -95,42 +88,40 @@ class GkdAbService : CompositionAbService({
|
|||
} catch (e: PackageManager.NameNotFoundException) {
|
||||
null
|
||||
} != null)
|
||||
Log.d("isActivity", "$appId, $activityId, $r")
|
||||
return r
|
||||
}
|
||||
|
||||
var lastTriggerShizukuTime = 0L
|
||||
var lastContentEventTime = 0L
|
||||
var job: Job? = null
|
||||
val singleThread = Dispatchers.IO.limitedParallelism(1)
|
||||
val eventThread = Dispatchers.IO.limitedParallelism(1)
|
||||
onDestroy {
|
||||
singleThread.cancel()
|
||||
}
|
||||
val lastQueryTimeFlow = MutableStateFlow(0L)
|
||||
val debounceTime = 500L
|
||||
fun newQueryTask() {
|
||||
job = scope.launchTry(singleThread) {
|
||||
scope.launchTry(singleThread) {
|
||||
val activityRule = getCurrentRules()
|
||||
for (rule in (activityRule.rules)) {
|
||||
if (!isActive && !rule.isOpenAd) break
|
||||
if (!isAvailableRule(rule)) continue
|
||||
for (rule in (activityRule.currentRules)) {
|
||||
val statusCode = rule.statusCode
|
||||
if (statusCode == 4 && rule.matchDelayJob == null) {
|
||||
rule.matchDelayJob = scope.launch {
|
||||
delay(rule.matchDelay)
|
||||
rule.matchDelayJob = null
|
||||
newQueryTask()
|
||||
}
|
||||
}
|
||||
if (statusCode != 0) continue
|
||||
val nodeVal = safeActiveWindow ?: continue
|
||||
val target = rule.query(nodeVal) ?: continue
|
||||
if (activityRule !== getCurrentRules()) break
|
||||
|
||||
// 开始 action 延迟
|
||||
if (rule.actionDelay > 0 && rule.actionDelayTriggerTime == 0L) {
|
||||
rule.triggerDelay()
|
||||
// 防止在 actionDelay 时间内没有事件发送导致无法触发
|
||||
scope.launch(singleThread) {
|
||||
delay(rule.actionDelay - debounceTime)
|
||||
lastQueryTimeFlow.value = System.currentTimeMillis()
|
||||
if (rule.checkDelay() && rule.actionDelayJob == null) {
|
||||
rule.actionDelayJob = scope.launch {
|
||||
delay(rule.actionDelay)
|
||||
rule.actionDelayJob = null
|
||||
newQueryTask()
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// 如果节点在屏幕外部, click 的结果为 null
|
||||
val actionResult = rule.performAction(context, target)
|
||||
if (actionResult.result) {
|
||||
LogUtils.d(
|
||||
|
@ -144,27 +135,24 @@ class GkdAbService : CompositionAbService({
|
|||
}
|
||||
}
|
||||
|
||||
scope.launch(singleThread) {
|
||||
lastQueryTimeFlow.debounce(debounceTime).collect {
|
||||
newQueryTask()
|
||||
}
|
||||
}
|
||||
val skipAppIds = setOf("com.android.systemui")
|
||||
val skipAppIds = listOf("com.android.systemui")
|
||||
onAccessibilityEvent { event ->
|
||||
if (event == null || event.packageName == null) return@onAccessibilityEvent
|
||||
if (!(event.eventType == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED || event.eventType == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED)) return@onAccessibilityEvent
|
||||
|
||||
if (skipAppIds.any { id -> id.contentEquals(event.packageName) }) return@onAccessibilityEvent
|
||||
if (event.eventType == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED &&
|
||||
skipAppIds.any { id ->
|
||||
id.contentEquals(
|
||||
event.packageName
|
||||
)
|
||||
}
|
||||
) {
|
||||
return@onAccessibilityEvent
|
||||
}
|
||||
|
||||
if (event.eventType == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED) {
|
||||
if (event.eventTime - appChangeTime > 5_000) { // app 启动 5s 内关闭限制
|
||||
if (event.eventTime - lastContentEventTime < 100) {
|
||||
if (event.eventTime - (lastTriggerRule?.actionTriggerTime?.value
|
||||
?: 0) > 1000
|
||||
) { // 规则刚刚触发后 1s 内关闭限制
|
||||
return@onAccessibilityEvent
|
||||
}
|
||||
}
|
||||
if (event.eventTime - lastContentEventTime < 100 && event.eventTime - appChangeTime > 5000) {
|
||||
return@onAccessibilityEvent
|
||||
}
|
||||
lastContentEventTime = event.eventTime
|
||||
}
|
||||
|
@ -182,16 +170,17 @@ class GkdAbService : CompositionAbService({
|
|||
// tv.danmaku.bili, com.miui.home, com.miui.home.launcher.Launcher
|
||||
if (isActivity(evAppId, evActivityId)) {
|
||||
topActivityFlow.value = TopActivity(
|
||||
evAppId, evActivityId
|
||||
evAppId, evActivityId, topActivityFlow.value.number + 1
|
||||
)
|
||||
activityChangeTime = System.currentTimeMillis()
|
||||
}
|
||||
} else {
|
||||
if (fixedEvent.eventTime - lastTriggerShizukuTime > 300) {
|
||||
val shizukuTop = getShizukuTopActivity()
|
||||
if (shizukuTop != null && shizukuTop.appId == rightAppId) {
|
||||
if (shizukuTop.activityId == evActivityId) {
|
||||
activityChangeTime = System.currentTimeMillis()
|
||||
topActivityFlow.value = TopActivity(
|
||||
evAppId, evActivityId, topActivityFlow.value.number + 1
|
||||
)
|
||||
}
|
||||
topActivityFlow.value = shizukuTop
|
||||
}
|
||||
|
@ -199,7 +188,7 @@ class GkdAbService : CompositionAbService({
|
|||
}
|
||||
}
|
||||
}
|
||||
if (rightAppId != topActivityFlow.value?.appId) {
|
||||
if (rightAppId != topActivityFlow.value.appId) {
|
||||
// 从 锁屏,下拉通知栏 返回等情况, 应用不会发送事件, 但是系统组件会发送事件
|
||||
val shizukuTop = getShizukuTopActivity()
|
||||
if (shizukuTop?.appId == rightAppId) {
|
||||
|
@ -209,7 +198,7 @@ class GkdAbService : CompositionAbService({
|
|||
}
|
||||
}
|
||||
|
||||
if (getCurrentRules().rules.isEmpty()) {
|
||||
if (getCurrentRules().currentRules.isEmpty()) {
|
||||
// 放在 evAppId != rightAppId 的前面使得 TopActivity 能借助 lastTopActivity 恢复
|
||||
return@launch
|
||||
}
|
||||
|
@ -218,18 +207,7 @@ class GkdAbService : CompositionAbService({
|
|||
return@launch
|
||||
}
|
||||
|
||||
|
||||
if (!storeFlow.value.enableService) return@launch
|
||||
val jobVal = job
|
||||
if (jobVal?.isActive == true) {
|
||||
if (openAdOptimized == true) {
|
||||
jobVal.cancel()
|
||||
} else {
|
||||
return@launch
|
||||
}
|
||||
}
|
||||
|
||||
lastQueryTimeFlow.value = fixedEvent.eventTime
|
||||
|
||||
newQueryTask()
|
||||
}
|
||||
|
@ -254,7 +232,7 @@ class GkdAbService : CompositionAbService({
|
|||
LogUtils.d("快速检测更新失败", subsItem, e)
|
||||
}
|
||||
}
|
||||
val newSubsRaw = SubscriptionRaw.parse(
|
||||
val newSubsRaw = RawSubscription.parse(
|
||||
client.get(subsItem.updateUrl).bodyAsText()
|
||||
)
|
||||
if (newSubsRaw.id != subsItem.id) {
|
||||
|
@ -263,11 +241,7 @@ class GkdAbService : CompositionAbService({
|
|||
if (oldSubsRaw != null && newSubsRaw.version <= oldSubsRaw.version) {
|
||||
return@forEach
|
||||
}
|
||||
subsItem.subsFile.writeText(
|
||||
SubscriptionRaw.stringify(
|
||||
newSubsRaw
|
||||
)
|
||||
)
|
||||
updateSubscription(newSubsRaw)
|
||||
val newItem = subsItem.copy(
|
||||
updateUrl = newSubsRaw.updateUrl ?: subsItem.updateUrl,
|
||||
mtime = System.currentTimeMillis()
|
||||
|
@ -298,9 +272,9 @@ class GkdAbService : CompositionAbService({
|
|||
scope.launch(Dispatchers.IO) {
|
||||
activityRuleFlow.debounce(300).collect {
|
||||
if (storeFlow.value.enableService) {
|
||||
LogUtils.d(it.topActivity, *it.rules.map { r ->
|
||||
"subsId:${r.subsItem.id}, gKey=${r.group.key}, gName:${r.group.name}, ruleIndex:${r.index}, rKey:${r.key}, active:${
|
||||
isAvailableRule(r)
|
||||
LogUtils.d(it.topActivity, *it.currentRules.map { r ->
|
||||
"id:${r.subsItem.id}, v:${r.rawSubs.version}, gKey=${r.group.key}, gName:${r.group.name}, rIndex:${r.index}, rKey:${r.key}, rCode:${
|
||||
r.statusCode
|
||||
}"
|
||||
}.toTypedArray())
|
||||
} else {
|
||||
|
@ -351,6 +325,52 @@ class GkdAbService : CompositionAbService({
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
fun createReceiver(): BroadcastReceiver {
|
||||
return object : BroadcastReceiver() {
|
||||
var lastTriggerTime = -1L
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
if (intent?.action == VOLUME_CHANGED_ACTION) {
|
||||
val t = System.currentTimeMillis()
|
||||
if (t - lastTriggerTime > 3000 && !ScreenUtils.isScreenLock()) {
|
||||
lastTriggerTime = t
|
||||
scope.launchTry(Dispatchers.IO) {
|
||||
SnapshotExt.captureSnapshot()
|
||||
ToastUtils.showShort("快照成功")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var captureVolumeReceiver: BroadcastReceiver? = null
|
||||
scope.launch {
|
||||
storeFlow.map(scope) { s -> s.captureVolumeChange }.collect {
|
||||
if (captureVolumeReceiver != null) {
|
||||
context.unregisterReceiver(captureVolumeReceiver)
|
||||
}
|
||||
captureVolumeReceiver = if (it) {
|
||||
createReceiver().apply {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
context.registerReceiver(
|
||||
this, IntentFilter(VOLUME_CHANGED_ACTION), Context.RECEIVER_EXPORTED
|
||||
)
|
||||
} else {
|
||||
context.registerReceiver(this, IntentFilter(VOLUME_CHANGED_ACTION))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
onDestroy {
|
||||
if (captureVolumeReceiver != null) {
|
||||
context.unregisterReceiver(captureVolumeReceiver)
|
||||
}
|
||||
}
|
||||
|
||||
onAccessibilityEvent { e ->
|
||||
if (!storeFlow.value.captureScreenshot) return@onAccessibilityEvent
|
||||
e ?: return@onAccessibilityEvent
|
||||
|
@ -368,11 +388,18 @@ class GkdAbService : CompositionAbService({
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
isRunning.value = true
|
||||
onDestroy {
|
||||
isRunning.value = false
|
||||
}
|
||||
}) {
|
||||
|
||||
companion object {
|
||||
var service: GkdAbService? = null
|
||||
fun isRunning() = ServiceUtils.isServiceRunning(GkdAbService::class.java)
|
||||
|
||||
val isRunning = MutableStateFlow(false)
|
||||
|
||||
fun execAction(gkdAction: GkdAction): ActionResult {
|
||||
val serviceVal = service ?: throw RpcError("无障碍没有运行")
|
||||
|
|
|
@ -1,13 +1,8 @@
|
|||
package li.songe.gkd.service
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.os.Build
|
||||
import com.blankj.utilcode.util.ScreenUtils
|
||||
import com.blankj.utilcode.util.ToastUtils
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
|
@ -16,14 +11,11 @@ import li.songe.gkd.app
|
|||
import li.songe.gkd.composition.CompositionExt.useLifeCycleLog
|
||||
import li.songe.gkd.composition.CompositionExt.useScope
|
||||
import li.songe.gkd.composition.CompositionService
|
||||
import li.songe.gkd.debug.SnapshotExt.captureSnapshot
|
||||
import li.songe.gkd.notif.abNotif
|
||||
import li.songe.gkd.notif.createNotif
|
||||
import li.songe.gkd.notif.defaultChannel
|
||||
import li.songe.gkd.util.VOLUME_CHANGED_ACTION
|
||||
import li.songe.gkd.util.appRuleFlow
|
||||
import li.songe.gkd.util.allRulesFlow
|
||||
import li.songe.gkd.util.clickCountFlow
|
||||
import li.songe.gkd.util.launchTry
|
||||
import li.songe.gkd.util.map
|
||||
import li.songe.gkd.util.storeFlow
|
||||
|
||||
|
@ -32,61 +24,21 @@ class ManageService : CompositionService({
|
|||
val context = this
|
||||
createNotif(context, defaultChannel.id, abNotif)
|
||||
val scope = useScope()
|
||||
|
||||
fun createReceiver(): BroadcastReceiver {
|
||||
return object : BroadcastReceiver() {
|
||||
var lastTriggerTime = -1L
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
if (intent?.action == VOLUME_CHANGED_ACTION) {
|
||||
val t = System.currentTimeMillis()
|
||||
if (t - lastTriggerTime > 3000 && !ScreenUtils.isScreenLock()) {
|
||||
lastTriggerTime = t
|
||||
scope.launchTry(Dispatchers.IO) {
|
||||
captureSnapshot()
|
||||
ToastUtils.showShort("快照成功")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var receiver: BroadcastReceiver? = null
|
||||
scope.launchTry(Dispatchers.IO) {
|
||||
storeFlow.map(scope) { s -> s.captureVolumeChange }.collect {
|
||||
if (receiver != null) {
|
||||
context.unregisterReceiver(receiver)
|
||||
}
|
||||
receiver = if (it) {
|
||||
createReceiver().apply {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
context.registerReceiver(
|
||||
this, IntentFilter(VOLUME_CHANGED_ACTION), Context.RECEIVER_EXPORTED
|
||||
)
|
||||
} else {
|
||||
context.registerReceiver(this, IntentFilter(VOLUME_CHANGED_ACTION))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
onDestroy {
|
||||
if (receiver != null) {
|
||||
context.unregisterReceiver(receiver)
|
||||
}
|
||||
}
|
||||
|
||||
scope.launch {
|
||||
combine(appRuleFlow, clickCountFlow, storeFlow) { appRule, clickCount, store ->
|
||||
if (!store.enableService) return@combine "服务已暂停"
|
||||
val appSize = appRule.visibleMap.keys.size
|
||||
val groupSize = appRule.visibleMap.values.sumOf { rules ->
|
||||
rules.map { r -> r.group.key }.toSet().size
|
||||
}
|
||||
(if (groupSize > 0) {
|
||||
"${appSize}应用/${groupSize}规则组"
|
||||
combine(
|
||||
allRulesFlow,
|
||||
clickCountFlow,
|
||||
storeFlow.map(scope) { it.enableService },
|
||||
GkdAbService.isRunning
|
||||
) { allRules, clickCount, enableService, abRunning ->
|
||||
if (!abRunning) return@combine "无障碍未授权"
|
||||
if (!enableService) return@combine "服务已暂停"
|
||||
(if (allRules.allGroupSize > 0) {
|
||||
if (allRules.appSize > 0) {
|
||||
"${allRules.appSize}应用/${allRules.allGroupSize}规则组"
|
||||
} else {
|
||||
"${allRules.allGroupSize}规则组"
|
||||
}
|
||||
} else {
|
||||
"暂无规则"
|
||||
}) + if (clickCount > 0) "/${clickCount}点击" else ""
|
||||
|
@ -98,12 +50,18 @@ class ManageService : CompositionService({
|
|||
)
|
||||
}
|
||||
}
|
||||
isRunning.value = true
|
||||
onDestroy {
|
||||
isRunning.value = false
|
||||
}
|
||||
}) {
|
||||
companion object {
|
||||
fun start(context: Context = app) {
|
||||
context.startForegroundService(Intent(context, ManageService::class.java))
|
||||
}
|
||||
|
||||
val isRunning = MutableStateFlow(false)
|
||||
|
||||
fun stop(context: Context = app) {
|
||||
context.stopService(Intent(context, ManageService::class.java))
|
||||
}
|
||||
|
|
|
@ -54,9 +54,8 @@ import com.blankj.utilcode.util.ToastUtils
|
|||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.annotation.RootNavGraph
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.serialization.encodeToString
|
||||
import li.songe.gkd.data.RawSubscription
|
||||
import li.songe.gkd.data.SubsConfig
|
||||
import li.songe.gkd.data.SubscriptionRaw
|
||||
import li.songe.gkd.db.DbSet
|
||||
import li.songe.gkd.ui.destinations.GroupItemPageDestination
|
||||
import li.songe.gkd.util.LocalNavController
|
||||
|
@ -68,6 +67,7 @@ import li.songe.gkd.util.json
|
|||
import li.songe.gkd.util.launchAsFn
|
||||
import li.songe.gkd.util.launchTry
|
||||
import li.songe.gkd.util.navigate
|
||||
import li.songe.gkd.util.updateSubscription
|
||||
|
||||
@RootNavGraph
|
||||
@Destination(style = ProfileTransitions::class)
|
||||
|
@ -92,7 +92,7 @@ fun AppItemPage(
|
|||
val groupToCategoryMap = subsRaw?.groupToCategoryMap ?: emptyMap()
|
||||
|
||||
val (showGroupItem, setShowGroupItem) = remember {
|
||||
mutableStateOf<SubscriptionRaw.GroupRaw?>(
|
||||
mutableStateOf<RawSubscription.RawAppGroup?>(
|
||||
null
|
||||
)
|
||||
}
|
||||
|
@ -102,10 +102,10 @@ fun AppItemPage(
|
|||
var showAddDlg by remember { mutableStateOf(false) }
|
||||
|
||||
val (menuGroupRaw, setMenuGroupRaw) = remember {
|
||||
mutableStateOf<SubscriptionRaw.GroupRaw?>(null)
|
||||
mutableStateOf<RawSubscription.RawAppGroup?>(null)
|
||||
}
|
||||
val (editGroupRaw, setEditGroupRaw) = remember {
|
||||
mutableStateOf<SubscriptionRaw.GroupRaw?>(null)
|
||||
mutableStateOf<RawSubscription.RawAppGroup?>(null)
|
||||
}
|
||||
|
||||
Scaffold(topBar = {
|
||||
|
@ -173,7 +173,7 @@ fun AppItemPage(
|
|||
)
|
||||
if (group.valid) {
|
||||
Text(
|
||||
text = group.desc ?: "-",
|
||||
text = group.desc ?: "",
|
||||
maxLines = 1,
|
||||
softWrap = false,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
|
@ -213,7 +213,7 @@ fun AppItemPage(
|
|||
Switch(checked = groupEnable, modifier = Modifier,
|
||||
onCheckedChange = scope.launchAsFn { enable ->
|
||||
val newItem = (subsConfig?.copy(enable = enable) ?: SubsConfig(
|
||||
type = SubsConfig.GroupType,
|
||||
type = SubsConfig.AppGroupType,
|
||||
subsItemId = subsItemId,
|
||||
appId = appId,
|
||||
groupKey = group.key,
|
||||
|
@ -244,7 +244,7 @@ fun AppItemPage(
|
|||
Text(text = "该规则组默认不启用")
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
}
|
||||
Text(text = showGroupItemVal.desc ?: "-")
|
||||
Text(text = showGroupItemVal.desc ?: "")
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
|
@ -255,8 +255,8 @@ fun AppItemPage(
|
|||
navController.navigate(
|
||||
GroupItemPageDestination(
|
||||
subsInt = subsItemId,
|
||||
groupKey = showGroupItemVal.key,
|
||||
appId = appId,
|
||||
groupKey = showGroupItemVal.key
|
||||
)
|
||||
)
|
||||
}) {
|
||||
|
@ -307,11 +307,7 @@ fun AppItemPage(
|
|||
appRawVal.copy(groups = appRawVal.groups.filter { g -> g.key != menuGroupRaw.key })
|
||||
)
|
||||
})
|
||||
subsItemVal.subsFile.writeText(
|
||||
json.encodeToString(
|
||||
newSubsRaw
|
||||
)
|
||||
)
|
||||
updateSubscription(newSubsRaw)
|
||||
DbSet.subsItemDao.update(subsItemVal.copy(mtime = System.currentTimeMillis()))
|
||||
DbSet.subsConfigDao.delete(
|
||||
subsItemVal.id, appRawVal.id, menuGroupRaw.key
|
||||
|
@ -356,7 +352,7 @@ fun AppItemPage(
|
|||
return@TextButton
|
||||
}
|
||||
val newGroupRaw = try {
|
||||
SubscriptionRaw.parseGroupRaw(source)
|
||||
RawSubscription.parseRawGroup(source)
|
||||
} catch (e: Exception) {
|
||||
LogUtils.d(e)
|
||||
ToastUtils.showShort("非法规则:${e.message}")
|
||||
|
@ -383,11 +379,7 @@ fun AppItemPage(
|
|||
)
|
||||
})
|
||||
vm.viewModelScope.launchTry(Dispatchers.IO) {
|
||||
subsItemVal.subsFile.writeText(
|
||||
json.encodeToString(
|
||||
newSubsRaw
|
||||
)
|
||||
)
|
||||
updateSubscription(newSubsRaw)
|
||||
DbSet.subsItemDao.update(subsItemVal.copy(mtime = System.currentTimeMillis()))
|
||||
ToastUtils.showShort("更新成功")
|
||||
}
|
||||
|
@ -413,13 +405,13 @@ fun AppItemPage(
|
|||
}, onDismissRequest = { showAddDlg = false }, confirmButton = {
|
||||
TextButton(onClick = {
|
||||
val newAppRaw = try {
|
||||
SubscriptionRaw.parseAppRaw(source)
|
||||
RawSubscription.parseRawApp(source)
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
val tempGroups = if (newAppRaw == null) {
|
||||
val newGroupRaw = try {
|
||||
SubscriptionRaw.parseGroupRaw(source)
|
||||
RawSubscription.parseRawGroup(source)
|
||||
} catch (e: Exception) {
|
||||
LogUtils.d(e)
|
||||
ToastUtils.showShort("非法规则:${e.message}")
|
||||
|
@ -460,8 +452,8 @@ fun AppItemPage(
|
|||
)
|
||||
})
|
||||
vm.viewModelScope.launchTry(Dispatchers.IO) {
|
||||
subsItemVal.subsFile.writeText(json.encodeToString(newSubsRaw))
|
||||
DbSet.subsItemDao.update(subsItemVal.copy(mtime = System.currentTimeMillis()))
|
||||
updateSubscription(newSubsRaw)
|
||||
showAddDlg = false
|
||||
ToastUtils.showShort("添加成功")
|
||||
}
|
||||
|
|
|
@ -24,7 +24,7 @@ class AppItemVm @Inject constructor(stateHandle: SavedStateHandle) : ViewModel()
|
|||
|
||||
val subsRawFlow = subsIdToRawFlow.map(viewModelScope) { s -> s[args.subsItemId] }
|
||||
|
||||
val subsConfigsFlow = DbSet.subsConfigDao.queryGroupTypeConfig(args.subsItemId, args.appId)
|
||||
val subsConfigsFlow = DbSet.subsConfigDao.queryAppGroupTypeConfig(args.subsItemId, args.appId)
|
||||
.stateIn(viewModelScope, SharingStarted.Eagerly, emptyList())
|
||||
|
||||
val categoryConfigsFlow = DbSet.categoryConfigDao.queryConfig(args.subsItemId)
|
||||
|
|
|
@ -47,14 +47,13 @@ import com.blankj.utilcode.util.ToastUtils
|
|||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.annotation.RootNavGraph
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.serialization.encodeToString
|
||||
import li.songe.gkd.data.CategoryConfig
|
||||
import li.songe.gkd.data.SubscriptionRaw
|
||||
import li.songe.gkd.data.RawSubscription
|
||||
import li.songe.gkd.db.DbSet
|
||||
import li.songe.gkd.util.LocalNavController
|
||||
import li.songe.gkd.util.ProfileTransitions
|
||||
import li.songe.gkd.util.json
|
||||
import li.songe.gkd.util.launchTry
|
||||
import li.songe.gkd.util.updateSubscription
|
||||
|
||||
@RootNavGraph
|
||||
@Destination(style = ProfileTransitions::class)
|
||||
|
@ -71,17 +70,17 @@ fun CategoryPage(subsItemId: Long) {
|
|||
mutableStateOf(false)
|
||||
}
|
||||
val (menuCategory, setMenuCategory) = remember {
|
||||
mutableStateOf<SubscriptionRaw.Category?>(null)
|
||||
mutableStateOf<RawSubscription.RawCategory?>(null)
|
||||
}
|
||||
var editEnableCategory by remember {
|
||||
mutableStateOf<SubscriptionRaw.Category?>(null)
|
||||
mutableStateOf<RawSubscription.RawCategory?>(null)
|
||||
}
|
||||
val (editNameCategory, setEditNameCategory) = remember {
|
||||
mutableStateOf<SubscriptionRaw.Category?>(null)
|
||||
mutableStateOf<RawSubscription.RawCategory?>(null)
|
||||
}
|
||||
|
||||
val categories = subsRaw?.categories ?: emptyList()
|
||||
val categoriesGroups = subsRaw?.categoriesGroups ?: emptyMap()
|
||||
val categoriesGroups = subsRaw?.categoryToGroupsMap ?: emptyMap()
|
||||
|
||||
Scaffold(topBar = {
|
||||
TopAppBar(navigationIcon = {
|
||||
|
@ -93,7 +92,7 @@ fun CategoryPage(subsItemId: Long) {
|
|||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
}, title = { Text(text = subsRaw?.name ?: subsItemId.toString()) }, actions = {})
|
||||
}, title = { Text(text = "${subsRaw?.name ?: subsItemId}/规则类别") }, actions = {})
|
||||
}, floatingActionButton = {
|
||||
if (editable) {
|
||||
FloatingActionButton(onClick = { showAddDlg = true }) {
|
||||
|
@ -158,7 +157,7 @@ fun CategoryPage(subsItemId: Long) {
|
|||
if (categories.isEmpty()) {
|
||||
Spacer(modifier = Modifier.height(40.dp))
|
||||
Text(
|
||||
text = "此订阅暂无类别",
|
||||
text = "暂无类别",
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
@ -250,7 +249,7 @@ fun CategoryPage(subsItemId: Long) {
|
|||
}
|
||||
vm.viewModelScope.launchTry(Dispatchers.IO) {
|
||||
subsItem?.apply {
|
||||
subsFile.writeText(json.encodeToString(subsRawVal.copy(
|
||||
updateSubscription(subsRawVal.copy(
|
||||
categories = categories.toMutableList().apply {
|
||||
val i =
|
||||
categories.indexOfFirst { c -> c.key == editNameCategory.key }
|
||||
|
@ -258,7 +257,7 @@ fun CategoryPage(subsItemId: Long) {
|
|||
set(i, editNameCategory.copy(name = source))
|
||||
}
|
||||
}
|
||||
)))
|
||||
))
|
||||
DbSet.subsItemDao.update(copy(mtime = System.currentTimeMillis()))
|
||||
}
|
||||
ToastUtils.showShort("修改成功")
|
||||
|
@ -300,15 +299,15 @@ fun CategoryPage(subsItemId: Long) {
|
|||
showAddDlg = false
|
||||
vm.viewModelScope.launchTry(Dispatchers.IO) {
|
||||
subsItem?.apply {
|
||||
subsFile.writeText(json.encodeToString(subsRawVal.copy(
|
||||
updateSubscription(subsRawVal.copy(
|
||||
categories = categories.toMutableList().apply {
|
||||
add(SubscriptionRaw.Category(
|
||||
add(RawSubscription.RawCategory(
|
||||
key = (categories.maxOfOrNull { c -> c.key } ?: -1) + 1,
|
||||
name = source,
|
||||
enable = null
|
||||
))
|
||||
}
|
||||
)))
|
||||
))
|
||||
DbSet.subsItemDao.update(copy(mtime = System.currentTimeMillis()))
|
||||
ToastUtils.showShort("添加成功")
|
||||
}
|
||||
|
@ -340,9 +339,9 @@ fun CategoryPage(subsItemId: Long) {
|
|||
.clickable {
|
||||
vm.viewModelScope.launchTry(Dispatchers.IO) {
|
||||
subsItem?.apply {
|
||||
subsFile.writeText(json.encodeToString(subsRawVal.copy(
|
||||
updateSubscription(subsRawVal.copy(
|
||||
categories = subsRawVal.categories.filter { c -> c.key != menuCategory.key }
|
||||
)))
|
||||
))
|
||||
DbSet.subsItemDao.update(copy(mtime = System.currentTimeMillis()))
|
||||
}
|
||||
DbSet.categoryConfigDao.deleteByCategoryKey(
|
||||
|
|
|
@ -44,8 +44,10 @@ import com.ramcosta.composedestinations.annotation.Destination
|
|||
import com.ramcosta.composedestinations.annotation.RootNavGraph
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import li.songe.gkd.data.ClickLog
|
||||
import li.songe.gkd.data.SubsConfig
|
||||
import li.songe.gkd.db.DbSet
|
||||
import li.songe.gkd.ui.destinations.AppItemPageDestination
|
||||
import li.songe.gkd.ui.destinations.GlobalRulePageDestination
|
||||
import li.songe.gkd.util.LocalNavController
|
||||
import li.songe.gkd.util.ProfileTransitions
|
||||
import li.songe.gkd.util.appInfoCacheFlow
|
||||
|
@ -142,7 +144,7 @@ fun ClickLogPage() {
|
|||
}
|
||||
if (rule?.name != null) {
|
||||
Spacer(modifier = Modifier.width(10.dp))
|
||||
Text(text = rule.name)
|
||||
Text(text = rule.name ?: "")
|
||||
} else if ((group?.rules?.size ?: 0) > 1) {
|
||||
Spacer(modifier = Modifier.width(10.dp))
|
||||
Text(text = (if (clickLog.ruleKey != null) "key=${clickLog.ruleKey}, " else "") + "index=${clickLog.ruleIndex}")
|
||||
|
@ -179,13 +181,22 @@ fun ClickLogPage() {
|
|||
Text(text = "查看规则组", modifier = Modifier
|
||||
.clickable {
|
||||
previewTriggerLogVal.appId ?: return@clickable
|
||||
navController.navigate(
|
||||
AppItemPageDestination(
|
||||
previewTriggerLogVal.subsId,
|
||||
previewTriggerLogVal.appId,
|
||||
previewTriggerLogVal.groupKey
|
||||
if (previewTriggerLogVal.groupType == SubsConfig.AppGroupType) {
|
||||
navController.navigate(
|
||||
AppItemPageDestination(
|
||||
previewTriggerLogVal.subsId,
|
||||
previewTriggerLogVal.appId,
|
||||
previewTriggerLogVal.groupKey
|
||||
)
|
||||
)
|
||||
)
|
||||
} else if (previewTriggerLogVal.groupType == SubsConfig.GlobalGroupType) {
|
||||
navController.navigate(
|
||||
GlobalRulePageDestination(
|
||||
previewTriggerLogVal.subsId,
|
||||
previewTriggerLogVal.groupKey
|
||||
)
|
||||
)
|
||||
}
|
||||
previewClickLog = null
|
||||
}
|
||||
.fillMaxWidth()
|
||||
|
|
|
@ -6,6 +6,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel
|
|||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import li.songe.gkd.data.SubsConfig
|
||||
import li.songe.gkd.data.Tuple3
|
||||
import li.songe.gkd.db.DbSet
|
||||
import li.songe.gkd.util.subsIdToRawFlow
|
||||
|
@ -17,8 +18,12 @@ class ClickLogVm @Inject constructor() : ViewModel() {
|
|||
val clickDataListFlow =
|
||||
combine(DbSet.clickLogDao.query(), subsIdToRawFlow) { clickLogs, subsIdToRaw ->
|
||||
clickLogs.map { c ->
|
||||
val app = subsIdToRaw[c.subsId]?.apps?.find { a -> a.id == c.appId }
|
||||
val group = app?.groups?.find { g -> g.key == c.groupKey }
|
||||
val group = if (c.groupType == SubsConfig.AppGroupType) {
|
||||
val app = subsIdToRaw[c.subsId]?.apps?.find { a -> a.id == c.appId }
|
||||
app?.groups?.find { g -> g.key == c.groupKey }
|
||||
} else {
|
||||
subsIdToRaw[c.subsId]?.globalGroups?.find { g -> g.key == c.groupKey }
|
||||
}
|
||||
val rule = group?.rules?.run {
|
||||
if (c.ruleKey != null) {
|
||||
find { r -> r.key == c.ruleKey }
|
||||
|
|
|
@ -25,18 +25,17 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import com.blankj.utilcode.util.ToastUtils
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import li.songe.gkd.MainActivity
|
||||
import li.songe.gkd.appScope
|
||||
import li.songe.gkd.service.GkdAbService
|
||||
import li.songe.gkd.service.ManageService
|
||||
import li.songe.gkd.ui.component.AuthCard
|
||||
import li.songe.gkd.ui.component.TextSwitch
|
||||
import li.songe.gkd.ui.destinations.ClickLogPageDestination
|
||||
import li.songe.gkd.util.LocalNavController
|
||||
import li.songe.gkd.util.SafeR
|
||||
import li.songe.gkd.util.checkOrRequestNotifPermission
|
||||
import li.songe.gkd.util.launchTry
|
||||
import li.songe.gkd.util.navigate
|
||||
import li.songe.gkd.util.storeFlow
|
||||
|
@ -54,63 +53,29 @@ fun ControlPage() {
|
|||
val subsStatus by vm.subsStatusFlow.collectAsState()
|
||||
val store by storeFlow.collectAsState()
|
||||
|
||||
val gkdAccessRunning by usePollState { GkdAbService.isRunning() }
|
||||
val notifEnabled by usePollState {
|
||||
NotificationManagerCompat.from(context).areNotificationsEnabled()
|
||||
}
|
||||
val gkdAccessRunning by GkdAbService.isRunning.collectAsState()
|
||||
val manageRunning by ManageService.isRunning.collectAsState()
|
||||
val canDrawOverlays by usePollState { Settings.canDrawOverlays(context) }
|
||||
|
||||
|
||||
Column(
|
||||
modifier = Modifier.verticalScroll(
|
||||
state = rememberScrollState()
|
||||
)
|
||||
) {
|
||||
if (!notifEnabled) {
|
||||
AuthCard(title = "通知权限", desc = "用于启动后台服务,展示服务运行状态", onAuthClick = {
|
||||
val intent = Intent()
|
||||
intent.action = Settings.ACTION_APP_NOTIFICATION_SETTINGS
|
||||
intent.putExtra(Settings.EXTRA_APP_PACKAGE, context.packageName)
|
||||
intent.putExtra(Settings.EXTRA_CHANNEL_ID, context.applicationInfo.uid)
|
||||
context.startActivity(intent)
|
||||
})
|
||||
Divider()
|
||||
}
|
||||
|
||||
if (!gkdAccessRunning) {
|
||||
AuthCard(title = "无障碍权限",
|
||||
desc = "用于获取屏幕信息,点击屏幕上的控件",
|
||||
onAuthClick = {
|
||||
if (notifEnabled) {
|
||||
appScope.launchTry(Dispatchers.IO) {
|
||||
val intent = Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS)
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
// android.content.ActivityNotFoundException
|
||||
// https://bugly.qq.com/v2/crash-reporting/crashes/d0ce46b353/113010?pid=1
|
||||
context.startActivity(intent)
|
||||
}
|
||||
} else {
|
||||
ToastUtils.showShort("必须先开启[通知权限]")
|
||||
appScope.launchTry {
|
||||
val intent = Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS)
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
// android.content.ActivityNotFoundException
|
||||
context.startActivity(intent)
|
||||
}
|
||||
})
|
||||
Divider()
|
||||
}
|
||||
|
||||
if (!canDrawOverlays) {
|
||||
AuthCard(title = "悬浮窗权限",
|
||||
desc = "用于后台提示,显示保存快照按钮等功能",
|
||||
onAuthClick = {
|
||||
val intent = Intent(
|
||||
Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
|
||||
)
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
context.startActivity(intent)
|
||||
})
|
||||
Divider()
|
||||
}
|
||||
|
||||
if (gkdAccessRunning) {
|
||||
TextSwitch(name = "服务开启",
|
||||
} else {
|
||||
TextSwitch(
|
||||
name = "服务开启",
|
||||
desc = "保持服务开启,根据订阅规则匹配屏幕目标节点",
|
||||
checked = store.enableService,
|
||||
onCheckedChange = {
|
||||
|
@ -120,6 +85,36 @@ fun ControlPage() {
|
|||
)
|
||||
)
|
||||
})
|
||||
}
|
||||
Divider()
|
||||
|
||||
TextSwitch(
|
||||
name = "常驻通知栏",
|
||||
desc = "在通知栏显示服务运行状态,避免被某些系统回收",
|
||||
checked = manageRunning,
|
||||
onCheckedChange = {
|
||||
if (it) {
|
||||
if (!checkOrRequestNotifPermission(context)) {
|
||||
return@TextSwitch
|
||||
}
|
||||
ManageService.start(context)
|
||||
} else {
|
||||
ManageService.stop(context)
|
||||
}
|
||||
})
|
||||
Divider()
|
||||
|
||||
if (!canDrawOverlays) {
|
||||
AuthCard(
|
||||
title = "悬浮窗权限",
|
||||
desc = "用于后台提示,显示保存快照按钮等功能",
|
||||
onAuthClick = {
|
||||
val intent = Intent(
|
||||
Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
|
||||
)
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
context.startActivity(intent)
|
||||
})
|
||||
Divider()
|
||||
}
|
||||
|
||||
|
|
|
@ -7,8 +7,8 @@ import kotlinx.coroutines.flow.SharingStarted
|
|||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import li.songe.gkd.db.DbSet
|
||||
import li.songe.gkd.util.allRulesFlow
|
||||
import li.songe.gkd.util.appInfoCacheFlow
|
||||
import li.songe.gkd.util.appRuleFlow
|
||||
import li.songe.gkd.util.clickCountFlow
|
||||
import li.songe.gkd.util.subsIdToRawFlow
|
||||
import javax.inject.Inject
|
||||
|
@ -37,12 +37,13 @@ class ControlVm @Inject constructor() : ViewModel() {
|
|||
}
|
||||
}.stateIn(viewModelScope, SharingStarted.Eagerly, null)
|
||||
|
||||
val subsStatusFlow = combine(appRuleFlow, clickCountFlow) { appRule, clickCount ->
|
||||
val appSize = appRule.visibleMap.keys.size
|
||||
val groupSize =
|
||||
appRule.visibleMap.values.sumOf { rules -> rules.map { r -> r.group.key }.toSet().size }
|
||||
(if (groupSize > 0) {
|
||||
"${appSize}应用/${groupSize}规则组"
|
||||
val subsStatusFlow = combine(allRulesFlow, clickCountFlow) { allRules, clickCount ->
|
||||
(if (allRules.allGroupSize > 0) {
|
||||
if (allRules.appSize > 0) {
|
||||
"${allRules.appSize}应用/${allRules.allGroupSize}规则组"
|
||||
} else {
|
||||
"${allRules.allGroupSize}规则组"
|
||||
}
|
||||
} else {
|
||||
"暂无规则"
|
||||
}) + if (clickCount > 0) "/${clickCount}点击" else ""
|
||||
|
|
|
@ -7,8 +7,10 @@ import android.media.projection.MediaProjectionManager
|
|||
import android.provider.Settings
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
|
@ -37,7 +39,6 @@ import androidx.compose.ui.text.input.KeyboardType
|
|||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.blankj.utilcode.util.LogUtils
|
||||
import com.blankj.utilcode.util.ToastUtils
|
||||
|
@ -61,6 +62,9 @@ import li.songe.gkd.util.Ext
|
|||
import li.songe.gkd.util.LocalLauncher
|
||||
import li.songe.gkd.util.LocalNavController
|
||||
import li.songe.gkd.util.ProfileTransitions
|
||||
import li.songe.gkd.util.authActionFlow
|
||||
import li.songe.gkd.util.canDrawOverlaysAuthAction
|
||||
import li.songe.gkd.util.checkOrRequestNotifPermission
|
||||
import li.songe.gkd.util.launchAsFn
|
||||
import li.songe.gkd.util.launchTry
|
||||
import li.songe.gkd.util.navigate
|
||||
|
@ -146,20 +150,7 @@ fun DebugPage() {
|
|||
Divider()
|
||||
}
|
||||
|
||||
TextSwitch(
|
||||
name = "匹配未知应用",
|
||||
desc = "匹配不在安装列表中(其它用户空间)的应用",
|
||||
checked = store.matchUnknownApp
|
||||
) {
|
||||
updateStorage(
|
||||
storeFlow, store.copy(
|
||||
matchUnknownApp = it
|
||||
)
|
||||
)
|
||||
}
|
||||
Divider()
|
||||
|
||||
val httpServerRunning by usePollState { HttpService.isRunning() }
|
||||
val httpServerRunning by HttpService.isRunning.collectAsState()
|
||||
TextSwitch(
|
||||
name = "HTTP服务",
|
||||
desc = "开启HTTP服务, 以便在同一局域网下传递数据" + if (httpServerRunning) "\n${
|
||||
|
@ -168,8 +159,7 @@ fun DebugPage() {
|
|||
}" else "",
|
||||
checked = httpServerRunning
|
||||
) {
|
||||
if (!NotificationManagerCompat.from(context).areNotificationsEnabled()) {
|
||||
ToastUtils.showShort("需要通知权限")
|
||||
if (!checkOrRequestNotifPermission(context)) {
|
||||
return@TextSwitch
|
||||
}
|
||||
if (it) {
|
||||
|
@ -200,15 +190,18 @@ fun DebugPage() {
|
|||
}
|
||||
Divider()
|
||||
|
||||
// android 11 以上可以使用无障碍服务获取屏幕截图
|
||||
// Build.VERSION.SDK_INT < Build.VERSION_CODES.R
|
||||
val screenshotRunning by usePollState { ScreenshotService.isRunning() }
|
||||
TextSwitch(name = "截屏服务",
|
||||
SettingItem(title = "快照记录", onClick = {
|
||||
navController.navigate(SnapshotPageDestination)
|
||||
})
|
||||
Divider()
|
||||
|
||||
val screenshotRunning by ScreenshotService.isRunning.collectAsState()
|
||||
TextSwitch(
|
||||
name = "截屏服务",
|
||||
desc = "生成快照需要获取屏幕截图,Android11无需开启",
|
||||
checked = screenshotRunning,
|
||||
onCheckedChange = appScope.launchAsFn<Boolean> {
|
||||
if (!NotificationManagerCompat.from(context).areNotificationsEnabled()) {
|
||||
ToastUtils.showShort("需要通知权限")
|
||||
if (!checkOrRequestNotifPermission(context)) {
|
||||
return@launchAsFn
|
||||
}
|
||||
if (it) {
|
||||
|
@ -225,26 +218,21 @@ fun DebugPage() {
|
|||
})
|
||||
Divider()
|
||||
|
||||
|
||||
val floatingRunning by usePollState {
|
||||
FloatingService.isRunning()
|
||||
}
|
||||
val floatingRunning by FloatingService.isRunning.collectAsState()
|
||||
TextSwitch(
|
||||
name = "悬浮窗服务",
|
||||
desc = "显示截屏按钮,便于用户主动保存快照",
|
||||
checked = floatingRunning
|
||||
) {
|
||||
if (!NotificationManagerCompat.from(context).areNotificationsEnabled()) {
|
||||
ToastUtils.showShort("需要通知权限")
|
||||
if (!checkOrRequestNotifPermission(context)) {
|
||||
return@TextSwitch
|
||||
}
|
||||
|
||||
if (it) {
|
||||
if (Settings.canDrawOverlays(context)) {
|
||||
val intent = Intent(context, FloatingService::class.java)
|
||||
ContextCompat.startForegroundService(context, intent)
|
||||
} else {
|
||||
ToastUtils.showShort("需要悬浮窗权限")
|
||||
authActionFlow.value = canDrawOverlaysAuthAction
|
||||
}
|
||||
} else {
|
||||
FloatingService.stop(context)
|
||||
|
@ -266,7 +254,7 @@ fun DebugPage() {
|
|||
Divider()
|
||||
TextSwitch(
|
||||
name = "截屏快照",
|
||||
desc = "当用户截屏时保存快照(需手动替换快照图片),仅支持MIUI14",
|
||||
desc = "当用户截屏时保存快照(需手动替换快照图片),仅支持部分MIUI14",
|
||||
checked = store.captureScreenshot
|
||||
) {
|
||||
updateStorage(
|
||||
|
@ -288,11 +276,8 @@ fun DebugPage() {
|
|||
)
|
||||
)
|
||||
}
|
||||
Divider()
|
||||
|
||||
SettingItem(title = "快照记录", onClick = {
|
||||
navController.navigate(SnapshotPageDestination)
|
||||
})
|
||||
Spacer(modifier = Modifier.height(40.dp))
|
||||
}
|
||||
})
|
||||
|
||||
|
|
398
app/src/main/kotlin/li/songe/gkd/ui/GlobalRulePage.kt
Normal file
398
app/src/main/kotlin/li/songe/gkd/ui/GlobalRulePage.kt
Normal file
|
@ -0,0 +1,398 @@
|
|||
package li.songe.gkd.ui
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.defaultMinSize
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.MoreVert
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.blankj.utilcode.util.ClipboardUtils
|
||||
import com.blankj.utilcode.util.LogUtils
|
||||
import com.blankj.utilcode.util.ToastUtils
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.annotation.RootNavGraph
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import li.songe.gkd.data.RawSubscription
|
||||
import li.songe.gkd.data.SubsConfig
|
||||
import li.songe.gkd.db.DbSet
|
||||
import li.songe.gkd.ui.component.getDialogResult
|
||||
import li.songe.gkd.ui.destinations.GroupItemPageDestination
|
||||
import li.songe.gkd.util.LocalNavController
|
||||
import li.songe.gkd.util.ProfileTransitions
|
||||
import li.songe.gkd.util.encodeToJson5String
|
||||
import li.songe.gkd.util.json
|
||||
import li.songe.gkd.util.launchAsFn
|
||||
import li.songe.gkd.util.launchTry
|
||||
import li.songe.gkd.util.navigate
|
||||
import li.songe.gkd.util.updateSubscription
|
||||
|
||||
@RootNavGraph
|
||||
@Destination(style = ProfileTransitions::class)
|
||||
@Composable
|
||||
fun GlobalRulePage(subsItemId: Long, focusGroupKey: Int? = null) {
|
||||
val navController = LocalNavController.current
|
||||
val vm = hiltViewModel<GlobalRuleVm>()
|
||||
val subsItem by vm.subsItemFlow.collectAsState()
|
||||
val rawSubs = vm.subsRawFlow.collectAsState().value
|
||||
val subsConfigs by vm.subsConfigsFlow.collectAsState()
|
||||
|
||||
val editable = subsItemId < 0 && rawSubs != null && subsItem != null
|
||||
val globalGroups = rawSubs?.globalGroups ?: emptyList()
|
||||
|
||||
var showAddDlg by remember { mutableStateOf(false) }
|
||||
val (menuGroupRaw, setMenuGroupRaw) = remember {
|
||||
mutableStateOf<RawSubscription.RawGlobalGroup?>(null)
|
||||
}
|
||||
val (editGroupRaw, setEditGroupRaw) = remember {
|
||||
mutableStateOf<RawSubscription.RawGlobalGroup?>(null)
|
||||
}
|
||||
val (showGroupItem, setShowGroupItem) = remember {
|
||||
mutableStateOf<RawSubscription.RawGlobalGroup?>(
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(navigationIcon = {
|
||||
IconButton(onClick = {
|
||||
navController.popBackStack()
|
||||
}) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.ArrowBack,
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
}, title = {
|
||||
Text(text = "${rawSubs?.name ?: subsItemId}/全局规则")
|
||||
})
|
||||
}, floatingActionButton = {
|
||||
if (editable) {
|
||||
FloatingActionButton(onClick = { showAddDlg = true }) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Add,
|
||||
contentDescription = "add",
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
content = { paddingValues ->
|
||||
LazyColumn(modifier = Modifier.padding(paddingValues)) {
|
||||
items(globalGroups, { g -> g.key }) { group ->
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.background(
|
||||
if (group.key == focusGroupKey) MaterialTheme.colorScheme.inversePrimary else Color.Transparent
|
||||
)
|
||||
.clickable { setShowGroupItem(group) }
|
||||
.padding(10.dp, 6.dp)
|
||||
.fillMaxWidth()
|
||||
.height(45.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.fillMaxHeight(),
|
||||
verticalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text(
|
||||
text = group.name,
|
||||
maxLines = 1,
|
||||
softWrap = false,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
if (group.valid) {
|
||||
Text(
|
||||
text = group.desc ?: "",
|
||||
maxLines = 1,
|
||||
softWrap = false,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
fontSize = 14.sp
|
||||
)
|
||||
} else {
|
||||
Text(
|
||||
text = "规则组损坏",
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
fontSize = 14.sp,
|
||||
color = MaterialTheme.colorScheme.error
|
||||
)
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.width(10.dp))
|
||||
|
||||
if (editable) {
|
||||
IconButton(onClick = {
|
||||
setMenuGroupRaw(group)
|
||||
}) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.MoreVert,
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.width(10.dp))
|
||||
}
|
||||
|
||||
val groupEnable = subsConfigs.find { c -> c.groupKey == group.key }?.enable
|
||||
?: group.enable ?: true
|
||||
val subsConfig = subsConfigs.find { it.groupKey == group.key }
|
||||
Switch(checked = groupEnable, modifier = Modifier,
|
||||
onCheckedChange = vm.viewModelScope.launchAsFn { enable ->
|
||||
val newItem = (subsConfig?.copy(enable = enable) ?: SubsConfig(
|
||||
type = SubsConfig.GlobalGroupType,
|
||||
subsItemId = subsItemId,
|
||||
groupKey = group.key,
|
||||
enable = enable
|
||||
))
|
||||
DbSet.subsConfigDao.insert(newItem)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(40.dp))
|
||||
if (globalGroups.isEmpty()) {
|
||||
Text(
|
||||
text = "暂无规则",
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
if (showAddDlg && rawSubs != null) {
|
||||
var source by remember {
|
||||
mutableStateOf("")
|
||||
}
|
||||
AlertDialog(title = { Text(text = "添加全局规则组") }, text = {
|
||||
OutlinedTextField(
|
||||
value = source,
|
||||
onValueChange = { source = it },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
placeholder = { Text(text = "请输入规则组") },
|
||||
maxLines = 8,
|
||||
)
|
||||
}, onDismissRequest = { showAddDlg = false }, confirmButton = {
|
||||
TextButton(onClick = {
|
||||
val newGroup = try {
|
||||
RawSubscription.parseRawGlobalGroup(source)
|
||||
} catch (e: Exception) {
|
||||
ToastUtils.showShort("非法规则\n${e.message ?: e}")
|
||||
return@TextButton
|
||||
}
|
||||
if (!newGroup.valid) {
|
||||
ToastUtils.showShort("非法规则:存在非法选择器")
|
||||
return@TextButton
|
||||
}
|
||||
if (rawSubs.globalGroups.any { g -> g.name == newGroup.name }) {
|
||||
ToastUtils.showShort("存在同名规则[${newGroup.name}]")
|
||||
return@TextButton
|
||||
}
|
||||
val newKey = (rawSubs.globalGroups.maxByOrNull { g -> g.key }?.key ?: -1) + 1
|
||||
val newRawSubs = rawSubs.copy(
|
||||
globalGroups = rawSubs.globalGroups.toMutableList()
|
||||
.apply { add(newGroup.copy(key = newKey)) }
|
||||
)
|
||||
updateSubscription(newRawSubs)
|
||||
vm.viewModelScope.launchTry(Dispatchers.IO) {
|
||||
showAddDlg = false
|
||||
ToastUtils.showShort("添加成功")
|
||||
}
|
||||
}, enabled = source.isNotEmpty()) {
|
||||
Text(text = "添加")
|
||||
}
|
||||
}, dismissButton = {
|
||||
TextButton(onClick = { showAddDlg = false }) {
|
||||
Text(text = "取消")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (menuGroupRaw != null && rawSubs != null) {
|
||||
Dialog(onDismissRequest = { setMenuGroupRaw(null) }) {
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
) {
|
||||
Column {
|
||||
Text(text = "编辑", modifier = Modifier
|
||||
.clickable {
|
||||
setEditGroupRaw(menuGroupRaw)
|
||||
setMenuGroupRaw(null)
|
||||
}
|
||||
.padding(16.dp)
|
||||
.fillMaxWidth())
|
||||
Text(text = "删除", modifier = Modifier
|
||||
.clickable {
|
||||
setMenuGroupRaw(null)
|
||||
vm.viewModelScope.launchTry {
|
||||
if (!getDialogResult("是否删除${menuGroupRaw.name}")) return@launchTry
|
||||
updateSubscription(
|
||||
rawSubs.copy(
|
||||
globalGroups = rawSubs.globalGroups.filter { g -> g.key != menuGroupRaw.key }
|
||||
)
|
||||
)
|
||||
DbSet.subsItemDao.updateMtime(rawSubs.id)
|
||||
}
|
||||
}
|
||||
.padding(16.dp)
|
||||
.fillMaxWidth(),
|
||||
color = MaterialTheme.colorScheme.error
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (editGroupRaw != null && rawSubs != null) {
|
||||
var source by remember {
|
||||
mutableStateOf(json.encodeToJson5String(editGroupRaw))
|
||||
}
|
||||
val oldSource = remember { source }
|
||||
AlertDialog(
|
||||
title = { Text(text = "编辑规则组") },
|
||||
text = {
|
||||
OutlinedTextField(
|
||||
value = source,
|
||||
onValueChange = { source = it },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
placeholder = { Text(text = "请输入规则组") },
|
||||
maxLines = 8,
|
||||
)
|
||||
},
|
||||
onDismissRequest = { setEditGroupRaw(null) },
|
||||
dismissButton = {
|
||||
TextButton(onClick = { setEditGroupRaw(null) }) {
|
||||
Text(text = "取消")
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
if (oldSource == source) {
|
||||
ToastUtils.showShort("规则无变动")
|
||||
return@TextButton
|
||||
}
|
||||
val newGroupRaw = try {
|
||||
RawSubscription.parseRawGlobalGroup(source)
|
||||
} catch (e: Exception) {
|
||||
LogUtils.d(e)
|
||||
ToastUtils.showShort("非法规则:${e.message}")
|
||||
return@TextButton
|
||||
}
|
||||
if (newGroupRaw.key != editGroupRaw.key) {
|
||||
ToastUtils.showShort("不能更改规则组的key")
|
||||
return@TextButton
|
||||
}
|
||||
if (!newGroupRaw.valid) {
|
||||
ToastUtils.showShort("非法规则:存在非法选择器")
|
||||
return@TextButton
|
||||
}
|
||||
setEditGroupRaw(null)
|
||||
val newGlobalGroups = rawSubs.globalGroups.toMutableList().apply {
|
||||
val i = rawSubs.globalGroups.indexOfFirst { g -> g.key == newGroupRaw.key }
|
||||
if (i >= 0) {
|
||||
set(i, newGroupRaw)
|
||||
}
|
||||
}
|
||||
updateSubscription(rawSubs.copy(globalGroups = newGlobalGroups))
|
||||
vm.viewModelScope.launchTry(Dispatchers.IO) {
|
||||
DbSet.subsItemDao.updateMtime(rawSubs.id)
|
||||
ToastUtils.showShort("更新成功")
|
||||
}
|
||||
}, enabled = source.isNotEmpty()) {
|
||||
Text(text = "更新")
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
if (showGroupItem != null) {
|
||||
AlertDialog(modifier = Modifier.defaultMinSize(300.dp),
|
||||
onDismissRequest = { setShowGroupItem(null) },
|
||||
title = {
|
||||
Text(text = showGroupItem.name)
|
||||
},
|
||||
text = {
|
||||
Column {
|
||||
if (showGroupItem.enable == false) {
|
||||
Text(text = "该规则组默认不启用")
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
}
|
||||
Text(text = showGroupItem.desc ?: "")
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
Row {
|
||||
if (showGroupItem.allExampleUrls.isNotEmpty()) {
|
||||
TextButton(onClick = {
|
||||
setShowGroupItem(null)
|
||||
navController.navigate(
|
||||
GroupItemPageDestination(
|
||||
subsInt = subsItemId,
|
||||
groupKey = showGroupItem.key
|
||||
)
|
||||
)
|
||||
}) {
|
||||
Text(text = "查看图片")
|
||||
}
|
||||
}
|
||||
TextButton(onClick = {
|
||||
val groupAppText = json.encodeToJson5String(showGroupItem)
|
||||
ClipboardUtils.copyText(groupAppText)
|
||||
ToastUtils.showShort("复制成功")
|
||||
}) {
|
||||
Text(text = "复制规则组")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
26
app/src/main/kotlin/li/songe/gkd/ui/GlobalRuleVm.kt
Normal file
26
app/src/main/kotlin/li/songe/gkd/ui/GlobalRuleVm.kt
Normal file
|
@ -0,0 +1,26 @@
|
|||
package li.songe.gkd.ui
|
||||
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import li.songe.gkd.db.DbSet
|
||||
import li.songe.gkd.ui.destinations.GlobalRulePageDestination
|
||||
import li.songe.gkd.util.map
|
||||
import li.songe.gkd.util.subsIdToRawFlow
|
||||
import li.songe.gkd.util.subsItemsFlow
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class GlobalRuleVm @Inject constructor(stateHandle: SavedStateHandle) : ViewModel() {
|
||||
private val args = GlobalRulePageDestination.argsFrom(stateHandle)
|
||||
val subsItemFlow =
|
||||
subsItemsFlow.map(viewModelScope) { s -> s.find { v -> v.id == args.subsItemId } }
|
||||
val subsRawFlow = subsIdToRawFlow.map(viewModelScope) { s -> s[args.subsItemId] }
|
||||
|
||||
val subsConfigsFlow = DbSet.subsConfigDao.queryGlobalGroupTypeConfig(args.subsItemId)
|
||||
.stateIn(viewModelScope, SharingStarted.Eagerly, emptyList())
|
||||
|
||||
}
|
|
@ -20,7 +20,6 @@ import androidx.compose.material3.TopAppBarDefaults
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
|
@ -30,6 +29,7 @@ import coil.compose.SubcomposeAsyncImage
|
|||
import coil.request.ImageRequest
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.annotation.RootNavGraph
|
||||
import li.songe.gkd.data.RawSubscription
|
||||
import li.songe.gkd.util.LocalNavController
|
||||
import li.songe.gkd.util.appInfoCacheFlow
|
||||
import li.songe.gkd.util.imageLoader
|
||||
|
@ -39,14 +39,20 @@ import li.songe.gkd.util.subsIdToRawFlow
|
|||
@RootNavGraph
|
||||
@Destination
|
||||
@Composable
|
||||
fun GroupItemPage(subsInt: Long, appId: String, groupKey: Int) {
|
||||
fun GroupItemPage(subsInt: Long, groupKey: Int, appId: String? = null) {
|
||||
val navController = LocalNavController.current
|
||||
val subsIdToRawState = subsIdToRawFlow.collectAsState()
|
||||
val appRaw = remember {
|
||||
subsIdToRawState.value[subsInt]?.apps?.first { a -> a.id == appId }
|
||||
val subsIdToRaw by subsIdToRawFlow.collectAsState()
|
||||
val rawSubs = subsIdToRaw[subsInt]
|
||||
val rawApp = rawSubs?.apps?.first { a -> a.id == appId }
|
||||
val group = if (appId == null) {
|
||||
rawSubs?.globalGroups?.find { g -> g.key == groupKey }
|
||||
} else {
|
||||
rawApp?.groups?.find { g -> g.key == groupKey }
|
||||
}
|
||||
val group = remember {
|
||||
appRaw?.groups?.find { g -> g.key == groupKey }
|
||||
val allExampleUrls = when (group) {
|
||||
is RawSubscription.RawAppGroup -> group.allExampleUrls
|
||||
is RawSubscription.RawGlobalGroup -> group.allExampleUrls
|
||||
else -> emptyList()
|
||||
}
|
||||
val appInfoCache by appInfoCacheFlow.collectAsState()
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
|
@ -62,10 +68,26 @@ fun GroupItemPage(subsInt: Long, appId: String, groupKey: Int) {
|
|||
}
|
||||
},
|
||||
title = {
|
||||
Text(
|
||||
text = ((appInfoCache[appId]?.name ?: appRaw?.name
|
||||
?: appId) + "/" + (group?.name ?: "未知规则"))
|
||||
)
|
||||
when (group) {
|
||||
is RawSubscription.RawAppGroup -> {
|
||||
Text(
|
||||
text = ((rawSubs?.name
|
||||
?: subsInt.toString()) + (appInfoCache[appId]?.name ?: rawApp?.name
|
||||
?: appId) + "/" + (group.name))
|
||||
)
|
||||
}
|
||||
|
||||
is RawSubscription.RawGlobalGroup -> {
|
||||
Text(
|
||||
text = "${rawSubs?.name ?: subsInt}/${group.name}"
|
||||
)
|
||||
}
|
||||
|
||||
else -> {
|
||||
Text(text = "未知规则")
|
||||
}
|
||||
|
||||
}
|
||||
},
|
||||
actions = {},
|
||||
modifier = Modifier.zIndex(1f),
|
||||
|
@ -74,11 +96,11 @@ fun GroupItemPage(subsInt: Long, appId: String, groupKey: Int) {
|
|||
)
|
||||
)
|
||||
if (group != null) {
|
||||
val state = rememberPagerState { group.allExampleUrls.size }
|
||||
val state = rememberPagerState { allExampleUrls.size }
|
||||
HorizontalPager(
|
||||
modifier = Modifier.fillMaxSize(), state = state
|
||||
) { p ->
|
||||
val url = group.allExampleUrls.getOrNull(p)
|
||||
val url = allExampleUrls.getOrNull(p)
|
||||
if (url != null) {
|
||||
SubcomposeAsyncImage(
|
||||
model = ImageRequest.Builder(LocalContext.current).data(url)
|
||||
|
|
|
@ -2,7 +2,6 @@ package li.songe.gkd.ui
|
|||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.blankj.utilcode.util.LogUtils
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import io.ktor.client.call.body
|
||||
import io.ktor.client.plugins.onUpload
|
||||
|
@ -16,22 +15,21 @@ import kotlinx.coroutines.Job
|
|||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.encodeToString
|
||||
import li.songe.gkd.appScope
|
||||
import li.songe.gkd.data.GithubPoliciesAsset
|
||||
import li.songe.gkd.data.RpcError
|
||||
import li.songe.gkd.data.SubsItem
|
||||
import li.songe.gkd.data.SubscriptionRaw
|
||||
import li.songe.gkd.db.DbSet
|
||||
import li.songe.gkd.debug.SnapshotExt
|
||||
import li.songe.gkd.util.FILE_UPLOAD_URL
|
||||
import li.songe.gkd.util.LoadStatus
|
||||
import li.songe.gkd.util.authActionFlow
|
||||
import li.songe.gkd.util.checkUpdate
|
||||
import li.songe.gkd.util.client
|
||||
import li.songe.gkd.util.dbFolder
|
||||
import li.songe.gkd.util.initFolder
|
||||
import li.songe.gkd.util.json
|
||||
import li.songe.gkd.util.launchTry
|
||||
import li.songe.gkd.util.logZipDir
|
||||
import li.songe.gkd.util.newVersionApkDir
|
||||
import li.songe.gkd.util.snapshotZipDir
|
||||
import li.songe.gkd.util.storeFlow
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
|
@ -48,34 +46,6 @@ class HomePageVm @Inject constructor() : ViewModel() {
|
|||
if (!DbSet.subsItemDao.query().first().any { s -> s.id == localSubsItem.id }) {
|
||||
DbSet.subsItemDao.insert(localSubsItem)
|
||||
}
|
||||
if (!localSubsItem.subsFile.exists()) {
|
||||
localSubsItem.subsFile.writeText(
|
||||
json.encodeToString(
|
||||
SubscriptionRaw(
|
||||
id = localSubsItem.id,
|
||||
name = "本地订阅",
|
||||
version = 0,
|
||||
author = "gkd",
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
appScope.launchTry(Dispatchers.IO) {
|
||||
// 迁移快照记录
|
||||
val oldDbFile = File(dbFolder, "snapshot.db")
|
||||
if (oldDbFile.exists()) {
|
||||
SnapshotExt.snapshotDir.walk().maxDepth(1).filter { f -> f.isDirectory }
|
||||
.mapNotNull { f -> f.name.toLongOrNull() }.forEach { snapshotId ->
|
||||
DbSet.snapshotDao.insertOrIgnore(
|
||||
json.decodeFromString(
|
||||
File(SnapshotExt.getSnapshotPath(snapshotId)).readText()
|
||||
)
|
||||
)
|
||||
}
|
||||
oldDbFile.delete()
|
||||
LogUtils.d("执行快照迁移")
|
||||
}
|
||||
}
|
||||
|
||||
if (storeFlow.value.autoCheckAppUpdate) {
|
||||
|
@ -88,6 +58,19 @@ class HomePageVm @Inject constructor() : ViewModel() {
|
|||
}
|
||||
}
|
||||
|
||||
viewModelScope.launchTry(Dispatchers.IO) {
|
||||
// 每次进入删除缓存
|
||||
listOf(snapshotZipDir, newVersionApkDir, logZipDir).forEach { dir ->
|
||||
if (dir.isDirectory && dir.exists()) {
|
||||
dir.listFiles()?.forEach { file ->
|
||||
if (file.isFile) {
|
||||
file.delete()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
viewModelScope.launchTry(Dispatchers.IO) {
|
||||
// 在某些机型由于未知原因创建失败
|
||||
// 在此保证每次重新打开APP都能重新检测创建
|
||||
|
@ -129,4 +112,9 @@ class HomePageVm @Inject constructor() : ViewModel() {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
authActionFlow.value = null
|
||||
}
|
||||
}
|
|
@ -31,7 +31,6 @@ fun ImagePreviewPage(
|
|||
title: String? = null,
|
||||
) {
|
||||
val navController = LocalNavController.current
|
||||
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
TopAppBar(
|
||||
navigationIcon = {
|
||||
|
|
|
@ -55,6 +55,8 @@ import li.songe.gkd.ui.destinations.DebugPageDestination
|
|||
import li.songe.gkd.util.LoadStatus
|
||||
import li.songe.gkd.util.LocalNavController
|
||||
import li.songe.gkd.util.SafeR
|
||||
import li.songe.gkd.util.authActionFlow
|
||||
import li.songe.gkd.util.canDrawOverlaysAuthAction
|
||||
import li.songe.gkd.util.checkUpdate
|
||||
import li.songe.gkd.util.checkUpdatingFlow
|
||||
import li.songe.gkd.util.launchTry
|
||||
|
@ -110,8 +112,8 @@ fun SettingsPage() {
|
|||
})
|
||||
Divider()
|
||||
|
||||
TextSwitch(name = "无障碍前台",
|
||||
desc = "添加前台透明悬浮窗,关闭可能导致不工作",
|
||||
TextSwitch(name = "前台悬浮窗",
|
||||
desc = "添加前台透明悬浮窗,关闭可能导致不点击/点击缓慢",
|
||||
checked = store.enableAbFloatWindow,
|
||||
onCheckedChange = {
|
||||
updateStorage(
|
||||
|
@ -129,14 +131,15 @@ fun SettingsPage() {
|
|||
showToastInputDlg = true
|
||||
},
|
||||
onCheckedChange = {
|
||||
if (it && !Settings.canDrawOverlays(context)) {
|
||||
authActionFlow.value = canDrawOverlaysAuthAction
|
||||
return@TextSwitch
|
||||
}
|
||||
updateStorage(
|
||||
storeFlow, store.copy(
|
||||
toastWhenClick = it
|
||||
)
|
||||
)
|
||||
if (it && !Settings.canDrawOverlays(context)) {
|
||||
ToastUtils.showShort("需要悬浮窗权限")
|
||||
}
|
||||
})
|
||||
Divider()
|
||||
|
||||
|
|
|
@ -53,19 +53,22 @@ import com.blankj.utilcode.util.ClipboardUtils
|
|||
import com.blankj.utilcode.util.ToastUtils
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import li.songe.gkd.data.RawSubscription
|
||||
import li.songe.gkd.data.SubsItem
|
||||
import li.songe.gkd.data.SubscriptionRaw
|
||||
import li.songe.gkd.db.DbSet
|
||||
import li.songe.gkd.ui.component.SubsItemCard
|
||||
import li.songe.gkd.ui.destinations.CategoryPageDestination
|
||||
import li.songe.gkd.ui.destinations.GlobalRulePageDestination
|
||||
import li.songe.gkd.ui.destinations.SubsPageDestination
|
||||
import li.songe.gkd.util.DEFAULT_SUBS_UPDATE_URL
|
||||
import li.songe.gkd.util.LocalNavController
|
||||
import li.songe.gkd.util.SafeR
|
||||
import li.songe.gkd.util.formatTimeAgo
|
||||
import li.songe.gkd.util.launchAsFn
|
||||
import li.songe.gkd.util.launchTry
|
||||
import li.songe.gkd.util.navigate
|
||||
import li.songe.gkd.util.shareFile
|
||||
import li.songe.gkd.util.subsFolder
|
||||
import li.songe.gkd.util.subsIdToRawFlow
|
||||
import li.songe.gkd.util.subsItemsFlow
|
||||
import org.burnoutcrew.reorderable.ReorderableItem
|
||||
|
@ -104,7 +107,7 @@ fun SubsManagePage() {
|
|||
var link by remember { mutableStateOf("") }
|
||||
|
||||
val (showSubsRaw, setShowSubsRaw) = remember {
|
||||
mutableStateOf<SubscriptionRaw?>(null)
|
||||
mutableStateOf<RawSubscription?>(null)
|
||||
}
|
||||
|
||||
|
||||
|
@ -171,17 +174,14 @@ fun SubsManagePage() {
|
|||
.animateItemPlacement()
|
||||
.padding(vertical = 3.dp, horizontal = 8.dp)
|
||||
.clickable {
|
||||
navController.navigate(SubsPageDestination(subItem.id))
|
||||
menuSubItem = subItem
|
||||
},
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
) {
|
||||
SubsItemCard(
|
||||
subsItem = subItem,
|
||||
subscriptionRaw = subsIdToRaw[subItem.id],
|
||||
rawSubscription = subsIdToRaw[subItem.id],
|
||||
index = index + 1,
|
||||
onMenuClick = {
|
||||
menuSubItem = subItem
|
||||
},
|
||||
onCheckedChange = vm.viewModelScope.launchAsFn<Boolean> {
|
||||
DbSet.subsItemDao.update(subItem.copy(enable = it))
|
||||
},
|
||||
|
@ -210,7 +210,7 @@ fun SubsManagePage() {
|
|||
Column {
|
||||
val subsRawVal = subsIdToRaw[menuSubItemVal.id]
|
||||
if (subsRawVal != null) {
|
||||
Text(text = "查看规则", modifier = Modifier
|
||||
Text(text = "应用规则", modifier = Modifier
|
||||
.clickable {
|
||||
menuSubItem = null
|
||||
navController.navigate(SubsPageDestination(subsRawVal.id))
|
||||
|
@ -226,12 +226,23 @@ fun SubsManagePage() {
|
|||
.fillMaxWidth()
|
||||
.padding(16.dp))
|
||||
Divider()
|
||||
Text(text = "全局规则", modifier = Modifier
|
||||
.clickable {
|
||||
menuSubItem = null
|
||||
navController.navigate(GlobalRulePageDestination(subsRawVal.id))
|
||||
}
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp))
|
||||
Divider()
|
||||
}
|
||||
if (menuSubItemVal.id < 0 && subsRawVal != null && menuSubItemVal.subsFile.exists()) {
|
||||
if (menuSubItemVal.id < 0 && subsRawVal != null) {
|
||||
Text(text = "分享文件", modifier = Modifier
|
||||
.clickable {
|
||||
menuSubItem = null
|
||||
context.shareFile(menuSubItemVal.subsFile, "分享订阅文件")
|
||||
vm.viewModelScope.launchTry {
|
||||
val subsFile = subsFolder.resolve("${menuSubItemVal.id}.json")
|
||||
context.shareFile(subsFile, "分享订阅文件")
|
||||
}
|
||||
}
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp))
|
||||
|
|
|
@ -13,16 +13,15 @@ import kotlinx.coroutines.Dispatchers
|
|||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import li.songe.gkd.data.RawSubscription
|
||||
import li.songe.gkd.data.SubsItem
|
||||
import li.songe.gkd.data.SubsVersion
|
||||
import li.songe.gkd.data.SubscriptionRaw
|
||||
import li.songe.gkd.db.DbSet
|
||||
import li.songe.gkd.util.client
|
||||
import li.songe.gkd.util.launchTry
|
||||
import li.songe.gkd.util.subsIdToRawFlow
|
||||
import li.songe.gkd.util.subsItemsFlow
|
||||
import java.io.File
|
||||
import li.songe.gkd.util.updateSubscription
|
||||
import javax.inject.Inject
|
||||
|
||||
|
||||
|
@ -51,7 +50,7 @@ class SubsManageVm @Inject constructor() : ViewModel() {
|
|||
return@launchTry
|
||||
}
|
||||
val newSubsRaw = try {
|
||||
SubscriptionRaw.parse(text)
|
||||
RawSubscription.parse(text)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
ToastUtils.showShort("解析订阅文件失败")
|
||||
|
@ -70,18 +69,7 @@ class SubsManageVm @Inject constructor() : ViewModel() {
|
|||
updateUrl = newSubsRaw.updateUrl ?: url,
|
||||
order = if (subItems.isEmpty()) 1 else (subItems.maxBy { it.order }.order + 1)
|
||||
)
|
||||
withContext(Dispatchers.IO) {
|
||||
val parentPath = newItem.subsFile.parent
|
||||
if (parentPath != null) {
|
||||
// https://bugly.qq.com/v2/crash-reporting/crashes/d0ce46b353/109028?pid=1
|
||||
File(parentPath).apply {
|
||||
if (!exists()) {
|
||||
mkdirs()
|
||||
}
|
||||
}
|
||||
}
|
||||
newItem.subsFile.writeText(text)
|
||||
}
|
||||
updateSubscription(newSubsRaw)
|
||||
DbSet.subsItemDao.insert(newItem)
|
||||
ToastUtils.showShort("成功添加订阅")
|
||||
} finally {
|
||||
|
@ -112,7 +100,7 @@ class SubsManageVm @Inject constructor() : ViewModel() {
|
|||
LogUtils.d("快速检测更新失败", oldItem, e)
|
||||
}
|
||||
}
|
||||
val newSubsRaw = SubscriptionRaw.parse(
|
||||
val newSubsRaw = RawSubscription.parse(
|
||||
client.get(oldItem.updateUrl).bodyAsText()
|
||||
)
|
||||
if (oldSubsRaw != null && newSubsRaw.version <= oldSubsRaw.version) {
|
||||
|
@ -122,13 +110,7 @@ class SubsManageVm @Inject constructor() : ViewModel() {
|
|||
updateUrl = newSubsRaw.updateUrl ?: oldItem.updateUrl,
|
||||
mtime = System.currentTimeMillis(),
|
||||
)
|
||||
withContext(Dispatchers.IO) {
|
||||
newItem.subsFile.writeText(
|
||||
SubscriptionRaw.stringify(
|
||||
newSubsRaw
|
||||
)
|
||||
)
|
||||
}
|
||||
updateSubscription(newSubsRaw)
|
||||
newItem
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
|
|
|
@ -51,9 +51,8 @@ import com.blankj.utilcode.util.ToastUtils
|
|||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.annotation.RootNavGraph
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.serialization.encodeToString
|
||||
import li.songe.gkd.data.RawSubscription
|
||||
import li.songe.gkd.data.SubsConfig
|
||||
import li.songe.gkd.data.SubscriptionRaw
|
||||
import li.songe.gkd.db.DbSet
|
||||
import li.songe.gkd.ui.component.AppBarTextField
|
||||
import li.songe.gkd.ui.component.SubsAppCard
|
||||
|
@ -66,6 +65,7 @@ import li.songe.gkd.util.json
|
|||
import li.songe.gkd.util.launchAsFn
|
||||
import li.songe.gkd.util.launchTry
|
||||
import li.songe.gkd.util.navigate
|
||||
import li.songe.gkd.util.updateSubscription
|
||||
|
||||
|
||||
@RootNavGraph
|
||||
|
@ -92,11 +92,11 @@ fun SubsPage(
|
|||
mutableStateOf(false)
|
||||
}
|
||||
|
||||
var menuAppRaw by remember {
|
||||
mutableStateOf<SubscriptionRaw.AppRaw?>(null)
|
||||
var menuRawApp by remember {
|
||||
mutableStateOf<RawSubscription.RawApp?>(null)
|
||||
}
|
||||
var editAppRaw by remember {
|
||||
mutableStateOf<SubscriptionRaw.AppRaw?>(null)
|
||||
var editRawApp by remember {
|
||||
mutableStateOf<RawSubscription.RawApp?>(null)
|
||||
}
|
||||
|
||||
var showSearchBar by rememberSaveable {
|
||||
|
@ -130,7 +130,7 @@ fun SubsPage(
|
|||
modifier = Modifier.focusRequester(focusRequester)
|
||||
)
|
||||
} else {
|
||||
Text(text = subsRaw?.name ?: subsItem?.id.toString())
|
||||
Text(text = "${subsRaw?.name ?: subsItemId}/应用规则")
|
||||
}
|
||||
}, actions = {
|
||||
if (showSearchBar) {
|
||||
|
@ -168,7 +168,8 @@ fun SubsPage(
|
|||
) {
|
||||
itemsIndexed(appAndConfigs, { i, a -> i.toString() + a.t0.id }) { _, a ->
|
||||
val (appRaw, subsConfig, enableSize) = a
|
||||
SubsAppCard(appRaw = appRaw,
|
||||
SubsAppCard(
|
||||
rawApp = appRaw,
|
||||
appInfo = appInfoCache[appRaw.id],
|
||||
subsConfig = subsConfig,
|
||||
enableSize = enableSize,
|
||||
|
@ -188,7 +189,7 @@ fun SubsPage(
|
|||
},
|
||||
showMenu = editable,
|
||||
onMenuClick = {
|
||||
menuAppRaw = appRaw
|
||||
menuRawApp = appRaw
|
||||
})
|
||||
}
|
||||
item {
|
||||
|
@ -201,7 +202,7 @@ fun SubsPage(
|
|||
if (searchStr.isNotEmpty()) {
|
||||
Text(text = "暂无搜索结果")
|
||||
} else {
|
||||
Text(text = "此订阅暂无规则")
|
||||
Text(text = "暂无规则")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -230,7 +231,7 @@ fun SubsPage(
|
|||
}, onDismissRequest = { showAddDlg = false }, confirmButton = {
|
||||
TextButton(onClick = {
|
||||
val newAppRaw = try {
|
||||
SubscriptionRaw.parseAppRaw(source)
|
||||
RawSubscription.parseRawApp(source)
|
||||
} catch (e: Exception) {
|
||||
LogUtils.d(e)
|
||||
ToastUtils.showShort("非法规则${e.message}")
|
||||
|
@ -281,11 +282,9 @@ fun SubsPage(
|
|||
}
|
||||
}
|
||||
vm.viewModelScope.launchTry {
|
||||
subsItemVal.subsFile.writeText(
|
||||
SubscriptionRaw.stringify(
|
||||
subsRaw.copy(
|
||||
apps = newApps, version = subsRaw.version + 1
|
||||
)
|
||||
updateSubscription(
|
||||
subsRaw.copy(
|
||||
apps = newApps, version = subsRaw.version + 1
|
||||
)
|
||||
)
|
||||
DbSet.subsItemDao.update(subsItemVal.copy(mtime = System.currentTimeMillis()))
|
||||
|
@ -302,7 +301,7 @@ fun SubsPage(
|
|||
})
|
||||
}
|
||||
|
||||
val editAppRawVal = editAppRaw
|
||||
val editAppRawVal = editRawApp
|
||||
if (editAppRawVal != null && subsItemVal != null && subsRaw != null) {
|
||||
var source by remember {
|
||||
mutableStateOf(json.encodeToJson5String(editAppRawVal))
|
||||
|
@ -315,27 +314,25 @@ fun SubsPage(
|
|||
modifier = Modifier.fillMaxWidth(),
|
||||
placeholder = { Text(text = "请输入规则") },
|
||||
)
|
||||
}, onDismissRequest = { editAppRaw = null }, confirmButton = {
|
||||
}, onDismissRequest = { editRawApp = null }, confirmButton = {
|
||||
TextButton(onClick = {
|
||||
try {
|
||||
val newAppRaw = SubscriptionRaw.parseAppRaw(source)
|
||||
val newAppRaw = RawSubscription.parseRawApp(source)
|
||||
if (newAppRaw.id != editAppRawVal.id) {
|
||||
ToastUtils.showShort("不允许修改规则id")
|
||||
return@TextButton
|
||||
}
|
||||
val oldAppRawIndex = subsRaw.apps.indexOfFirst { a -> a.id == editAppRawVal.id }
|
||||
vm.viewModelScope.launchTry {
|
||||
subsItemVal.subsFile.writeText(
|
||||
SubscriptionRaw.stringify(
|
||||
subsRaw.copy(
|
||||
apps = subsRaw.apps.toMutableList().apply {
|
||||
set(oldAppRawIndex, newAppRaw)
|
||||
}, version = subsRaw.version + 1
|
||||
)
|
||||
updateSubscription(
|
||||
subsRaw.copy(
|
||||
apps = subsRaw.apps.toMutableList().apply {
|
||||
set(oldAppRawIndex, newAppRaw)
|
||||
}, version = subsRaw.version + 1
|
||||
)
|
||||
)
|
||||
DbSet.subsItemDao.update(subsItemVal.copy(mtime = System.currentTimeMillis()))
|
||||
editAppRaw = null
|
||||
editRawApp = null
|
||||
ToastUtils.showShort("更新成功")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
|
@ -346,16 +343,16 @@ fun SubsPage(
|
|||
Text(text = "添加")
|
||||
}
|
||||
}, dismissButton = {
|
||||
TextButton(onClick = { editAppRaw = null }) {
|
||||
TextButton(onClick = { editRawApp = null }) {
|
||||
Text(text = "取消")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
val menuAppRawVal = menuAppRaw
|
||||
val menuAppRawVal = menuRawApp
|
||||
if (menuAppRawVal != null && subsItemVal != null && subsRaw != null) {
|
||||
Dialog(onDismissRequest = { menuAppRaw = null }) {
|
||||
Dialog(onDismissRequest = { menuRawApp = null }) {
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
|
@ -363,33 +360,31 @@ fun SubsPage(
|
|||
shape = RoundedCornerShape(16.dp),
|
||||
) {
|
||||
Column {
|
||||
Text(text = "复制", modifier = Modifier
|
||||
.clickable {
|
||||
ClipboardUtils.copyText(
|
||||
json.encodeToJson5String(menuAppRawVal)
|
||||
)
|
||||
ToastUtils.showShort("复制成功")
|
||||
menuAppRaw = null
|
||||
}
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp))
|
||||
Text(text = "删除", modifier = Modifier
|
||||
.clickable {
|
||||
// 也许需要二次确认
|
||||
vm.viewModelScope.launchTry(Dispatchers.IO) {
|
||||
subsItemVal.subsFile.writeText(
|
||||
json.encodeToString(
|
||||
subsRaw.copy(apps = subsRaw.apps.filter { a -> a.id != menuAppRawVal.id })
|
||||
)
|
||||
Text(
|
||||
text = "复制", modifier = Modifier
|
||||
.clickable {
|
||||
ClipboardUtils.copyText(
|
||||
json.encodeToJson5String(menuAppRawVal)
|
||||
)
|
||||
DbSet.subsItemDao.update(subsItemVal.copy(mtime = System.currentTimeMillis()))
|
||||
DbSet.subsConfigDao.delete(subsItemVal.id, menuAppRawVal.id)
|
||||
ToastUtils.showShort("删除成功")
|
||||
ToastUtils.showShort("复制成功")
|
||||
menuRawApp = null
|
||||
}
|
||||
menuAppRaw = null
|
||||
}
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp), color = MaterialTheme.colorScheme.error)
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp))
|
||||
Text(
|
||||
text = "删除", modifier = Modifier
|
||||
.clickable {
|
||||
// 也许需要二次确认
|
||||
vm.viewModelScope.launchTry(Dispatchers.IO) {
|
||||
updateSubscription(subsRaw.copy(apps = subsRaw.apps.filter { a -> a.id != menuAppRawVal.id }))
|
||||
DbSet.subsItemDao.update(subsItemVal.copy(mtime = System.currentTimeMillis()))
|
||||
DbSet.subsConfigDao.delete(subsItemVal.id, menuAppRawVal.id)
|
||||
ToastUtils.showShort("删除成功")
|
||||
}
|
||||
menuRawApp = null
|
||||
}
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp), color = MaterialTheme.colorScheme.error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,8 +9,8 @@ import kotlinx.coroutines.flow.SharingStarted
|
|||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import li.songe.gkd.data.RawSubscription
|
||||
import li.songe.gkd.data.SubsConfig
|
||||
import li.songe.gkd.data.SubscriptionRaw
|
||||
import li.songe.gkd.data.Tuple3
|
||||
import li.songe.gkd.db.DbSet
|
||||
import li.songe.gkd.ui.destinations.SubsPageDestination
|
||||
|
@ -82,7 +82,7 @@ class SubsVm @Inject constructor(stateHandle: SavedStateHandle) : ViewModel() {
|
|||
if (searchStr.isBlank()) {
|
||||
appAndConfigs
|
||||
} else {
|
||||
val results = mutableListOf<Tuple3<SubscriptionRaw.AppRaw, SubsConfig?, Int>>()
|
||||
val results = mutableListOf<Tuple3<RawSubscription.RawApp, SubsConfig?, Int>>()
|
||||
val remnantList = appAndConfigs.toMutableList()
|
||||
//1. 搜索已安装应用名称
|
||||
remnantList.toList().apply { remnantList.clear() }.forEach { a ->
|
||||
|
|
|
@ -12,14 +12,14 @@ import kotlin.coroutines.resume
|
|||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
|
||||
data class DialogParams(
|
||||
private data class DialogParams(
|
||||
val title: String,
|
||||
val text: String? = null,
|
||||
val resolve: () -> Unit,
|
||||
val reject: () -> Unit
|
||||
)
|
||||
|
||||
val dialogParamsFlow = MutableStateFlow<DialogParams?>(null)
|
||||
private val dialogParamsFlow = MutableStateFlow<DialogParams?>(null)
|
||||
|
||||
@Composable
|
||||
fun ConfirmDialog() {
|
||||
|
|
|
@ -27,17 +27,17 @@ import androidx.compose.ui.text.style.TextOverflow
|
|||
import androidx.compose.ui.unit.dp
|
||||
import com.google.accompanist.drawablepainter.rememberDrawablePainter
|
||||
import li.songe.gkd.data.AppInfo
|
||||
import li.songe.gkd.data.RawSubscription
|
||||
import li.songe.gkd.data.SubsConfig
|
||||
import li.songe.gkd.data.SubscriptionRaw
|
||||
import li.songe.gkd.util.SafeR
|
||||
|
||||
|
||||
@Composable
|
||||
fun SubsAppCard(
|
||||
appRaw: SubscriptionRaw.AppRaw,
|
||||
rawApp: RawSubscription.RawApp,
|
||||
appInfo: AppInfo? = null,
|
||||
subsConfig: SubsConfig? = null,
|
||||
enableSize: Int = appRaw.groups.count { g -> g.enable ?: true },
|
||||
enableSize: Int = rawApp.groups.count { g -> g.enable ?: true },
|
||||
onClick: (() -> Unit)? = null,
|
||||
showMenu: Boolean = false,
|
||||
onMenuClick: (() -> Unit)? = null,
|
||||
|
@ -72,7 +72,7 @@ fun SubsAppCard(
|
|||
verticalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text(
|
||||
text = appInfo?.name ?: appRaw.name ?: appRaw.id,
|
||||
text = appInfo?.name ?: rawApp.name ?: rawApp.id,
|
||||
maxLines = 1,
|
||||
softWrap = false,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
|
@ -80,9 +80,9 @@ fun SubsAppCard(
|
|||
)
|
||||
|
||||
val enableDesc = when (enableSize) {
|
||||
0 -> "${appRaw.groups.size}组规则/${appRaw.groups.size}关闭"
|
||||
appRaw.groups.size -> "${appRaw.groups.size}组规则"
|
||||
else -> "${appRaw.groups.size}组规则/${enableSize}启用/${appRaw.groups.size - enableSize}关闭"
|
||||
0 -> "${rawApp.groups.size}组规则/${rawApp.groups.size}关闭"
|
||||
rawApp.groups.size -> "${rawApp.groups.size}组规则"
|
||||
else -> "${rawApp.groups.size}组规则/${enableSize}启用/${rawApp.groups.size - enableSize}关闭"
|
||||
}
|
||||
Text(
|
||||
text = enableDesc,
|
||||
|
@ -107,7 +107,7 @@ fun SubsAppCard(
|
|||
}
|
||||
|
||||
Switch(
|
||||
subsConfig?.enable ?: true,
|
||||
subsConfig?.enable ?: (appInfo != null),
|
||||
onValueChange,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,18 +1,11 @@
|
|||
package li.songe.gkd.ui.component
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.MoreVert
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
|
@ -21,8 +14,8 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import li.songe.gkd.data.RawSubscription
|
||||
import li.songe.gkd.data.SubsItem
|
||||
import li.songe.gkd.data.SubscriptionRaw
|
||||
import li.songe.gkd.util.formatTimeAgo
|
||||
import li.songe.gkd.util.safeRemoteBaseUrls
|
||||
|
||||
|
@ -30,94 +23,88 @@ import li.songe.gkd.util.safeRemoteBaseUrls
|
|||
@Composable
|
||||
fun SubsItemCard(
|
||||
subsItem: SubsItem,
|
||||
subscriptionRaw: SubscriptionRaw?,
|
||||
rawSubscription: RawSubscription?,
|
||||
index: Int,
|
||||
onMenuClick: (() -> Unit)? = null,
|
||||
onCheckedChange: ((Boolean) -> Unit)? = null,
|
||||
) {
|
||||
Box {
|
||||
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) {
|
||||
if (subsItem.id < 0) {
|
||||
Text(text = "本地来源", fontSize = 12.sp)
|
||||
} else if (subsItem.updateUrl != null && safeRemoteBaseUrls.any { s ->
|
||||
subsItem.updateUrl.startsWith(
|
||||
s
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(8.dp)
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
if (rawSubscription != null) {
|
||||
Row {
|
||||
Text(
|
||||
text = index.toString() + ". " + (rawSubscription.name),
|
||||
maxLines = 1,
|
||||
softWrap = false,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}) {
|
||||
Text(text = "可信来源", fontSize = 12.sp)
|
||||
} else {
|
||||
Text(text = "未知来源", fontSize = 12.sp)
|
||||
}
|
||||
Spacer(modifier = Modifier.width(10.dp))
|
||||
}
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(8.dp)
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
if (subscriptionRaw != null) {
|
||||
Row {
|
||||
}
|
||||
Spacer(modifier = Modifier.height(5.dp))
|
||||
Row {
|
||||
Text(
|
||||
text = formatTimeAgo(subsItem.mtime),
|
||||
maxLines = 1,
|
||||
softWrap = false,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
fontSize = 14.sp
|
||||
)
|
||||
Spacer(modifier = Modifier.width(10.dp))
|
||||
val sourceText =
|
||||
if (subsItem.id < 0) {
|
||||
"本地来源"
|
||||
} else if (subsItem.updateUrl != null && safeRemoteBaseUrls.any { s ->
|
||||
subsItem.updateUrl.startsWith(
|
||||
s
|
||||
)
|
||||
}) {
|
||||
"可信来源"
|
||||
} else {
|
||||
"未知来源"
|
||||
}
|
||||
Text(text = sourceText, fontSize = 14.sp)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(5.dp))
|
||||
Row {
|
||||
if (subsItem.id >= 0) {
|
||||
Text(
|
||||
text = index.toString() + ". " + (subscriptionRaw.name),
|
||||
maxLines = 1,
|
||||
softWrap = false,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(5.dp))
|
||||
Row {
|
||||
Text(
|
||||
text = formatTimeAgo(subsItem.mtime),
|
||||
text = "v" + (rawSubscription.version.toString()),
|
||||
maxLines = 1,
|
||||
softWrap = false,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
fontSize = 14.sp
|
||||
)
|
||||
Spacer(modifier = Modifier.width(10.dp))
|
||||
if (subsItem.id >= 0) {
|
||||
Text(
|
||||
text = "v" + (subscriptionRaw.version.toString()),
|
||||
maxLines = 1,
|
||||
softWrap = false,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
fontSize = 14.sp
|
||||
)
|
||||
Spacer(modifier = Modifier.width(10.dp))
|
||||
}
|
||||
val apps = subscriptionRaw.apps
|
||||
val groupsSize = apps.sumOf { it.groups.size }
|
||||
if (groupsSize > 0) {
|
||||
Text(
|
||||
text = "${apps.size}应用/${groupsSize}规则组", fontSize = 14.sp
|
||||
)
|
||||
} else {
|
||||
Text(
|
||||
text = "暂无规则", fontSize = 14.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val apps = rawSubscription.apps
|
||||
val groupsSize = rawSubscription.allGroupSize
|
||||
val ruleNumText = if (groupsSize > 0) {
|
||||
if (apps.isNotEmpty()) {
|
||||
"${apps.size}应用/${groupsSize}规则组"
|
||||
} else {
|
||||
"${groupsSize}规则组"
|
||||
}
|
||||
} else {
|
||||
"暂无规则"
|
||||
}
|
||||
Text(
|
||||
text = "本地无订阅文件,请下拉刷新",
|
||||
maxLines = 1,
|
||||
softWrap = false,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
text = ruleNumText,
|
||||
fontSize = 14.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.width(5.dp))
|
||||
|
||||
IconButton(onClick = { onMenuClick?.invoke() }) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.MoreVert,
|
||||
contentDescription = "more",
|
||||
} else {
|
||||
Text(
|
||||
text = "本地无订阅文件,请下拉刷新",
|
||||
maxLines = 1,
|
||||
softWrap = false,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.width(10.dp))
|
||||
Switch(
|
||||
checked = subsItem.enable,
|
||||
onCheckedChange = onCheckedChange,
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.width(10.dp))
|
||||
Switch(
|
||||
checked = subsItem.enable,
|
||||
onCheckedChange = onCheckedChange,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,7 +9,6 @@ import android.os.Build
|
|||
import com.blankj.utilcode.util.AppUtils
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import li.songe.gkd.app
|
||||
|
@ -17,11 +16,7 @@ import li.songe.gkd.appScope
|
|||
import li.songe.gkd.data.AppInfo
|
||||
import li.songe.gkd.util.Ext.getApplicationInfoExt
|
||||
|
||||
|
||||
private val _appInfoCacheFlow = MutableStateFlow(mapOf<String, AppInfo>())
|
||||
|
||||
val appInfoCacheFlow: StateFlow<Map<String, AppInfo>>
|
||||
get() = _appInfoCacheFlow
|
||||
val appInfoCacheFlow = MutableStateFlow(mapOf<String, AppInfo>())
|
||||
|
||||
private val packageReceiver by lazy {
|
||||
object : BroadcastReceiver() {
|
||||
|
@ -33,7 +28,7 @@ private val packageReceiver by lazy {
|
|||
val appId = intent?.data?.schemeSpecificPart ?: return
|
||||
if (intent.action == Intent.ACTION_PACKAGE_ADDED || intent.action == Intent.ACTION_PACKAGE_REPLACED || intent.action == Intent.ACTION_PACKAGE_REMOVED) {
|
||||
// update
|
||||
updateAppInfo(appId)
|
||||
updateAppInfo(listOf(appId))
|
||||
}
|
||||
}
|
||||
}.apply {
|
||||
|
@ -79,13 +74,13 @@ private fun getAppInfo(id: String): AppInfo? {
|
|||
return info
|
||||
}
|
||||
|
||||
val mutex by lazy { Mutex() }
|
||||
private val updateAppMutex by lazy { Mutex() }
|
||||
|
||||
fun updateAppInfo(vararg appIds: String) {
|
||||
fun updateAppInfo(appIds: List<String>) {
|
||||
if (appIds.isEmpty()) return
|
||||
appScope.launchTry(Dispatchers.IO) {
|
||||
mutex.withLock {
|
||||
val newMap = _appInfoCacheFlow.value.toMutableMap()
|
||||
updateAppMutex.withLock {
|
||||
val newMap = appInfoCacheFlow.value.toMutableMap()
|
||||
appIds.forEach { appId ->
|
||||
val newAppInfo = getAppInfo(appId)
|
||||
if (newAppInfo != null) {
|
||||
|
@ -94,7 +89,7 @@ fun updateAppInfo(vararg appIds: String) {
|
|||
newMap.remove(appId)
|
||||
}
|
||||
}
|
||||
_appInfoCacheFlow.value = newMap
|
||||
appInfoCacheFlow.value = newMap
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
110
app/src/main/kotlin/li/songe/gkd/util/AuthDialog.kt
Normal file
110
app/src/main/kotlin/li/songe/gkd/util/AuthDialog.kt
Normal file
|
@ -0,0 +1,110 @@
|
|||
package li.songe.gkd.util
|
||||
|
||||
import android.Manifest
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.provider.Settings
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import li.songe.gkd.app
|
||||
|
||||
data class AuthAction(
|
||||
val title: String,
|
||||
val text: String,
|
||||
val confirm: () -> Unit
|
||||
)
|
||||
|
||||
private val notifAuthAction by lazy {
|
||||
AuthAction(
|
||||
title = "权限请求",
|
||||
text = "当前操作需要通知权限\n您需要前往[通知管理]打开此权限",
|
||||
confirm = {
|
||||
val intent = Intent()
|
||||
intent.action = Settings.ACTION_APP_NOTIFICATION_SETTINGS
|
||||
intent.putExtra(Settings.EXTRA_APP_PACKAGE, app.packageName)
|
||||
intent.putExtra(Settings.EXTRA_CHANNEL_ID, app.applicationInfo.uid)
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
app.startActivity(intent)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
val canDrawOverlaysAuthAction by lazy {
|
||||
AuthAction(
|
||||
title = "权限请求",
|
||||
text = "当前操作需要悬浮窗权限\n您需要前往[显示在其它应用的上层]打开此权限",
|
||||
confirm = {
|
||||
val intent = Intent(
|
||||
Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
|
||||
)
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
app.startActivity(intent)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
val authActionFlow = MutableStateFlow<AuthAction?>(null)
|
||||
|
||||
@Composable
|
||||
fun AuthDialog() {
|
||||
val authAction = authActionFlow.collectAsState().value
|
||||
if (authAction != null) {
|
||||
AlertDialog(
|
||||
title = {
|
||||
Text(text = authAction.title)
|
||||
},
|
||||
text = {
|
||||
Text(text = authAction.text)
|
||||
},
|
||||
onDismissRequest = { authActionFlow.value = null },
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
authActionFlow.value = null
|
||||
authAction.confirm()
|
||||
}) {
|
||||
Text(text = "确认")
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { authActionFlow.value = null }) {
|
||||
Text(text = "取消")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun checkOrRequestNotifPermission(context: Activity): Boolean {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && ActivityCompat.checkSelfPermission(
|
||||
context,
|
||||
Manifest.permission.POST_NOTIFICATIONS
|
||||
) == PackageManager.PERMISSION_DENIED
|
||||
) {
|
||||
if (ActivityCompat.shouldShowRequestPermissionRationale(
|
||||
context,
|
||||
Manifest.permission.POST_NOTIFICATIONS
|
||||
)
|
||||
) {
|
||||
ActivityCompat.requestPermissions(
|
||||
context,
|
||||
arrayOf(Manifest.permission.POST_NOTIFICATIONS),
|
||||
0 // TODO 如何感知 (adb shell pm grant)/appops 这类引起的授权变化?
|
||||
)
|
||||
} else {
|
||||
authActionFlow.value = notifAuthAction
|
||||
}
|
||||
return false
|
||||
} else if (!NotificationManagerCompat.from(context).areNotificationsEnabled()) {
|
||||
authActionFlow.value = notifAuthAction
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
|
@ -28,6 +28,7 @@ fun initFolder() {
|
|||
imageCacheDir
|
||||
).forEach { f ->
|
||||
if (!f.exists()) {
|
||||
// TODO 在某些机型上无法创建目录 用户反馈重启手机后解决 是否存在其它解决方式?
|
||||
f.mkdirs()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package li.songe.gkd.util
|
||||
|
||||
import blue.endless.jankson.Jankson
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonArray
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
|
@ -48,7 +49,8 @@ private fun escapeString(value: String): String {
|
|||
return sb.toString()
|
||||
}
|
||||
|
||||
fun convertJsonElementToJson5(element: JsonElement): String {
|
||||
fun convertJsonElementToJson5(element: JsonElement, indent: Int = 2): String {
|
||||
val spaces = "\u0020".repeat(indent)
|
||||
return when (element) {
|
||||
is JsonPrimitive -> {
|
||||
val content = element.content
|
||||
|
@ -61,20 +63,22 @@ fun convertJsonElementToJson5(element: JsonElement): String {
|
|||
|
||||
is JsonObject -> {
|
||||
// Handle JSON objects
|
||||
val entries = element.entries.joinToString(",") { (key, value) ->
|
||||
val entries = element.entries.joinToString(",\n") { (key, value) ->
|
||||
// If key is a valid identifier, no quotes are needed
|
||||
if (key.matches(json5IdentifierReg)) {
|
||||
"$key:${convertJsonElementToJson5(value)}"
|
||||
"$key: ${convertJsonElementToJson5(value, indent)}"
|
||||
} else {
|
||||
"${escapeString(key)}:${convertJsonElementToJson5(value)}"
|
||||
"${escapeString(key)}: ${convertJsonElementToJson5(value, indent)}"
|
||||
}
|
||||
}
|
||||
"{$entries}"
|
||||
}.lineSequence().map { l -> spaces + l }.joinToString("\n")
|
||||
"{\n$entries\n}"
|
||||
}
|
||||
|
||||
is JsonArray -> {
|
||||
val elements = element.joinToString(",") { convertJsonElementToJson5(it) }
|
||||
"[$elements]"
|
||||
val elements =
|
||||
element.joinToString(",\n") { convertJsonElementToJson5(it, indent) }
|
||||
.lineSequence().map { l -> spaces + l }.joinToString("\n")
|
||||
"[\n$elements\n]"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -82,3 +86,7 @@ fun convertJsonElementToJson5(element: JsonElement): String {
|
|||
inline fun <reified T> Json.encodeToJson5String(value: T): String {
|
||||
return convertJsonElementToJson5(encodeToJsonElement(serializersModule.serializer(), value))
|
||||
}
|
||||
|
||||
fun json5ToJson(source: String): String {
|
||||
return Jankson.builder().build().load(source).toJson()
|
||||
}
|
|
@ -59,7 +59,6 @@ data class Store(
|
|||
val log2FileSwitch: Boolean = true,
|
||||
val enableDarkTheme: Boolean? = null,
|
||||
val enableAbFloatWindow: Boolean = true,
|
||||
val matchUnknownApp: Boolean = false,
|
||||
)
|
||||
|
||||
val storeFlow by lazy {
|
||||
|
|
|
@ -1,18 +1,22 @@
|
|||
package li.songe.gkd.util
|
||||
|
||||
import com.blankj.utilcode.util.LogUtils
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.encodeToString
|
||||
import li.songe.gkd.appScope
|
||||
import li.songe.gkd.data.AppRule
|
||||
import li.songe.gkd.data.CategoryConfig
|
||||
import li.songe.gkd.data.Rule
|
||||
import li.songe.gkd.data.GlobalApp
|
||||
import li.songe.gkd.data.GlobalRule
|
||||
import li.songe.gkd.data.RawSubscription
|
||||
import li.songe.gkd.data.SubsConfig
|
||||
import li.songe.gkd.data.SubsItem
|
||||
import li.songe.gkd.data.SubscriptionRaw
|
||||
import li.songe.gkd.db.DbSet
|
||||
import li.songe.selector.Selector
|
||||
|
||||
|
@ -20,23 +24,39 @@ val subsItemsFlow by lazy {
|
|||
DbSet.subsItemDao.query().stateIn(appScope, SharingStarted.Eagerly, emptyList())
|
||||
}
|
||||
|
||||
private val subsIdToMtimeFlow by lazy {
|
||||
subsItemsFlow.map { it.sortedBy { s -> s.id }.associate { s -> s.id to s.mtime } }
|
||||
.stateIn(appScope, SharingStarted.Eagerly, emptyMap())
|
||||
val subsIdToRawFlow by lazy {
|
||||
MutableStateFlow<Map<Long, RawSubscription>>(emptyMap())
|
||||
}
|
||||
|
||||
val subsIdToRawFlow by lazy {
|
||||
MutableStateFlow<Map<Long, SubscriptionRaw?>>(emptyMap())
|
||||
private val updateSubsFileMutex by lazy { Mutex() }
|
||||
fun updateSubscription(subscription: RawSubscription) {
|
||||
appScope.launchTry {
|
||||
updateSubsFileMutex.withLock {
|
||||
val newMap = subsIdToRawFlow.value.toMutableMap()
|
||||
newMap[subscription.id] = subscription
|
||||
subsIdToRawFlow.value = newMap
|
||||
withContext(Dispatchers.IO) {
|
||||
subsFolder.resolve("${subscription.id}.json")
|
||||
.writeText(json.encodeToString(subscription))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteSubscription(subsId: Long) {
|
||||
val newMap = subsIdToRawFlow.value.toMutableMap()
|
||||
newMap.remove(subsId)
|
||||
subsIdToRawFlow.value = newMap
|
||||
}
|
||||
|
||||
fun getGroupRawEnable(
|
||||
groupRaw: SubscriptionRaw.GroupRaw,
|
||||
rawGroup: RawSubscription.RawGroupProps,
|
||||
subsConfigs: List<SubsConfig>,
|
||||
category: SubscriptionRaw.Category?,
|
||||
category: RawSubscription.RawCategory?,
|
||||
categoryConfigs: List<CategoryConfig>
|
||||
): Boolean {
|
||||
// 优先级: 规则用户配置 > 批量配置 > 批量默认 > 规则默认
|
||||
val groupConfig = subsConfigs.find { c -> c.groupKey == groupRaw.key }
|
||||
val groupConfig = subsConfigs.find { c -> c.groupKey == rawGroup.key }
|
||||
// 1.规则用户配置
|
||||
return groupConfig?.enable ?: if (category != null) {// 这个规则被批量配置捕获
|
||||
val categoryConfig = categoryConfigs.find { c -> c.categoryKey == category.key }
|
||||
|
@ -50,183 +70,279 @@ fun getGroupRawEnable(
|
|||
enable
|
||||
} else {
|
||||
null
|
||||
} ?: groupRaw.enable ?: true
|
||||
} ?: rawGroup.enable ?: true
|
||||
}
|
||||
|
||||
private val appIdToRulesFlow by lazy {
|
||||
private fun getFixActivityIds(
|
||||
appId: String,
|
||||
activityIds: List<String>?,
|
||||
): List<String> {
|
||||
activityIds ?: return emptyList()
|
||||
return activityIds.map { activityId ->
|
||||
if (activityId.startsWith('.')) { // .a.b.c -> com.x.y.x.a.b.c
|
||||
appId + activityId
|
||||
} else {
|
||||
activityId
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class AllRules(
|
||||
val globalRules: List<GlobalRule> = emptyList(),
|
||||
val globalGroups: List<RawSubscription.RawGlobalGroup> = emptyList(),
|
||||
val appIdToRules: Map<String, List<AppRule>> = emptyMap(),
|
||||
val appIdToGroups: Map<String, List<RawSubscription.RawAppGroup>> = emptyMap(),
|
||||
) {
|
||||
val appSize = appIdToRules.keys.size
|
||||
val allGroupSize = globalGroups.size + appIdToGroups.values.sumOf { s -> s.size }
|
||||
}
|
||||
|
||||
val allRulesFlow by lazy {
|
||||
combine(
|
||||
subsItemsFlow,
|
||||
subsIdToRawFlow,
|
||||
appInfoCacheFlow,
|
||||
DbSet.subsConfigDao.query(),
|
||||
DbSet.categoryConfigDao.query(),
|
||||
) { subsItems, subsIdToRaw, subsConfigs, categoryConfigs ->
|
||||
) { subsItems, subsIdToRaw, appInfoCache, subsConfigs, categoryConfigs ->
|
||||
val globalSubsConfigs = subsConfigs.filter { c -> c.type == SubsConfig.GlobalGroupType }
|
||||
val appSubsConfigs = subsConfigs.filter { c -> c.type == SubsConfig.AppType }
|
||||
val groupSubsConfigs = subsConfigs.filter { c -> c.type == SubsConfig.GroupType }
|
||||
val appIdToRules = mutableMapOf<String, MutableList<Rule>>()
|
||||
val groupSubsConfigs = subsConfigs.filter { c -> c.type == SubsConfig.AppGroupType }
|
||||
val appRules = mutableMapOf<String, MutableList<AppRule>>()
|
||||
val globalRules = mutableListOf<GlobalRule>()
|
||||
val globalGroups = mutableListOf<RawSubscription.RawGlobalGroup>()
|
||||
val appGroups = mutableMapOf<String, List<RawSubscription.RawAppGroup>>()
|
||||
subsItems.filter { it.enable }.forEach { subsItem ->
|
||||
val subsRaw = subsIdToRaw[subsItem.id] ?: return@forEach
|
||||
val rawSubs = subsIdToRaw[subsItem.id] ?: return@forEach
|
||||
val subGlobalSubsConfigs = globalSubsConfigs.filter { c -> c.subsItemId == subsItem.id }
|
||||
rawSubs.globalGroups.filter { g ->
|
||||
g.valid && (subGlobalSubsConfigs.find { c -> c.groupKey == g.key }?.enable
|
||||
?: g.enable ?: true)
|
||||
}.forEach { groupRaw ->
|
||||
globalGroups.add(groupRaw)
|
||||
val subRules = groupRaw.rules.mapIndexed { ruleIndex, ruleRaw ->
|
||||
val apps = mutableMapOf<String, GlobalApp>()
|
||||
(ruleRaw.apps ?: groupRaw.apps ?: emptyList()).forEach { a ->
|
||||
apps[a.id] = GlobalApp(
|
||||
id = a.id,
|
||||
enable = a.enable ?: true,
|
||||
activityIds = a.activityIds ?: emptyList(),
|
||||
excludeActivityIds = a.excludeActivityIds ?: emptyList()
|
||||
)
|
||||
}
|
||||
|
||||
val matchAnyApp = ruleRaw.matchAnyApp ?: groupRaw.matchAnyApp ?: false
|
||||
|
||||
val quickFind =
|
||||
ruleRaw.quickFind ?: groupRaw.quickFind ?: false
|
||||
val matchDelay =
|
||||
ruleRaw.matchDelay ?: groupRaw.matchDelay ?: 0
|
||||
val matchTime = ruleRaw.matchTime ?: groupRaw.matchTime
|
||||
val resetMatch =
|
||||
ruleRaw.resetMatch ?: groupRaw.resetMatch
|
||||
val actionDelay =
|
||||
ruleRaw.actionDelay ?: groupRaw.actionDelay ?: 0
|
||||
|
||||
GlobalRule(
|
||||
quickFind = quickFind,
|
||||
actionDelay = actionDelay,
|
||||
index = ruleIndex,
|
||||
matches = ruleRaw.matches.map { Selector.parse(it) },
|
||||
excludeMatches = (ruleRaw.excludeMatches ?: emptyList()).map {
|
||||
Selector.parse(
|
||||
it
|
||||
)
|
||||
},
|
||||
matchDelay = matchDelay,
|
||||
matchTime = matchTime,
|
||||
key = ruleRaw.key,
|
||||
preKeys = (ruleRaw.preKeys ?: emptyList()).toSet(),
|
||||
rule = ruleRaw,
|
||||
group = groupRaw,
|
||||
subsItem = subsItem,
|
||||
resetMatch = resetMatch,
|
||||
matchAnyApp = matchAnyApp,
|
||||
apps = apps,
|
||||
rawSubs = rawSubs,
|
||||
)
|
||||
}
|
||||
subRules.forEach { ruleConfig ->
|
||||
// 保留原始对象引用, 方便判断 lastTriggerRule 时直接使用 ===
|
||||
ruleConfig.preAppRules = subRules.filter { otherRule ->
|
||||
(otherRule.key != null) && ruleConfig.preKeys.contains(
|
||||
otherRule.key
|
||||
)
|
||||
}.toSet()
|
||||
// 共用次数
|
||||
val maxKey =
|
||||
ruleConfig.rule.actionMaximumKey ?: ruleConfig.group.actionMaximumKey
|
||||
if (maxKey != null) {
|
||||
val otherRule = subRules.find { r -> r.key == maxKey }
|
||||
if (otherRule != null) {
|
||||
ruleConfig.actionCount = otherRule.actionCount
|
||||
}
|
||||
}
|
||||
// 共用 cd
|
||||
val cdKey = ruleConfig.rule.actionCdKey ?: ruleConfig.group.actionCdKey
|
||||
if (cdKey != null) {
|
||||
val otherRule = subRules.find { r -> r.key == cdKey }
|
||||
if (otherRule != null) {
|
||||
ruleConfig.actionTriggerTime = otherRule.actionTriggerTime
|
||||
}
|
||||
}
|
||||
}
|
||||
globalRules.addAll(subRules)
|
||||
}
|
||||
val subAppSubsConfigs = appSubsConfigs.filter { c -> c.subsItemId == subsItem.id }
|
||||
val subGroupSubsConfigs = groupSubsConfigs.filter { c -> c.subsItemId == subsItem.id }
|
||||
val subCategoryConfigs = categoryConfigs.filter { c -> c.subsItemId == subsItem.id }
|
||||
subsRaw.apps.filter { appRaw ->
|
||||
rawSubs.apps.filter { appRaw ->
|
||||
// 筛选 当前启用的 app 订阅规则
|
||||
(subAppSubsConfigs.find { c -> c.appId == appRaw.id }?.enable ?: true)
|
||||
(subAppSubsConfigs.find { c -> c.appId == appRaw.id }?.enable
|
||||
?: (appInfoCache[appRaw.id] != null))
|
||||
}.forEach { appRaw ->
|
||||
val subAppGroups = mutableListOf<RawSubscription.RawAppGroup>()
|
||||
val appGroupConfigs = subGroupSubsConfigs.filter { c -> c.appId == appRaw.id }
|
||||
val rules = appIdToRules[appRaw.id] ?: mutableListOf()
|
||||
appIdToRules[appRaw.id] = rules
|
||||
appRaw.groups.filter { groupRaw ->
|
||||
getGroupRawEnable(
|
||||
groupRaw.valid && getGroupRawEnable(
|
||||
groupRaw,
|
||||
appGroupConfigs,
|
||||
subsRaw.groupToCategoryMap[groupRaw],
|
||||
rawSubs.groupToCategoryMap[groupRaw],
|
||||
subCategoryConfigs
|
||||
)
|
||||
}.filter { groupRaw ->
|
||||
// 筛选合法选择器的规则组, 如果一个规则组内某个选择器语法错误, 则禁用/丢弃此规则组
|
||||
groupRaw.valid
|
||||
}.forEach { groupRaw ->
|
||||
val groupRuleList = mutableListOf<Rule>()
|
||||
groupRaw.rules.forEachIndexed { ruleIndex, ruleRaw ->
|
||||
subAppGroups.add(groupRaw)
|
||||
val subRules = groupRaw.rules.mapIndexed { ruleIndex, ruleRaw ->
|
||||
val activityIds =
|
||||
(ruleRaw.activityIds ?: groupRaw.activityIds ?: appRaw.activityIds
|
||||
?: emptyList()).map { activityId ->
|
||||
if (activityId.startsWith('.')) { // .a.b.c -> com.x.y.x.a.b.c
|
||||
appRaw.id + activityId
|
||||
} else {
|
||||
activityId
|
||||
}
|
||||
}.toSet()
|
||||
getFixActivityIds(
|
||||
appRaw.id,
|
||||
ruleRaw.activityIds ?: groupRaw.activityIds
|
||||
)
|
||||
|
||||
val excludeActivityIds =
|
||||
(ruleRaw.excludeActivityIds ?: groupRaw.excludeActivityIds
|
||||
?: appRaw.excludeActivityIds ?: emptyList()).map { activityId ->
|
||||
if (activityId.startsWith('.')) { // .a.b.c -> com.x.y.x.a.b.c
|
||||
appRaw.id + activityId
|
||||
} else {
|
||||
activityId
|
||||
}
|
||||
}.toSet()
|
||||
val excludeActivityIds = getFixActivityIds(
|
||||
appRaw.id,
|
||||
ruleRaw.excludeActivityIds ?: groupRaw.excludeActivityIds,
|
||||
)
|
||||
|
||||
val quickFind =
|
||||
ruleRaw.quickFind ?: groupRaw.quickFind ?: appRaw.quickFind ?: false
|
||||
ruleRaw.quickFind ?: groupRaw.quickFind ?: false
|
||||
|
||||
val matchDelay =
|
||||
ruleRaw.matchDelay ?: groupRaw.matchDelay ?: appRaw.matchDelay
|
||||
val matchTime = ruleRaw.matchTime ?: groupRaw.matchTime ?: appRaw.matchTime
|
||||
ruleRaw.matchDelay ?: groupRaw.matchDelay ?: 0
|
||||
val matchTime = ruleRaw.matchTime ?: groupRaw.matchTime
|
||||
val resetMatch =
|
||||
ruleRaw.resetMatch ?: groupRaw.resetMatch ?: appRaw.resetMatch
|
||||
ruleRaw.resetMatch ?: groupRaw.resetMatch
|
||||
val actionDelay =
|
||||
ruleRaw.actionDelay ?: groupRaw.actionDelay ?: appRaw.actionDelay ?: 0
|
||||
ruleRaw.actionDelay ?: groupRaw.actionDelay ?: 0
|
||||
|
||||
groupRuleList.add(
|
||||
Rule(
|
||||
quickFind = quickFind,
|
||||
actionDelay = actionDelay,
|
||||
index = ruleIndex,
|
||||
matches = ruleRaw.matches.map { Selector.parse(it) },
|
||||
excludeMatches = (ruleRaw.excludeMatches ?: emptyList()).map {
|
||||
Selector.parse(
|
||||
it
|
||||
)
|
||||
},
|
||||
matchDelay = matchDelay,
|
||||
matchTime = matchTime,
|
||||
appId = appRaw.id,
|
||||
activityIds = activityIds,
|
||||
excludeActivityIds = excludeActivityIds,
|
||||
key = ruleRaw.key,
|
||||
preKeys = (ruleRaw.preKeys ?: emptyList()).toSet(),
|
||||
rule = ruleRaw,
|
||||
group = groupRaw,
|
||||
app = appRaw,
|
||||
subsItem = subsItem,
|
||||
resetMatch = resetMatch,
|
||||
)
|
||||
AppRule(
|
||||
quickFind = quickFind,
|
||||
actionDelay = actionDelay,
|
||||
index = ruleIndex,
|
||||
matches = ruleRaw.matches.map { Selector.parse(it) },
|
||||
excludeMatches = (ruleRaw.excludeMatches ?: emptyList()).map {
|
||||
Selector.parse(
|
||||
it
|
||||
)
|
||||
},
|
||||
matchDelay = matchDelay,
|
||||
matchTime = matchTime,
|
||||
appId = appRaw.id,
|
||||
activityIds = activityIds,
|
||||
excludeActivityIds = excludeActivityIds,
|
||||
key = ruleRaw.key,
|
||||
preKeys = (ruleRaw.preKeys ?: emptyList()).toSet(),
|
||||
rule = ruleRaw,
|
||||
group = groupRaw,
|
||||
app = appRaw,
|
||||
subsItem = subsItem,
|
||||
resetMatch = resetMatch,
|
||||
rawSubs = rawSubs,
|
||||
)
|
||||
|
||||
}
|
||||
groupRuleList.forEach { ruleConfig ->
|
||||
subRules.forEach { ruleConfig ->
|
||||
// 保留原始对象引用, 方便判断 lastTriggerRule 时直接使用 ===
|
||||
ruleConfig.preRules = groupRuleList.filter { otherRule ->
|
||||
ruleConfig.preAppRules = subRules.filter { otherRule ->
|
||||
(otherRule.key != null) && ruleConfig.preKeys.contains(
|
||||
otherRule.key
|
||||
)
|
||||
}.toSet()
|
||||
// 共用次数
|
||||
val maxKey = ruleConfig.rule.actionMaximumKey
|
||||
val maxKey =
|
||||
ruleConfig.rule.actionMaximumKey ?: ruleConfig.group.actionMaximumKey
|
||||
if (maxKey != null) {
|
||||
val otherRule = groupRuleList.find { r -> r.key == maxKey }
|
||||
val otherRule = subRules.find { r -> r.key == maxKey }
|
||||
if (otherRule != null) {
|
||||
ruleConfig.actionCount = otherRule.actionCount
|
||||
}
|
||||
}
|
||||
// 共用 cd
|
||||
val cdKey = ruleConfig.rule.actionCdKey
|
||||
val cdKey = ruleConfig.rule.actionCdKey ?: ruleConfig.group.actionCdKey
|
||||
if (cdKey != null) {
|
||||
val otherRule = groupRuleList.find { r -> r.key == cdKey }
|
||||
val otherRule = subRules.find { r -> r.key == cdKey }
|
||||
if (otherRule != null) {
|
||||
ruleConfig.actionTriggerTime = otherRule.actionTriggerTime
|
||||
}
|
||||
}
|
||||
}
|
||||
rules.addAll(groupRuleList)
|
||||
if (subRules.isNotEmpty()) {
|
||||
val rules = appRules[appRaw.id] ?: mutableListOf()
|
||||
appRules[appRaw.id] = rules
|
||||
rules.addAll(subRules)
|
||||
}
|
||||
}
|
||||
if (subAppGroups.isNotEmpty()) {
|
||||
appGroups[appRaw.id] = subAppGroups
|
||||
}
|
||||
}
|
||||
}
|
||||
appIdToRules.values.forEach { rules ->
|
||||
// 让开屏广告类规则全排在最前面
|
||||
rules.sortBy { r -> if (r.isOpenAd) 0 else 1 }
|
||||
}
|
||||
appIdToRules.filter { it.value.isNotEmpty() }
|
||||
}.stateIn(appScope, SharingStarted.Eagerly, emptyMap<String, List<Rule>>())
|
||||
}
|
||||
|
||||
data class AppRule(
|
||||
val visibleMap: Map<String, List<Rule>> = emptyMap(),
|
||||
val unVisibleMap: Map<String, List<Rule>> = emptyMap(),
|
||||
val allMap: Map<String, List<Rule>> = emptyMap(),
|
||||
)
|
||||
|
||||
val appRuleFlow by lazy {
|
||||
combine(appIdToRulesFlow, appInfoCacheFlow) { appIdToRules, appInfoCache ->
|
||||
val visibleMap = mutableMapOf<String, List<Rule>>()
|
||||
val unVisibleMap = mutableMapOf<String, List<Rule>>()
|
||||
appIdToRules.forEach { (appId, rules) ->
|
||||
if (appInfoCache.containsKey(appId)) {
|
||||
visibleMap[appId] = rules
|
||||
} else {
|
||||
unVisibleMap[appId] = rules
|
||||
}
|
||||
}
|
||||
AppRule(
|
||||
visibleMap = visibleMap, unVisibleMap = unVisibleMap, allMap = appIdToRules
|
||||
AllRules(
|
||||
appIdToRules = appRules,
|
||||
globalRules = globalRules,
|
||||
globalGroups = globalGroups,
|
||||
appIdToGroups = appGroups,
|
||||
)
|
||||
}.stateIn(appScope, SharingStarted.Eagerly, AppRule())
|
||||
}.stateIn(appScope, SharingStarted.Eagerly, AllRules())
|
||||
}
|
||||
|
||||
|
||||
fun initSubsState() {
|
||||
subsItemsFlow.value
|
||||
subsIdToRawFlow.value
|
||||
appScope.launchTry {
|
||||
// reload subsRaw file by diff
|
||||
var oldSubsIdToMtime = emptyMap<Long, Long>()
|
||||
subsIdToMtimeFlow.collect { subsIdToMtime ->
|
||||
val commonMap = subsIdToMtime.filter { e -> oldSubsIdToMtime[e.key] == e.value }
|
||||
oldSubsIdToMtime = subsIdToMtime
|
||||
val oldSubsIdToRaw = subsIdToRawFlow.value
|
||||
subsIdToRawFlow.value = subsIdToMtime.map { entry ->
|
||||
if (commonMap.containsKey(entry.key)) {
|
||||
entry.key to oldSubsIdToRaw[entry.key]
|
||||
} else {
|
||||
val newSubsRaw =
|
||||
withContext(Dispatchers.IO) { SubsItem.getSubscriptionRaw(entry.key) }
|
||||
newSubsRaw?.apps?.apply {
|
||||
updateAppInfo(*map { a -> a.id }.toTypedArray())
|
||||
}
|
||||
entry.key to newSubsRaw
|
||||
appScope.launchTry(Dispatchers.IO) {
|
||||
if (subsFolder.exists() && subsFolder.isDirectory) {
|
||||
val fileRegex = Regex("^-?\\d+\\.json$")
|
||||
val files =
|
||||
subsFolder.listFiles { f -> f.isFile && f.name.matches(fileRegex) } ?: emptyArray()
|
||||
val subscriptions = files.mapNotNull { f ->
|
||||
try {
|
||||
RawSubscription.parse(f.readText())
|
||||
} catch (e: Exception) {
|
||||
LogUtils.d("加载订阅文件失败", e)
|
||||
null
|
||||
}
|
||||
}.toMap()
|
||||
}
|
||||
val newMap = subsIdToRawFlow.value.toMutableMap()
|
||||
subscriptions.forEach { s ->
|
||||
newMap[s.id] = s
|
||||
}
|
||||
if (newMap[-2] == null) {
|
||||
newMap[-2] = RawSubscription(
|
||||
id = -2,
|
||||
name = "本地订阅",
|
||||
version = 0,
|
||||
author = "gkd",
|
||||
)
|
||||
}
|
||||
subsIdToRawFlow.value = newMap
|
||||
}
|
||||
var oldAppIds = emptySet<String>()
|
||||
val appIdsFlow = subsIdToRawFlow.map(appScope) { e ->
|
||||
e.values.map { s -> s.apps.map { a -> a.id } }.flatten().toSet()
|
||||
}
|
||||
appIdsFlow.collect { newAppIds ->
|
||||
// diff new appId
|
||||
updateAppInfo(newAppIds.subtract(oldAppIds).toList())
|
||||
oldAppIds = newAppIds
|
||||
}
|
||||
}
|
||||
}
|
|
@ -90,9 +90,9 @@ fun startDownload(newVersion: NewVersion) {
|
|||
} else if (downloadStatus is LoadStatus.Failure) {
|
||||
// 提前终止下载
|
||||
job?.cancel()
|
||||
}
|
||||
}
|
||||
}.bodyAsChannel()
|
||||
}
|
||||
}.bodyAsChannel()
|
||||
if (downloadStatusFlow.value is LoadStatus.Loading) {
|
||||
channel.copyAndClose(newApkFile.writeChannel())
|
||||
downloadStatusFlow.value = LoadStatus.Success(newApkFile.absolutePath)
|
||||
|
@ -109,7 +109,6 @@ fun startDownload(newVersion: NewVersion) {
|
|||
fun UpgradeDialog() {
|
||||
val newVersion by newVersionFlow.collectAsState()
|
||||
newVersion?.let { newVersionVal ->
|
||||
|
||||
AlertDialog(title = {
|
||||
Text(text = "检测到新版本")
|
||||
}, text = {
|
||||
|
|
Loading…
Reference in New Issue
Block a user