Merge branch 'main' into pr/tiann/402

This commit is contained in:
lisonge 2023-12-26 21:39:58 +08:00
commit e5d96dcfc0
17 changed files with 315 additions and 389 deletions

View File

@ -1,43 +1,25 @@
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,
) {
val appId = app.id
val activityIds = getFixActivityIds(app.id, rule.activityIds ?: group.activityIds)
val excludeActivityIds =
getFixActivityIds(app.id, rule.excludeActivityIds ?: group.excludeActivityIds)
override val type = "app"
override fun matchActivity(topActivity: TopActivity?): Boolean {
topActivity ?: return false
topActivity.activityId ?: return true

View File

@ -1,7 +1,6 @@
package li.songe.gkd.data
import li.songe.gkd.service.TopActivity
import li.songe.selector.Selector
data class GlobalApp(
val id: String,
@ -11,38 +10,31 @@ data class GlobalApp(
)
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,
) {
val matchAnyApp = rule.matchAnyApp ?: group.matchAnyApp ?: true
val apps = mutableMapOf<String, GlobalApp>().apply {
(rule.apps ?: group.apps ?: emptyList()).forEach { a ->
this[a.id] = GlobalApp(
id = a.id,
enable = a.enable ?: true,
activityIds = getFixActivityIds(a.id, a.activityIds),
excludeActivityIds = getFixActivityIds(a.id, a.excludeActivityIds)
)
}
}
override val type = "global"
private val excludeAppIds = apps.filter { e -> !e.value.enable }.keys
override fun matchActivity(topActivity: TopActivity?): Boolean {
topActivity ?: return false

View File

@ -25,9 +25,9 @@ data class RawSubscription(
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(),
val categories: List<RawCategory> = emptyList(),
val apps: List<RawApp> = emptyList(),
) {
@IgnoredOnParcel
@ -57,8 +57,27 @@ data class RawSubscription(
}
@IgnoredOnParcel
val allGroupSize by lazy {
globalGroups.size + appGroups.size
val numText by lazy {
val appsSize = apps.size
val appGroupsSize = appGroups.size
val globalGroupSize = globalGroups.size
if (appGroupsSize + globalGroupSize > 0) {
if (globalGroupSize > 0) {
"${globalGroupSize}全局" + if (appGroupsSize > 0) {
"/"
} else {
""
}
} else {
""
} + if (appGroupsSize > 0) {
"${appsSize}应用/${appGroupsSize}规则组"
} else {
""
}
} else {
"暂无规则"
}
}
@Serializable
@ -118,9 +137,9 @@ data class RawSubscription(
data class RawGlobalApp(
val id: String,
val enable: Boolean?,
val activityIds: List<String>?,
val excludeActivityIds: List<String>?,
)
override val activityIds: List<String>?,
override val excludeActivityIds: List<String>?,
) : RawAppRuleProps
@Serializable

View File

@ -9,26 +9,63 @@ 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()
val key = rule.key
val index = group.rules.indexOf(rule)
val preKeys = (rule.preKeys ?: emptyList()).toSet()
val resetMatch = rule.resetMatch ?: group.resetMatch
val matches = rule.matches.map { s -> Selector.parse(s) }
val excludeMatches = (rule.excludeMatches ?: emptyList()).map { s -> Selector.parse(s) }
val matchDelay = rule.matchDelay ?: group.matchDelay ?: 0L
val actionDelay = rule.actionDelay ?: group.actionDelay ?: 0L
val matchTime = rule.matchTime ?: group.matchTime
val quickFind = rule.quickFind ?: group.quickFind ?: false
val actionCdKey = rule.actionCdKey ?: group.actionCdKey
val actionCd = rule.actionCd ?: if (actionCdKey != null) {
group.rules.find { r -> r.key == actionCdKey }?.actionCd
} else {
null
} ?: group.actionCd ?: 1000L
val actionMaximumKey = rule.actionMaximumKey ?: group.actionMaximumKey
val actionMaximum = rule.actionMaximum ?: if (actionMaximumKey != null) {
group.rules.find { r -> r.key == actionMaximumKey }?.actionMaximum
} else {
null
} ?: group.actionMaximum
var groupRules: List<ResolvedRule> = emptyList()
set(value) {
field = value
// 共享次数
if (actionMaximumKey != null) {
val otherRule = field.find { r -> r.key == actionMaximumKey }
if (otherRule != null) {
actionCount = otherRule.actionCount
}
}
// 共享 cd
if (actionCdKey != null) {
val otherRule = field.find { r -> r.key == actionCdKey }
if (otherRule != null) {
actionTriggerTime = otherRule.actionTriggerTime
}
}
preRules = field.filter { otherRule ->
(otherRule.key != null) && preKeys.contains(
otherRule.key
)
}.toSet()
}
var preRules = emptySet<ResolvedRule>()
val hasNext = group.rules.any { r -> r.preKeys?.any { k -> k == rule.key } == true }
var actionDelayTriggerTime = 0L
var actionDelayJob: Job? = null
fun checkDelay(): Boolean {
@ -39,12 +76,6 @@ sealed class ResolvedRule(
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()
@ -55,13 +86,6 @@ sealed class ResolvedRule(
lastTriggerRule = 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
@ -90,42 +114,69 @@ sealed class ResolvedRule(
var matchDelayJob: Job? = null
val statusCode: Int
val status: RuleStatus
get() {
if (actionMaximum != null) {
if (actionCount.value >= actionMaximum) {
return 1 // 达到最大执行次数
return RuleStatus.Status1 // 达到最大执行次数
}
}
if (preAppRules.isNotEmpty()) { // 需要提前点击某个规则
lastTriggerRule ?: return 2
return if (preAppRules.any { it === lastTriggerRule }) {
0
if (preRules.isNotEmpty()) { // 需要提前点击某个规则
return if (preRules.any { it === lastTriggerRule }) {
RuleStatus.StatusOk
} else {
3 // 上一个点击的规则不在当前需要点击的列表
RuleStatus.Status2
}
}
val t = System.currentTimeMillis()
if (matchDelay > 0 && t - matchChangedTime < matchDelay) {
return 4 // 处于匹配延迟中
return RuleStatus.Status3 // 处于匹配延迟中
}
if (matchTime != null && t - matchChangedTime > matchLimitTime) {
return 5 // 超出匹配时间
return RuleStatus.Status4 // 超出匹配时间
}
if (actionTriggerTime.value + actionCd > t) {
return 6 // 处于冷却时间
return RuleStatus.Status5 // 处于冷却时间
}
if (actionDelayTriggerTime > 0) {
if (actionDelayTriggerTime + actionDelay > t) {
return 7 // 处于点击延迟中
return RuleStatus.Status6 // 处于点击延迟中
}
}
return 0
return RuleStatus.StatusOk
}
fun statusText(): String {
return "id:${subsItem.id}, v:${rawSubs.version}, type:${type}, gKey=${group.key}, gName:${group.name}, index:${index}, key:${key}, status:${status.name}"
}
abstract val type: String
abstract fun matchActivity(topActivity: TopActivity?): Boolean
}
sealed class RuleStatus(val name: String) {
data object StatusOk : RuleStatus("ok")
data object Status1 : RuleStatus("达到最大执行次数")
data object Status2 : RuleStatus("需要提前点击某个规则")
data object Status3 : RuleStatus("处于匹配延迟")
data object Status4 : RuleStatus("超出匹配时间")
data object Status5 : RuleStatus("处于冷却时间")
data object Status6 : RuleStatus("处于点击延迟")
}
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
}
}
}

View File

@ -6,14 +6,16 @@ import io.ktor.http.HttpStatusCode
import io.ktor.server.application.createApplicationPlugin
import io.ktor.server.application.hooks.CallFailed
import io.ktor.server.request.uri
import io.ktor.server.response.header
import io.ktor.server.response.respond
import li.songe.gkd.data.RpcError
val KtorErrorPlugin = createApplicationPlugin(name = "KtorErrorPlugin") {
onCall { call ->
// TODO 在局域网会被扫描工具批量请求多个路径
if (call.request.uri == "/" || call.request.uri.startsWith("/api/")) {
Log.d("Ktor", "onCall: ${call.request.uri}")
}
}
on(CallFailed) { call, cause ->
when (cause) {
is RpcError -> {

View File

@ -120,10 +120,15 @@ private val getAttr: (AccessibilityNodeInfo, String) -> Any? = { node, name ->
when (name) {
"id" -> node.viewIdResourceName
"vid" -> node.viewIdResourceName?.let { id ->
val appId = node.packageName
if (appId != null && id.startsWith(appId) && id.startsWith(":id/", appId.length)) {
id.subSequence(
(node.packageName?.length ?: 0) + ":id/".length,
appId.length + ":id/".length,
id.length
)
} else {
null
}
}
"name" -> node.className

View File

@ -102,22 +102,20 @@ var lastTriggerRule: ResolvedRule? = null
var lastTriggerTime = 0L
var appChangeTime = 0L
fun insertClickLog(appRule: ResolvedRule) {
appRule.trigger()
toastClickTip()
fun insertClickLog(rule: ResolvedRule) {
appScope.launchTry(Dispatchers.IO) {
val clickLog = ClickLog(
appId = topActivityFlow.value.appId,
activityId = topActivityFlow.value.activityId,
subsId = appRule.subsItem.id,
subsVersion = appRule.rawSubs.version,
groupKey = appRule.group.key,
groupType = when (appRule) {
subsId = rule.subsItem.id,
subsVersion = rule.rawSubs.version,
groupKey = rule.group.key,
groupType = when (rule) {
is AppRule -> SubsConfig.AppGroupType
is GlobalRule -> SubsConfig.GlobalGroupType
},
ruleIndex = appRule.index,
ruleKey = appRule.key,
ruleIndex = rule.index,
ruleKey = rule.key,
)
DbSet.clickLogDao.insert(clickLog)
increaseClickCount()

View File

@ -23,6 +23,7 @@ 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
@ -37,11 +38,14 @@ import li.songe.gkd.data.AttrInfo
import li.songe.gkd.data.GkdAction
import li.songe.gkd.data.RawSubscription
import li.songe.gkd.data.RpcError
import li.songe.gkd.data.RuleStatus
import li.songe.gkd.data.SubsVersion
import li.songe.gkd.data.getActionFc
import li.songe.gkd.db.DbSet
import li.songe.gkd.debug.SnapshotExt
import li.songe.gkd.shizuku.shizukuIsSafeOK
import li.songe.gkd.shizuku.useSafeGetTasksFc
import li.songe.gkd.shizuku.useShizukuAliveState
import li.songe.gkd.util.VOLUME_CHANGED_ACTION
import li.songe.gkd.util.client
import li.songe.gkd.util.launchTry
@ -49,6 +53,7 @@ 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.updateStorage
import li.songe.gkd.util.updateSubscription
import li.songe.selector.Selector
import kotlin.coroutines.resume
@ -66,7 +71,25 @@ class GkdAbService : CompositionAbService({
service = null
}
val safeGetTasksFc = useSafeGetTasksFc(scope)
val shizukuAliveFlow = useShizukuAliveState()
val shizukuGrantFlow = MutableStateFlow(false)
var lastCheckShizukuTime = 0L
onAccessibilityEvent { // 借助无障碍轮询校验 shizuku 权限
if (storeFlow.value.enableService && it.eventType == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {// 筛选降低判断频率
val t = System.currentTimeMillis()
if (t - lastCheckShizukuTime > 5000L) {
lastCheckShizukuTime = t
scope.launchTry(Dispatchers.IO) {
shizukuGrantFlow.value = if (shizukuAliveFlow.value) {
shizukuIsSafeOK()
} else {
false
}
}
}
}
}
val safeGetTasksFc = useSafeGetTasksFc(scope, shizukuGrantFlow, shizukuAliveFlow)
// 当锁屏/上拉通知栏时, safeActiveWindow 没有 activityId, 但是此时 shizuku 获取到是前台 app 的 appId 和 activityId
fun getShizukuTopActivity(): TopActivity? {
@ -94,62 +117,65 @@ class GkdAbService : CompositionAbService({
var lastTriggerShizukuTime = 0L
var lastContentEventTime = 0L
val singleThread = Dispatchers.IO.limitedParallelism(1)
val queryThread = Dispatchers.IO.limitedParallelism(1)
val eventThread = Dispatchers.IO.limitedParallelism(1)
onDestroy {
singleThread.cancel()
queryThread.cancel()
}
val loopCheckTask = MutableStateFlow(0)
var queryTaskJob: Job? = null
fun newQueryTask(eventNode: AccessibilityNodeInfo? = null) {
if (!storeFlow.value.enableService) return
val ctx = if (System.currentTimeMillis() - appChangeTime < 5000L) {
Dispatchers.IO
} else {
singleThread
queryThread
}
scope.launchTry(ctx) {
queryTaskJob = scope.launchTry(ctx) {
val activityRule = getCurrentRules()
for (rule in (activityRule.currentRules)) {
val statusCode = rule.statusCode
if (statusCode == 4 && rule.matchDelayJob == null) {
rule.matchDelayJob = scope.launch {
val statusCode = rule.status
if (statusCode == RuleStatus.Status3 && rule.matchDelayJob == null) {
rule.matchDelayJob = scope.launch(queryThread) {
delay(rule.matchDelay)
rule.matchDelayJob = null
newQueryTask()
}
}
if (statusCode != 0) continue
if (statusCode != RuleStatus.StatusOk) continue
val nodeVal = (eventNode ?: safeActiveWindow) ?: continue
val target = rule.query(nodeVal) ?: continue
if (activityRule !== getCurrentRules()) break
if (rule.checkDelay() && rule.actionDelayJob == null) {
rule.actionDelayJob = scope.launch {
rule.actionDelayJob = scope.launch(queryThread) {
delay(rule.actionDelay)
rule.actionDelayJob = null
newQueryTask()
}
continue
}
scope.launch(singleThread) {
if (rule.statusCode != 0) return@launch
scope.launch(queryThread) {
if (rule.status != RuleStatus.StatusOk) return@launch
val actionResult = rule.performAction(context, target)
if (actionResult.result) {
rule.trigger()
if (rule.hasNext) {
scope.launch(queryThread) {
delay(500L)
if (queryTaskJob?.isActive != true) {
newQueryTask()
}
}
}
toastClickTip()
insertClickLog(rule)
LogUtils.d(
*rule.matches.toTypedArray(),
rule.statusText(),
AttrInfo.info2data(nodeVal, 0, 0),
actionResult
)
}
}
}
if (activityRule.currentRules.any { r -> r.statusCode != 5 }) {
loopCheckTask.value++
}
}
}
scope.launch(singleThread) {
loopCheckTask.debounce(5000).collect {
newQueryTask()
}
}
@ -169,7 +195,7 @@ class GkdAbService : CompositionAbService({
lastContentEventTime = fixedEvent.time
}
// AccessibilityEvent 的 clear 方法会在后续时间被系统调用导致内部数据丢失
// AccessibilityEvent 的 clear 方法会在后续时间被 某些系统 调用导致内部数据丢失
// 因此不要在协程/子线程内传递引用, 此处使用 data class 保存数据
val evAppId = fixedEvent.appId
val evActivityId = fixedEvent.className
@ -225,14 +251,11 @@ class GkdAbService : CompositionAbService({
return@launch
}
if (!storeFlow.value.enableService) return@launch
newQueryTask(eventNode)
}
}
fun checkSubsUpdate() {
scope.launchTry(Dispatchers.IO) { // 自动从网络更新订阅文件
fun checkSubsUpdate() = scope.launchTry(Dispatchers.IO) { // 自动从网络更新订阅文件
LogUtils.d("开始自动检测更新")
subsItemsFlow.value.forEach { subsItem ->
if (subsItem.updateUrl == null) return@forEach
@ -265,14 +288,13 @@ class GkdAbService : CompositionAbService({
mtime = System.currentTimeMillis()
)
DbSet.subsItemDao.update(newItem)
LogUtils.d("更新磁盘订阅文件:${newSubsRaw.name}")
LogUtils.d("更新订阅文件:${newSubsRaw.name}")
} catch (e: Exception) {
e.printStackTrace()
LogUtils.d("检测更新失败", e)
}
}
}
}
var lastUpdateSubsTime = 0L
onAccessibilityEvent {
@ -291,9 +313,7 @@ class GkdAbService : CompositionAbService({
activityRuleFlow.debounce(300).collect {
if (storeFlow.value.enableService) {
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
}"
r.statusText()
}.toTypedArray())
} else {
LogUtils.d(
@ -314,7 +334,7 @@ class GkdAbService : CompositionAbService({
}
}
if (it) {
aliveView = View(context)
val tempView = View(context)
val lp = WindowManager.LayoutParams().apply {
type = WindowManager.LayoutParams.TYPE_ACCESSIBILITY_OVERLAY
format = PixelFormat.TRANSLUCENT
@ -325,10 +345,16 @@ class GkdAbService : CompositionAbService({
}
withContext(Dispatchers.Main) {
try {
wm.addView(aliveView, lp)
// 在某些机型创建失败, 原因未知
wm.addView(tempView, lp)
aliveView = tempView
} catch (e: Exception) {
LogUtils.d(e)
ToastUtils.showShort("创建无障碍悬浮窗失败!")
LogUtils.d("创建无障碍悬浮窗失败", e)
ToastUtils.showShort("创建无障碍悬浮窗失败")
updateStorage(
storeFlow,
storeFlow.value.copy(enableAbFloatWindow = false)
)
}
}
} else {
@ -344,7 +370,7 @@ class GkdAbService : CompositionAbService({
}
fun createReceiver(): BroadcastReceiver {
fun createVolumeReceiver(): BroadcastReceiver {
return object : BroadcastReceiver() {
var lastTriggerTime = -1L
override fun onReceive(context: Context?, intent: Intent?) {
@ -369,7 +395,7 @@ class GkdAbService : CompositionAbService({
context.unregisterReceiver(captureVolumeReceiver)
}
captureVolumeReceiver = if (it) {
createReceiver().apply {
createVolumeReceiver().apply {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
context.registerReceiver(
this, IntentFilter(VOLUME_CHANGED_ACTION), Context.RECEIVER_EXPORTED
@ -393,11 +419,9 @@ class GkdAbService : CompositionAbService({
if (!storeFlow.value.captureScreenshot) return@onAccessibilityEvent
val appId = e.packageName ?: return@onAccessibilityEvent
val appCls = e.className ?: return@onAccessibilityEvent
if (appId.contentEquals("com.miui.screenshot") &&
e.eventType == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED &&
!e.isFullScreen &&
appCls.contentEquals("android.widget.RelativeLayout") &&
e.text.firstOrNull()?.contentEquals("截屏缩略图") == true // [截屏缩略图, 截长屏, 发送]
if (appId.contentEquals("com.miui.screenshot") && e.eventType == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED && !e.isFullScreen && appCls.contentEquals(
"android.widget.RelativeLayout"
) && e.text.firstOrNull()?.contentEquals("截屏缩略图") == true // [截屏缩略图, 截长屏, 发送]
) {
LogUtils.d("captureScreenshot", e)
scope.launchTry(Dispatchers.IO) {
@ -406,7 +430,6 @@ class GkdAbService : CompositionAbService({
}
}
isRunning.value = true
onDestroy {
isRunning.value = false

View File

@ -33,15 +33,11 @@ class ManageService : CompositionService({
) { allRules, clickCount, enableService, abRunning ->
if (!abRunning) return@combine "无障碍未授权"
if (!enableService) return@combine "服务已暂停"
(if (allRules.allGroupSize > 0) {
if (allRules.appSize > 0) {
"${allRules.appSize}应用/${allRules.allGroupSize}规则组"
allRules.numText + if (clickCount > 0) {
"/${clickCount}点击"
} else {
"${allRules.allGroupSize}规则组"
""
}
} else {
"暂无规则"
}) + if (clickCount > 0) "/${clickCount}点击" else ""
}.stateIn(scope, SharingStarted.Eagerly, "").collect { text ->
createNotif(
context, defaultChannel.id, abNotif.copy(

View File

@ -7,8 +7,6 @@ import android.view.Display
import com.blankj.utilcode.util.LogUtils
import com.blankj.utilcode.util.ToastUtils
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
@ -16,7 +14,6 @@ import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn
import li.songe.gkd.composition.CanOnDestroy
import li.songe.gkd.data.DeviceInfo
import li.songe.gkd.util.launchWhile
import li.songe.gkd.util.map
import li.songe.gkd.util.storeFlow
import rikka.shizuku.Shizuku
@ -95,13 +92,11 @@ fun CanOnDestroy.useShizukuAliveState(): StateFlow<Boolean> {
return shizukuAliveFlow
}
fun CanOnDestroy.useSafeGetTasksFc(scope: CoroutineScope): () -> List<ActivityManager.RunningTaskInfo>? {
val shizukuAliveFlow = useShizukuAliveState()
val shizukuGrantFlow = MutableStateFlow(false)
scope.launchWhile(Dispatchers.IO) {
shizukuGrantFlow.value = if (shizukuAliveFlow.value) shizukuIsSafeOK() else false
delay(3000)
}
fun useSafeGetTasksFc(
scope: CoroutineScope,
shizukuGrantFlow: StateFlow<Boolean>,
shizukuAliveFlow: StateFlow<Boolean>
): () -> List<ActivityManager.RunningTaskInfo>? {
val shizukuCanUsedFlow = combine(
shizukuAliveFlow,
shizukuGrantFlow,

View File

@ -38,15 +38,11 @@ class ControlVm @Inject constructor() : ViewModel() {
}.stateIn(viewModelScope, SharingStarted.Eagerly, null)
val subsStatusFlow = combine(allRulesFlow, clickCountFlow) { allRules, clickCount ->
(if (allRules.allGroupSize > 0) {
if (allRules.appSize > 0) {
"${allRules.appSize}应用/${allRules.allGroupSize}规则组"
allRules.numText + if (clickCount > 0) {
"/${clickCount}点击"
} else {
"${allRules.allGroupSize}规则组"
""
}
} else {
"暂无规则"
}) + if (clickCount > 0) "/${clickCount}点击" else ""
}.stateIn(viewModelScope, SharingStarted.Eagerly, "")
}

View File

@ -153,10 +153,10 @@ fun DebugPage() {
val httpServerRunning by HttpService.isRunning.collectAsState()
TextSwitch(
name = "HTTP服务",
desc = "开启HTTP服务, 以便在同一局域网下传递数据" + if (httpServerRunning) "\n${
desc = if (httpServerRunning) "浏览器打开下面任意链接即可自动连接\n${
Ext.getIpAddressInLocalNetwork()
.map { host -> "http://${host}:${store.httpServerPort}" }.joinToString(",")
}" else "",
.map { host -> "http://${host}:${store.httpServerPort}" }.joinToString("\n")
}" else "开启HTTP服务在同一局域网下连接调试工具",
checked = httpServerRunning
) {
if (!checkOrRequestNotifPermission(context)) {

View File

@ -113,7 +113,7 @@ fun SettingsPage() {
Divider()
TextSwitch(name = "前台悬浮窗",
desc = "添加前台透明悬浮窗,关闭可能导致不点击/点击缓慢",
desc = "添加透明悬浮窗,关闭可能导致不点击/点击缓慢",
checked = store.enableAbFloatWindow,
onCheckedChange = {
updateStorage(

View File

@ -76,19 +76,9 @@ fun SubsItemCard(
)
Spacer(modifier = Modifier.width(10.dp))
}
val apps = rawSubscription.apps
val groupsSize = rawSubscription.allGroupSize
val ruleNumText = if (groupsSize > 0) {
if (apps.isNotEmpty()) {
"${apps.size}应用/${groupsSize}规则组"
} else {
"${groupsSize}规则组"
}
} else {
"暂无规则"
}
Text(
text = ruleNumText,
text = rawSubscription.numText,
fontSize = 14.sp
)
}

View File

@ -1,6 +1,7 @@
package li.songe.gkd.ui.theme
import android.os.Build
import androidx.activity.ComponentActivity
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
@ -8,10 +9,12 @@ import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.platform.LocalContext
import androidx.core.view.WindowInsetsControllerCompat
import li.songe.gkd.util.map
import li.songe.gkd.util.storeFlow
@ -23,17 +26,23 @@ fun AppTheme(
content: @Composable () -> Unit,
) {
// https://developer.android.com/jetpack/compose/designsystems/material3?hl=zh-cn
val context = LocalContext.current as ComponentActivity
val scope = rememberCoroutineScope()
val enableDarkTheme by storeFlow.map(scope) { s -> s.enableDarkTheme }.collectAsState()
val systemInDarkTheme = isSystemInDarkTheme()
val darkTheme = enableDarkTheme ?: systemInDarkTheme
val dynamicColor = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
val colorScheme = when {
dynamicColor && darkTheme -> dynamicDarkColorScheme(LocalContext.current)
dynamicColor && !darkTheme -> dynamicLightColorScheme(LocalContext.current)
dynamicColor && darkTheme -> dynamicDarkColorScheme(context)
dynamicColor && !darkTheme -> dynamicLightColorScheme(context)
darkTheme -> DarkColorScheme
else -> LightColorScheme
}
LaunchedEffect(darkTheme) {
WindowInsetsControllerCompat(context.window, context.window.decorView).apply {
isAppearanceLightStatusBars = !darkTheme
}
}
MaterialTheme(
colorScheme = colorScheme, content = content
)

View File

@ -5,22 +5,10 @@ import com.blankj.utilcode.util.ToastUtils
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
fun CoroutineScope.launchWhile(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit,
) = launch(context, start) {
while (isActive) {
block()
}
}
fun CoroutineScope.launchTry(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,

View File

@ -13,12 +13,10 @@ 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.GlobalApp
import li.songe.gkd.data.GlobalRule
import li.songe.gkd.data.RawSubscription
import li.songe.gkd.data.SubsConfig
import li.songe.gkd.db.DbSet
import li.songe.selector.Selector
val subsItemsFlow by lazy {
DbSet.subsItemDao.query().stateIn(appScope, SharingStarted.Eagerly, emptyList())
@ -77,28 +75,32 @@ fun getGroupRawEnable(
} ?: rawGroup.enable ?: true
}
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 }
private val appSize = appIdToRules.keys.size
private val appGroupSize = appIdToGroups.values.sumOf { s -> s.size }
val numText = if (globalGroups.size + appGroupSize > 0) {
if (globalGroups.isNotEmpty()) {
"${globalGroups.size}全局" + if (appGroupSize > 0) {
"/"
} else {
""
}
} else {
""
} + if (appGroupSize > 0) {
"${appSize}应用/${appGroupSize}规则组"
} else {
""
}
} else {
"暂无规则"
}
}
val allRulesFlow by lazy {
@ -124,76 +126,16 @@ val allRulesFlow by lazy {
?: 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
val subRules = groupRaw.rules.map { ruleRaw ->
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,
subsItem = subsItem,
)
}
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
}
}
subRules.forEach { r ->
r.groupRules = subRules
}
globalRules.addAll(subRules)
}
@ -216,79 +158,17 @@ val allRulesFlow by lazy {
)
}.forEach { groupRaw ->
subAppGroups.add(groupRaw)
val subRules = groupRaw.rules.mapIndexed { ruleIndex, ruleRaw ->
val activityIds =
getFixActivityIds(
appRaw.id,
ruleRaw.activityIds ?: groupRaw.activityIds
)
val excludeActivityIds = getFixActivityIds(
appRaw.id,
ruleRaw.excludeActivityIds ?: groupRaw.excludeActivityIds,
)
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
val subRules = groupRaw.rules.map { ruleRaw ->
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,
subsItem = subsItem,
)
}
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
}
}
subRules.forEach { r ->
r.groupRules = subRules
}
if (subRules.isNotEmpty()) {
val rules = appRules[appRaw.id] ?: mutableListOf()