diff --git a/app/src/main/java/li/songe/gkd/data/Rule.kt b/app/src/main/java/li/songe/gkd/data/Rule.kt index 03cbbccad..bde68a0f1 100644 --- a/app/src/main/java/li/songe/gkd/data/Rule.kt +++ b/app/src/main/java/li/songe/gkd/data/Rule.kt @@ -6,10 +6,11 @@ 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.lastTriggerRuleFlow -import li.songe.gkd.service.launcherActivityIdFlow +import li.songe.gkd.service.lastTriggerRule +import li.songe.gkd.service.openAdOptimized import li.songe.gkd.service.querySelector import li.songe.selector.Selector @@ -42,6 +43,12 @@ data class Rule( val app: SubscriptionRaw.AppRaw, val subsItem: SubsItem, ) { + + /** + * 优化: 切换 APP 后短时间内, 如果存在开屏广告的规则并且没有一次触发, 则不启用其它规则, 避免过多规则阻塞运行 + */ + val isOpenAd = group.name.startsWith("开屏广告") + /** * 任意一个元素是上次点击过的 */ @@ -51,6 +58,10 @@ data class Rule( fun triggerDelay() { // 触发延迟, 一段时间内此规则不可利用 actionDelayTriggerTime = System.currentTimeMillis() + LogUtils.d( + "触发延迟", + "subsId:${subsItem.id}, gKey=${group.key}, gName:${group.name}, ruleIndex:${index}, rKey:${key}, delay:${actionDelay}" + ) } var actionTriggerTime = 0L @@ -59,7 +70,10 @@ data class Rule( // 重置延迟点 actionDelayTriggerTime = 0L actionCount++ - lastTriggerRuleFlow.value = this + lastTriggerRule = this + if (isOpenAd && openAdOptimized == true) { + openAdOptimized = false + } } var actionCount = 0 @@ -91,12 +105,15 @@ data class Rule( fun matchActivityId(activityId: String?): Boolean { if (matchAnyActivity) return true - if (activityId == null) return false + if (activityId == null) { + if (matchLauncher) { + return true + } + return false + } if (excludeActivityIds.any { activityId.startsWith(it) }) return false if (activityIds.isEmpty()) return true - if (matchLauncher && launcherActivityIdFlow.value == activityId) { - return true - } + return activityIds.any { activityId.startsWith(it) } } diff --git a/app/src/main/java/li/songe/gkd/data/SubscriptionRaw.kt b/app/src/main/java/li/songe/gkd/data/SubscriptionRaw.kt index 81b00eb9a..4c75c17e6 100644 --- a/app/src/main/java/li/songe/gkd/data/SubscriptionRaw.kt +++ b/app/src/main/java/li/songe/gkd/data/SubscriptionRaw.kt @@ -5,7 +5,7 @@ import kotlinx.parcelize.IgnoredOnParcel import kotlinx.serialization.Serializable import kotlinx.serialization.encodeToString import kotlinx.serialization.json.* -import li.songe.gkd.util.Singleton +import li.songe.gkd.util.json import li.songe.selector.Selector @@ -302,12 +302,12 @@ data class SubscriptionRaw( } // 订阅文件状态: 文件不存在, 文件正常, 文件损坏(损坏原因) - fun stringify(source: SubscriptionRaw) = Singleton.json.encodeToString(source) + 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(Singleton.json.parseToJsonElement(text).jsonObject) + val obj = jsonToSubscriptionRaw(json.parseToJsonElement(text).jsonObject) val duplicatedApps = obj.apps.groupingBy { it }.eachCount().filter { it.value > 1 }.keys if (duplicatedApps.isNotEmpty()) { @@ -334,12 +334,12 @@ data class SubscriptionRaw( fun parseAppRaw(source: String, json5: Boolean = true): AppRaw { val text = if (json5) Jankson.builder().build().load(source).toJson() else source - return jsonToAppRaw(Singleton.json.parseToJsonElement(text).jsonObject, 0) + 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(Singleton.json.parseToJsonElement(text).jsonObject, 0) + return jsonToGroupRaw(json.parseToJsonElement(text).jsonObject, 0) } } diff --git a/app/src/main/java/li/songe/gkd/db/DbSet.kt b/app/src/main/java/li/songe/gkd/db/DbSet.kt index e47f37bc5..7b4e61a56 100644 --- a/app/src/main/java/li/songe/gkd/db/DbSet.kt +++ b/app/src/main/java/li/songe/gkd/db/DbSet.kt @@ -15,7 +15,7 @@ import li.songe.gkd.appScope 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.Singleton +import li.songe.gkd.util.client import li.songe.gkd.util.dbFolder import li.songe.gkd.util.launchTry import java.io.File @@ -50,7 +50,7 @@ object DbSet { val newSubsRaw = try { withTimeout(3000) { SubscriptionRaw.parse( - Singleton.client.get(defaultSubsItem.updateUrl!!).bodyAsText() + client.get(defaultSubsItem.updateUrl!!).bodyAsText() ) } } catch (e: Exception) { diff --git a/app/src/main/java/li/songe/gkd/debug/HttpService.kt b/app/src/main/java/li/songe/gkd/debug/HttpService.kt index a906e2b8e..a7878a7d9 100644 --- a/app/src/main/java/li/songe/gkd/debug/HttpService.kt +++ b/app/src/main/java/li/songe/gkd/debug/HttpService.kt @@ -31,8 +31,8 @@ import kotlinx.serialization.Serializable import li.songe.gkd.app import li.songe.gkd.appScope import li.songe.gkd.composition.CompositionService -import li.songe.gkd.data.GkdAction import li.songe.gkd.data.DeviceInfo +import li.songe.gkd.data.GkdAction import li.songe.gkd.data.RpcError import li.songe.gkd.data.SubsItem import li.songe.gkd.data.SubscriptionRaw @@ -43,7 +43,7 @@ import li.songe.gkd.notif.httpChannel import li.songe.gkd.notif.httpNotif import li.songe.gkd.service.GkdAbService import li.songe.gkd.util.Ext.getIpAddressInLocalNetwork -import li.songe.gkd.util.Singleton +import li.songe.gkd.util.keepNullJson import li.songe.gkd.util.launchTry import li.songe.gkd.util.map import li.songe.gkd.util.storeFlow @@ -67,7 +67,7 @@ class HttpService : CompositionService({ return embeddedServer(Netty, port, configure = { tcpKeepAlive = true }) { install(KtorCorsPlugin) install(KtorErrorPlugin) - install(ContentNegotiation) { json(Singleton.keepNullJson) } + install(ContentNegotiation) { json(keepNullJson) } routing { get("/") { call.respond("hello world") } diff --git a/app/src/main/java/li/songe/gkd/debug/SnapshotExt.kt b/app/src/main/java/li/songe/gkd/debug/SnapshotExt.kt index f0e7ce9d2..96b549e28 100644 --- a/app/src/main/java/li/songe/gkd/debug/SnapshotExt.kt +++ b/app/src/main/java/li/songe/gkd/debug/SnapshotExt.kt @@ -21,7 +21,7 @@ import li.songe.gkd.data.createComplexSnapshot import li.songe.gkd.data.toSnapshot import li.songe.gkd.db.DbSet import li.songe.gkd.service.GkdAbService -import li.songe.gkd.util.Singleton +import li.songe.gkd.util.keepNullJson import li.songe.gkd.util.snapshotZipDir import li.songe.gkd.util.storeFlow import java.io.File @@ -112,7 +112,7 @@ object SnapshotExt { File(getScreenshotPath(snapshot.id)).outputStream().use { stream -> bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream) } - val text = Singleton.keepNullJson.encodeToString(snapshot) + val text = keepNullJson.encodeToString(snapshot) File(getSnapshotPath(snapshot.id)).writeText(text) DbSet.snapshotDao.insert(snapshot.toSnapshot()) } diff --git a/app/src/main/java/li/songe/gkd/debug/SnapshotTileService.kt b/app/src/main/java/li/songe/gkd/debug/SnapshotTileService.kt index ff8f70f78..d30b33f24 100644 --- a/app/src/main/java/li/songe/gkd/debug/SnapshotTileService.kt +++ b/app/src/main/java/li/songe/gkd/debug/SnapshotTileService.kt @@ -7,7 +7,7 @@ import kotlinx.coroutines.delay import li.songe.gkd.appScope import li.songe.gkd.debug.SnapshotExt.captureSnapshot import li.songe.gkd.service.GkdAbService -import li.songe.gkd.service.topActivityFlow +import li.songe.gkd.service.safeActiveWindow import li.songe.gkd.util.launchTry class SnapshotTileService : TileService() { @@ -18,12 +18,13 @@ class SnapshotTileService : TileService() { ToastUtils.showShort("无障碍没有开启") return } - val oldAppId = topActivityFlow.value?.appId + val oldAppId = service.safeActiveWindow?.packageName + ?: return ToastUtils.showShort("获取界面信息根节点失败") appScope.launchTry { val interval = 500L val waitTime = 3000L var i = 0 - while (topActivityFlow.value?.appId == oldAppId) { + while (oldAppId.contentEquals(service.safeActiveWindow?.packageName)) { service.performGlobalAction(AccessibilityService.GLOBAL_ACTION_BACK) delay(interval) i++ diff --git a/app/src/main/java/li/songe/gkd/service/AbState.kt b/app/src/main/java/li/songe/gkd/service/AbState.kt index 6474e6b38..bfa10eafd 100644 --- a/app/src/main/java/li/songe/gkd/service/AbState.kt +++ b/app/src/main/java/li/songe/gkd/service/AbState.kt @@ -1,18 +1,15 @@ package li.songe.gkd.service +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.stateIn import li.songe.gkd.appScope +import li.songe.gkd.data.ClickLog import li.songe.gkd.data.Rule +import li.songe.gkd.db.DbSet import li.songe.gkd.util.appIdToRulesFlow +import li.songe.gkd.util.increaseClickCount import li.songe.gkd.util.launchTry -import li.songe.gkd.util.map - -val launcherActivityIdFlow by lazy { - MutableStateFlow(null) -} +import li.songe.gkd.util.recordStoreFlow data class TopActivity( val appId: String, @@ -23,46 +20,86 @@ val topActivityFlow by lazy { MutableStateFlow(null) } -val currentRulesFlow by lazy { - combine(appIdToRulesFlow, topActivityFlow) { appIdToRules, topActivity -> - (appIdToRules[topActivity?.appId] ?: emptyList()).filter { rule -> - rule.matchActivityId(topActivity?.activityId) - } - }.stateIn(appScope, SharingStarted.Eagerly, emptyList()) -} +data class ActivityRule( + val rules: List = emptyList(), + val topActivity: TopActivity? = null, + val appIdToRules: Map> = emptyMap(), +) + +val activityRuleFlow by lazy { MutableStateFlow(ActivityRule()) } -val lastTriggerRuleFlow by lazy { - MutableStateFlow(null) +private val lastActivityIdCacheMap by lazy { mutableMapOf() } + +private fun getFixTopActivity(): TopActivity? { + val top = topActivityFlow.value ?: return null + if (top.activityId == null) { + topActivityFlow.value = top.copy(activityId = lastActivityIdCacheMap[top.appId]) + } else { + // 当从通知栏上拉返回应用等时, activityId 的无障碍事件不会触发, 此时使用上一次获得的 activityId 填充 + lastActivityIdCacheMap[top.appId] = top.activityId + } + return topActivityFlow.value } -val activityChangeTimeFlow by lazy { - MutableStateFlow(System.currentTimeMillis()).apply { - appScope.launchTry { - topActivityFlow.collect { - this@apply.value = System.currentTimeMillis() - } +fun getCurrentRules(): ActivityRule { + val topActivity = getFixTopActivity() + val activityRule = activityRuleFlow.value + val appIdToRules = appIdToRulesFlow.value + 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 } } } + return activityRuleFlow.value } -val appChangeTimeFlow by lazy { - topActivityFlow.map(appScope) { t -> t?.appId }.map(appScope) { System.currentTimeMillis() } -} +var lastTriggerRule: Rule? = null +var appChangeTime = 0L +var activityChangeTime = 0L + +// null: app 切换过 +// true: 需要执行优化(此界面组需要存在开屏广告) +// false: 执行优化过了/已经过了优化时间 +var openAdOptimized: Boolean? = null +const val openAdOptimizedTime = 5000L fun isAvailableRule(rule: Rule): Boolean { + val t = System.currentTimeMillis() + if (!rule.isOpenAd && openAdOptimized == true) { + if (t - appChangeTime < openAdOptimizedTime) { + // app 切换一段时间内, 仅开屏广告可使用 + return false + } else { + openAdOptimized = false + } + } if (rule.resetMatchTypeWhenActivity) { - if (activityChangeTimeFlow.value != rule.matchChangeTime) { + if (activityChangeTime != rule.matchChangeTime) { // 当 界面 更新时, 重置操作延迟点, 重置点击次数 rule.actionDelayTriggerTime = 0 rule.actionCount = 0 - rule.matchChangeTime = activityChangeTimeFlow.value + rule.matchChangeTime = activityChangeTime } } else { - if (appChangeTimeFlow.value != rule.matchChangeTime) { + if (appChangeTime != rule.matchChangeTime) { // 当 切换APP 时, 重置点击次数 rule.actionDelayTriggerTime = 0 rule.actionCount = 0 - rule.matchChangeTime = appChangeTimeFlow.value + rule.matchChangeTime = appChangeTime } } if (rule.actionMaximum != null) { @@ -70,15 +107,14 @@ fun isAvailableRule(rule: Rule): Boolean { return false // 达到最大执行次数 } } - val t = System.currentTimeMillis() if (rule.matchDelay != null) { // 处于匹配延迟中 if (rule.resetMatchTypeWhenActivity) { - if (t - activityChangeTimeFlow.value < rule.matchDelay) { + if (t - activityChangeTime < rule.matchDelay) { return false } } else { - if (t - appChangeTimeFlow.value < rule.matchDelay) { + if (t - appChangeTime < rule.matchDelay) { return false } } @@ -86,20 +122,20 @@ fun isAvailableRule(rule: Rule): Boolean { if (rule.matchTime != null) { // 超出了匹配时间 if (rule.resetMatchTypeWhenActivity) { - if (t - activityChangeTimeFlow.value > rule.matchAllTime) { + if (t - activityChangeTime > rule.matchAllTime) { return false } } else { - if (t - appChangeTimeFlow.value > rule.matchAllTime) { + if (t - appChangeTime > rule.matchAllTime) { return false } } } if (rule.actionTriggerTime + rule.actionCd > t) return false // 处于冷却时间 if (rule.preRules.isNotEmpty()) { // 需要提前点击某个规则 - lastTriggerRuleFlow.value ?: return false + lastTriggerRule ?: return false // 上一个点击的规则不在当前需要点击的列表 - return rule.preRules.any { it === lastTriggerRuleFlow.value } + return rule.preRules.any { it === lastTriggerRule } } if (rule.actionDelayTriggerTime > 0) { if (rule.actionDelayTriggerTime + rule.actionDelay > t) { @@ -109,4 +145,22 @@ fun isAvailableRule(rule: Rule): Boolean { return true } - +fun insertClickLog(rule: Rule) { + toastClickTip() + rule.trigger() + 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 + ) + DbSet.clickLogDao.insert(clickLog) + increaseClickCount() + if (recordStoreFlow.value.clickCount % 100 == 0) { + DbSet.clickLogDao.deleteKeepLatest() + } + } +} diff --git a/app/src/main/java/li/songe/gkd/service/GkdAbService.kt b/app/src/main/java/li/songe/gkd/service/GkdAbService.kt index 9597ae911..ec82fcc2b 100644 --- a/app/src/main/java/li/songe/gkd/service/GkdAbService.kt +++ b/app/src/main/java/li/songe/gkd/service/GkdAbService.kt @@ -1,9 +1,6 @@ package li.songe.gkd.service -import android.app.KeyguardManager import android.content.ComponentName -import android.content.Context -import android.content.Intent import android.content.pm.PackageManager import android.graphics.Bitmap import android.graphics.PixelFormat @@ -14,15 +11,13 @@ import android.view.View import android.view.WindowManager import android.view.accessibility.AccessibilityEvent import com.blankj.utilcode.util.LogUtils -import com.blankj.utilcode.util.NetworkUtils -import com.blankj.utilcode.util.ScreenUtils import com.blankj.utilcode.util.ServiceUtils import io.ktor.client.request.get import io.ktor.client.statement.bodyAsText import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.Job import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -30,7 +25,6 @@ import li.songe.gkd.composition.CompositionAbService import li.songe.gkd.composition.CompositionExt.useLifeCycleLog import li.songe.gkd.composition.CompositionExt.useScope import li.songe.gkd.data.ActionResult -import li.songe.gkd.data.ClickLog import li.songe.gkd.data.GkdAction import li.songe.gkd.data.NodeInfo import li.songe.gkd.data.RpcError @@ -38,12 +32,10 @@ import li.songe.gkd.data.SubscriptionRaw import li.songe.gkd.data.getActionFc import li.songe.gkd.db.DbSet import li.songe.gkd.shizuku.useSafeGetTasksFc -import li.songe.gkd.util.Singleton -import li.songe.gkd.util.increaseClickCount +import li.songe.gkd.util.client import li.songe.gkd.util.launchTry import li.songe.gkd.util.launchWhile import li.songe.gkd.util.map -import li.songe.gkd.util.recordStoreFlow import li.songe.gkd.util.storeFlow import li.songe.gkd.util.subsIdToRawFlow import li.songe.gkd.util.subsItemsFlow @@ -72,14 +64,19 @@ class GkdAbService : CompositionAbService({ } val safeGetTasksFc = useSafeGetTasksFc(scope) - fun getActivityIdByShizuku(): String? { - return safeGetTasksFc()?.lastOrNull()?.topActivity?.className + + // 当锁屏/上拉通知栏时, safeActiveWindow 没有 activityId, 但是此时 shizuku 获取到是前台 app 的 appId 和 activityId + fun getShizukuTopActivity(): TopActivity? { + // 平均耗时 5 ms + val top = safeGetTasksFc()?.lastOrNull()?.topActivity ?: return null + return TopActivity(appId = top.packageName, activityId = top.className) } fun isActivity( appId: String, activityId: String, ): Boolean { + if (appId == topActivityFlow.value?.appId && activityId == topActivityFlow.value?.activityId) return true val r = (try { packageManager.getActivityInfo( ComponentName( @@ -94,195 +91,153 @@ class GkdAbService : CompositionAbService({ } var lastTriggerShizukuTime = 0L - onAccessibilityEvent { event -> // 根据事件获取 activityId, 概率不准确 + var lastContentEventTime = 0L + var job: Job? = null + val logs = mutableListOf() + onAccessibilityEvent { event -> if (event == null) return@onAccessibilityEvent - if (event.eventType == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED || event.eventType == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED) { - val newAppId = event.packageName?.toString() ?: return@onAccessibilityEvent - val newActivityId = event.className?.toString() ?: return@onAccessibilityEvent - val rightAppId = - safeActiveWindow?.packageName?.toString() ?: return@onAccessibilityEvent - if (rightAppId != newAppId) return@onAccessibilityEvent + if (!(event.eventType == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED || event.eventType == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED)) return@onAccessibilityEvent + + if (event.eventType == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED) { + if (event.eventTime - appChangeTime > 5_000) { + if (event.eventTime - lastContentEventTime < 100) { + return@onAccessibilityEvent + } + } + lastContentEventTime = event.eventTime + } + val evAppId = event.packageName?.toString() ?: return@onAccessibilityEvent + val evActivityId = event.className?.toString() ?: return@onAccessibilityEvent + val rightAppId = safeActiveWindow?.packageName?.toString() ?: return@onAccessibilityEvent + if (rightAppId == evAppId) { if (event.eventType == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) { // tv.danmaku.bili, com.miui.home, com.miui.home.launcher.Launcher - if (isActivity(newAppId, newActivityId)) { + if (isActivity(evAppId, evActivityId)) { topActivityFlow.value = TopActivity( - newAppId, newActivityId + evAppId, evActivityId ) - activityChangeTimeFlow.value = System.currentTimeMillis() - return@onAccessibilityEvent + activityChangeTime = System.currentTimeMillis() } - } - lastTriggerShizukuTime = - if (newActivityId.startsWith("android.view.") || newActivityId.startsWith("android.widget.")) { - val t = System.currentTimeMillis() - if (t - lastTriggerShizukuTime < if (currentRulesFlow.value.isNotEmpty()) 200 else 400) { - return@onAccessibilityEvent + } else { + if (event.eventTime - lastTriggerShizukuTime > 300) { + val shizukuTop = getShizukuTopActivity() + if (shizukuTop != null && shizukuTop.appId == rightAppId) { + if (shizukuTop.activityId == evActivityId) { + activityChangeTime = System.currentTimeMillis() + } + topActivityFlow.value = shizukuTop } - t - } else { - 0L + lastTriggerShizukuTime = event.eventTime } - val shizukuActivityId = getActivityIdByShizuku() ?: return@onAccessibilityEvent - if (shizukuActivityId == newActivityId) { - activityChangeTimeFlow.value = System.currentTimeMillis() } - topActivityFlow.value = TopActivity( - rightAppId, shizukuActivityId - ) - } - } - - scope.launchWhile(Dispatchers.IO) { - val intent = Intent(Intent.ACTION_MAIN) - intent.addCategory(Intent.CATEGORY_HOME) - val info = context.packageManager.resolveActivity( - intent, 0 - ) - launcherActivityIdFlow.value = info?.activityInfo?.name - delay(10 * 60_000) - } - - val km = context.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager? - - scope.launchWhile(Dispatchers.Default) { // 屏幕无障碍信息轮询 - delay(200) - - if (km?.isKeyguardLocked == true) { // isScreenLock - return@launchWhile } - val rightAppId = safeActiveWindow?.packageName?.toString() - if (rightAppId != null) { - if (rightAppId != topActivityFlow.value?.appId) { - topActivityFlow.value = topActivityFlow.value?.copy( - appId = rightAppId, - activityId = getActivityIdByShizuku() ?: launcherActivityIdFlow.value - ) - return@launchWhile - } else if (topActivityFlow.value?.activityId == null) { - val rightId = getActivityIdByShizuku() - if (rightId != null) { - topActivityFlow.value = topActivityFlow.value?.copy( - appId = rightAppId, activityId = rightId - ) - return@launchWhile - } + if (rightAppId != topActivityFlow.value?.appId) { + // 从 锁屏,下拉通知栏 返回等情况 + val shizukuTop = getShizukuTopActivity() + if (shizukuTop?.appId == rightAppId) { + topActivityFlow.value = shizukuTop + } else { + topActivityFlow.value = TopActivity(rightAppId) } } - if (!storeFlow.value.enableService) return@launchWhile + if (getCurrentRules().rules.isEmpty()) { + return@onAccessibilityEvent + } - val currentRules = currentRulesFlow.value - val topActivity = topActivityFlow.value - for (rule in currentRules) { - if (!isAvailableRule(rule)) continue - val nodeVal = safeActiveWindow ?: continue - val target = rule.query(nodeVal) ?: continue + if (evAppId != rightAppId) return@onAccessibilityEvent + if (!storeFlow.value.enableService) return@onAccessibilityEvent + if (job?.isActive == true) return@onAccessibilityEvent + Log.d( + "event", + "e:${event.eventType}, evId:${evAppId}, evCls:${evActivityId}, okId:${rightAppId}" + ) + logs.add(System.currentTimeMillis()) + if (logs.size > 20) { + LogUtils.d(logs.map { i -> i - logs[0] }.average()) + logs.clear() + } + job = scope.launchTry(Dispatchers.Default) { + val activityRule = getCurrentRules() - if (currentRules !== currentRulesFlow.value || topActivity != topActivityFlow.value) break + for (rule in activityRule.rules) { + if (!isAvailableRule(rule)) continue + val nodeVal = safeActiveWindow ?: continue + val target = rule.query(nodeVal) ?: continue - // 开始 action 延迟 - if (rule.actionDelay > 0 && rule.actionDelayTriggerTime == 0L) { - rule.triggerDelay() - LogUtils.d( - "触发延迟", - "subsId:${rule.subsItem.id}, gKey=${rule.group.key}, gName:${rule.group.name}, ruleIndex:${rule.index}, rKey:${rule.key}, delay:${rule.actionDelay}" - ) - continue - } + if (activityRule !== getCurrentRules()) break - // 如果节点在屏幕外部, click 的结果为 null + // 开始 action 延迟 + if (rule.actionDelay > 0 && rule.actionDelayTriggerTime == 0L) { + rule.triggerDelay() + continue + } - val actionResult = rule.performAction(context, target) - if (actionResult.result) { - toastClickTip() - rule.trigger() - scope.launchTry(Dispatchers.IO) { + // 如果节点在屏幕外部, click 的结果为 null + val actionResult = rule.performAction(context, target) + if (actionResult.result) { LogUtils.d( - *rule.matches.toTypedArray(), NodeInfo.abNodeToNode( - nodeVal, target - ).attr, actionResult + *rule.matches.toTypedArray(), + NodeInfo.abNodeToNode(nodeVal, target).attr, + actionResult ) - 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 - ) - DbSet.clickLogDao.insert(clickLog) - increaseClickCount() - if (recordStoreFlow.value.clickCount % 100 == 0) { - DbSet.clickLogDao.deleteKeepLatest() - } + insertClickLog(rule) } } } + } - var lastUpdateSubsTime = System.currentTimeMillis() - scope.launchWhile(Dispatchers.IO) { // 自动从网络更新订阅文件 - delay(10 * 60_000) // 每 10 分钟检查一次 - if (ScreenUtils.isScreenLock() // 锁屏 - || storeFlow.value.updateSubsInterval <= 0 // 暂停更新 - || System.currentTimeMillis() - lastUpdateSubsTime < storeFlow.value.updateSubsInterval.coerceAtLeast( - 60 * 60_000 - ) // 距离上次更新的时间小于更新间隔 - ) { - return@launchWhile - } - if (!NetworkUtils.isAvailable()) {// 产生 io - return@launchWhile - } - subsItemsFlow.value.forEach { subsItem -> - if (subsItem.updateUrl == null) return@forEach - try { - val newSubsRaw = SubscriptionRaw.parse( - Singleton.client.get(subsItem.updateUrl).bodyAsText() - ) - if (newSubsRaw.id != subsItem.id) { - return@forEach - } - val oldSubsRaw = subsIdToRawFlow.value[subsItem.id] - if (oldSubsRaw != null && newSubsRaw.version <= oldSubsRaw.version) { - return@forEach - } - subsItem.subsFile.writeText( - SubscriptionRaw.stringify( - newSubsRaw + var lastUpdateSubsTime = 0L + scope.launchWhile(Dispatchers.IO) { // 自动从网络更新订阅文件 + if (storeFlow.value.updateSubsInterval > 0 && System.currentTimeMillis() - lastUpdateSubsTime < storeFlow.value.updateSubsInterval) { + subsItemsFlow.value.forEach { subsItem -> + if (subsItem.updateUrl == null) return@forEach + try { + val newSubsRaw = SubscriptionRaw.parse( + client.get(subsItem.updateUrl).bodyAsText() ) - ) - val newItem = subsItem.copy( - updateUrl = newSubsRaw.updateUrl ?: subsItem.updateUrl, - mtime = System.currentTimeMillis() - ) - DbSet.subsItemDao.update(newItem) - LogUtils.d("更新磁盘订阅文件:${newSubsRaw.name}") - } catch (e: Exception) { - e.printStackTrace() + if (newSubsRaw.id != subsItem.id) { + return@forEach + } + val oldSubsRaw = subsIdToRawFlow.value[subsItem.id] + if (oldSubsRaw != null && newSubsRaw.version <= oldSubsRaw.version) { + return@forEach + } + subsItem.subsFile.writeText( + SubscriptionRaw.stringify( + newSubsRaw + ) + ) + val newItem = subsItem.copy( + updateUrl = newSubsRaw.updateUrl ?: subsItem.updateUrl, + mtime = System.currentTimeMillis() + ) + DbSet.subsItemDao.update(newItem) + LogUtils.d("更新磁盘订阅文件:${newSubsRaw.name}") + } catch (e: Exception) { + e.printStackTrace() + } } + lastUpdateSubsTime = System.currentTimeMillis() } - lastUpdateSubsTime = System.currentTimeMillis() + delay(30 * 60_000) // 每 30 分钟检查一次 } scope.launch(Dispatchers.IO) { - combine( - topActivityFlow, currentRulesFlow - ) { topActivity, currentRules -> - topActivity to currentRules - }.debounce(300).collect { (topActivity, currentRules) -> + activityRuleFlow.debounce(300).collect { if (storeFlow.value.enableService) { - LogUtils.d(topActivity, *currentRules.map { r -> + 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) }" }.toTypedArray()) } else { LogUtils.d( - topActivity + it.topActivity ) } } diff --git a/app/src/main/java/li/songe/gkd/ui/AppItemPage.kt b/app/src/main/java/li/songe/gkd/ui/AppItemPage.kt index 9d71c113b..15e77d799 100644 --- a/app/src/main/java/li/songe/gkd/ui/AppItemPage.kt +++ b/app/src/main/java/li/songe/gkd/ui/AppItemPage.kt @@ -1,5 +1,6 @@ package li.songe.gkd.ui +import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -61,13 +62,13 @@ import li.songe.gkd.db.DbSet import li.songe.gkd.ui.destinations.GroupItemPageDestination import li.songe.gkd.util.LocalNavController import li.songe.gkd.util.ProfileTransitions -import li.songe.gkd.util.Singleton import li.songe.gkd.util.appInfoCacheFlow +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.storeFlow import li.songe.gkd.util.subsIdToRawFlow -import li.songe.gkd.util.navigate @RootNavGraph @Destination(style = ProfileTransitions::class) @@ -256,7 +257,7 @@ fun AppItemPage( } } TextButton(onClick = { - val groupAppText = Singleton.json.encodeToString( + val groupAppText = json.encodeToString( appRaw?.copy( groups = listOf(showGroupItemVal) ) @@ -299,7 +300,7 @@ fun AppItemPage( ) }) subsItemVal.subsFile.writeText( - Singleton.json.encodeToString( + json.encodeToString( newSubsRaw ) ) @@ -320,7 +321,7 @@ fun AppItemPage( if (editGroupRaw != null && appRawVal != null && subsItemVal != null) { var source by remember { - mutableStateOf(Singleton.json.encodeToString(editGroupRaw)) + mutableStateOf(json.encodeToString(editGroupRaw)) } val oldSource = remember { source } AlertDialog( @@ -374,7 +375,7 @@ fun AppItemPage( }) vm.viewModelScope.launchTry(Dispatchers.IO) { subsItemVal.subsFile.writeText( - Singleton.json.encodeToString( + json.encodeToString( newSubsRaw ) ) @@ -449,7 +450,7 @@ fun AppItemPage( ) }) vm.viewModelScope.launchTry(Dispatchers.IO) { - subsItemVal.subsFile.writeText(Singleton.json.encodeToString(newSubsRaw)) + subsItemVal.subsFile.writeText(json.encodeToString(newSubsRaw)) DbSet.subsItemDao.update(subsItemVal.copy(mtime = System.currentTimeMillis())) showAddDlg = false ToastUtils.showShort("添加成功") diff --git a/app/src/main/java/li/songe/gkd/ui/GroupItemPage.kt b/app/src/main/java/li/songe/gkd/ui/GroupItemPage.kt index 2ffa566fb..98084684c 100644 --- a/app/src/main/java/li/songe/gkd/ui/GroupItemPage.kt +++ b/app/src/main/java/li/songe/gkd/ui/GroupItemPage.kt @@ -31,8 +31,8 @@ import coil.request.ImageRequest import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.annotation.RootNavGraph import li.songe.gkd.util.LocalNavController -import li.songe.gkd.util.Singleton.imageLoader import li.songe.gkd.util.appInfoCacheFlow +import li.songe.gkd.util.imageLoader import li.songe.gkd.util.subsIdToRawFlow diff --git a/app/src/main/java/li/songe/gkd/ui/HomePageVm.kt b/app/src/main/java/li/songe/gkd/ui/HomePageVm.kt index 671a3ec4f..7ee358b18 100644 --- a/app/src/main/java/li/songe/gkd/ui/HomePageVm.kt +++ b/app/src/main/java/li/songe/gkd/ui/HomePageVm.kt @@ -26,9 +26,10 @@ 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.Singleton import li.songe.gkd.util.checkUpdate +import li.songe.gkd.util.client import li.songe.gkd.util.dbFolder +import li.songe.gkd.util.json import li.songe.gkd.util.launchTry import li.songe.gkd.util.storeFlow import java.io.File @@ -48,7 +49,7 @@ class HomePageVm @Inject constructor() : ViewModel() { } if (!localSubsItem.subsFile.exists()) { localSubsItem.subsFile.writeText( - Singleton.json.encodeToString( + json.encodeToString( SubscriptionRaw( id = localSubsItem.id, name = "本地订阅", @@ -66,7 +67,7 @@ class HomePageVm @Inject constructor() : ViewModel() { SnapshotExt.snapshotDir.walk().maxDepth(1).filter { f -> f.isDirectory } .mapNotNull { f -> f.name.toLongOrNull() }.forEach { snapshotId -> DbSet.snapshotDao.insertOrIgnore( - Singleton.json.decodeFromString( + json.decodeFromString( File(SnapshotExt.getSnapshotPath(snapshotId)).readText() ) ) @@ -94,17 +95,16 @@ class HomePageVm @Inject constructor() : ViewModel() { uploadJob = viewModelScope.launchTry(Dispatchers.IO) { uploadStatusFlow.value = LoadStatus.Loading() try { - val response = Singleton.client.submitFormWithBinaryData( - url = FILE_UPLOAD_URL, - formData = formData { + val response = + client.submitFormWithBinaryData(url = FILE_UPLOAD_URL, formData = formData { append("\"file\"", zipFile.readBytes(), Headers.build { append(HttpHeaders.ContentType, "application/x-zip-compressed") append(HttpHeaders.ContentDisposition, "filename=\"file.zip\"") }) }) { - onUpload { bytesSentTotal, contentLength -> - if (uploadStatusFlow.value is LoadStatus.Loading) { - uploadStatusFlow.value = + onUpload { bytesSentTotal, contentLength -> + if (uploadStatusFlow.value is LoadStatus.Loading) { + uploadStatusFlow.value = LoadStatus.Loading(bytesSentTotal / contentLength.toFloat()) } } diff --git a/app/src/main/java/li/songe/gkd/ui/SnapshotVm.kt b/app/src/main/java/li/songe/gkd/ui/SnapshotVm.kt index d281af48e..a25ca0d32 100644 --- a/app/src/main/java/li/songe/gkd/ui/SnapshotVm.kt +++ b/app/src/main/java/li/songe/gkd/ui/SnapshotVm.kt @@ -23,7 +23,7 @@ import li.songe.gkd.db.DbSet import li.songe.gkd.debug.SnapshotExt.getSnapshotZipFile import li.songe.gkd.util.FILE_UPLOAD_URL import li.songe.gkd.util.LoadStatus -import li.songe.gkd.util.Singleton +import li.songe.gkd.util.client import li.songe.gkd.util.launchTry import javax.inject.Inject @@ -41,17 +41,17 @@ class SnapshotVm @Inject constructor() : ViewModel() { val zipFile = getSnapshotZipFile(snapshot.id) uploadStatusFlow.value = LoadStatus.Loading() try { - val response = Singleton.client.submitFormWithBinaryData(url = FILE_UPLOAD_URL, - formData = formData { + val response = + client.submitFormWithBinaryData(url = FILE_UPLOAD_URL, formData = formData { append("\"file\"", zipFile.readBytes(), Headers.build { append(HttpHeaders.ContentType, "application/x-zip-compressed") append(HttpHeaders.ContentDisposition, "filename=\"file.zip\"") }) }) { - onUpload { bytesSentTotal, contentLength -> - if (uploadStatusFlow.value is LoadStatus.Loading) { - uploadStatusFlow.value = - LoadStatus.Loading(bytesSentTotal / contentLength.toFloat()) + onUpload { bytesSentTotal, contentLength -> + if (uploadStatusFlow.value is LoadStatus.Loading) { + uploadStatusFlow.value = + LoadStatus.Loading(bytesSentTotal / contentLength.toFloat()) } } } diff --git a/app/src/main/java/li/songe/gkd/ui/SubsManageVm.kt b/app/src/main/java/li/songe/gkd/ui/SubsManageVm.kt index f8d96d921..b6a474a0d 100644 --- a/app/src/main/java/li/songe/gkd/ui/SubsManageVm.kt +++ b/app/src/main/java/li/songe/gkd/ui/SubsManageVm.kt @@ -15,7 +15,7 @@ import kotlinx.coroutines.withContext import li.songe.gkd.data.SubsItem import li.songe.gkd.data.SubscriptionRaw import li.songe.gkd.db.DbSet -import li.songe.gkd.util.Singleton +import li.songe.gkd.util.client import li.songe.gkd.util.launchTry import li.songe.gkd.util.subsIdToRawFlow import li.songe.gkd.util.subsItemsFlow @@ -41,7 +41,7 @@ class SubsManageVm @Inject constructor() : ViewModel() { refreshingFlow.value = true try { val text = try { - Singleton.client.get(url).bodyAsText() + client.get(url).bodyAsText() } catch (e: Exception) { e.printStackTrace() ToastUtils.showShort("下载订阅文件失败") @@ -98,7 +98,7 @@ class SubsManageVm @Inject constructor() : ViewModel() { val oldSubsRaw = subsIdToRawFlow.value[oldItem.id] try { val newSubsRaw = SubscriptionRaw.parse( - Singleton.client.get(oldItem.updateUrl).bodyAsText() + client.get(oldItem.updateUrl).bodyAsText() ) if (oldSubsRaw != null && newSubsRaw.version <= oldSubsRaw.version) { return@mapNotNull null diff --git a/app/src/main/java/li/songe/gkd/ui/SubsPage.kt b/app/src/main/java/li/songe/gkd/ui/SubsPage.kt index b3199b555..da13d4860 100644 --- a/app/src/main/java/li/songe/gkd/ui/SubsPage.kt +++ b/app/src/main/java/li/songe/gkd/ui/SubsPage.kt @@ -60,8 +60,8 @@ import li.songe.gkd.ui.component.SubsAppCard import li.songe.gkd.ui.destinations.AppItemPageDestination import li.songe.gkd.util.LocalNavController import li.songe.gkd.util.ProfileTransitions -import li.songe.gkd.util.Singleton import li.songe.gkd.util.appInfoCacheFlow +import li.songe.gkd.util.json import li.songe.gkd.util.launchAsFn import li.songe.gkd.util.launchTry import li.songe.gkd.util.navigate @@ -293,7 +293,7 @@ fun SubsPage( val editAppRawVal = editAppRaw if (editAppRawVal != null && subsItemVal != null && subsRaw != null) { var source by remember { - mutableStateOf(Singleton.json.encodeToString(editAppRawVal)) + mutableStateOf(json.encodeToString(editAppRawVal)) } AlertDialog(title = { Text(text = "编辑本地APP规则") }, text = { OutlinedTextField( @@ -354,7 +354,7 @@ fun SubsPage( Text(text = "复制", modifier = Modifier .clickable { ClipboardUtils.copyText( - Singleton.json.encodeToString( + json.encodeToString( menuAppRawVal ) ) @@ -368,7 +368,7 @@ fun SubsPage( // 也许需要二次确认 vm.viewModelScope.launchTry(Dispatchers.IO) { subsItemVal.subsFile.writeText( - Singleton.json.encodeToString( + json.encodeToString( subsRaw.copy(apps = subsRaw.apps.filter { a -> a.id != menuAppRawVal.id }) ) ) diff --git a/app/src/main/java/li/songe/gkd/util/Singleton.kt b/app/src/main/java/li/songe/gkd/util/Singleton.kt index 9b28ae698..3525ef1fc 100644 --- a/app/src/main/java/li/songe/gkd/util/Singleton.kt +++ b/app/src/main/java/li/songe/gkd/util/Singleton.kt @@ -5,6 +5,7 @@ import coil.ImageLoader import coil.decode.GifDecoder import coil.decode.ImageDecoderDecoder import coil.disk.DiskCache +import com.tencent.mmkv.MMKV import io.ktor.client.HttpClient import io.ktor.client.engine.okhttp.OkHttp import io.ktor.client.plugins.contentnegotiation.ContentNegotiation @@ -14,47 +15,47 @@ import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.json.Json import li.songe.gkd.app + +val kv by lazy { MMKV.mmkvWithID("kv")!! } + @OptIn(ExperimentalSerializationApi::class) -object Singleton { - - val json by lazy { - Json { - isLenient = true - ignoreUnknownKeys = true - explicitNulls = false - encodeDefaults = true - } +val json by lazy { + Json { + isLenient = true + ignoreUnknownKeys = true + explicitNulls = false + encodeDefaults = true } +} - val keepNullJson by lazy { - Json { - isLenient = true - ignoreUnknownKeys = true - encodeDefaults = true - } +val keepNullJson by lazy { + Json { + isLenient = true + ignoreUnknownKeys = true + encodeDefaults = true } +} - val client by lazy { - HttpClient(OkHttp) { - install(ContentNegotiation) { - json(json, ContentType.Any) - } - engine { - clientCacheSize = 0 - } +val client by lazy { + HttpClient(OkHttp) { + install(ContentNegotiation) { + json(json, ContentType.Any) + } + engine { + clientCacheSize = 0 } } +} - val imageLoader by lazy { - ImageLoader.Builder(app).components { - if (Build.VERSION.SDK_INT >= 28) { - add(ImageDecoderDecoder.Factory()) - } else { - add(GifDecoder.Factory()) - } - }.diskCache { - DiskCache.Builder().directory(imageCacheDir).build() - }.build() - } +val imageLoader by lazy { + ImageLoader.Builder(app).components { + if (Build.VERSION.SDK_INT >= 28) { + add(ImageDecoderDecoder.Factory()) + } else { + add(GifDecoder.Factory()) + } + }.diskCache { + DiskCache.Builder().directory(imageCacheDir).build() + }.build() +} -} \ No newline at end of file diff --git a/app/src/main/java/li/songe/gkd/util/Store.kt b/app/src/main/java/li/songe/gkd/util/Store.kt index c7b49ba06..13b57ef71 100644 --- a/app/src/main/java/li/songe/gkd/util/Store.kt +++ b/app/src/main/java/li/songe/gkd/util/Store.kt @@ -18,7 +18,7 @@ private inline fun createStorageFlow( val str = kv.getString(key, null) val initValue = if (str != null) { try { - Singleton.json.decodeFromString(str) + json.decodeFromString(str) } catch (e: Exception) { e.printStackTrace() LogUtils.d(e) @@ -31,7 +31,7 @@ private inline fun createStorageFlow( appScope.launch { stateFlow.drop(1).collect { withContext(Dispatchers.IO) { - kv.encode(key, Singleton.json.encodeToString(it)) + kv.encode(key, json.encodeToString(it)) } } } diff --git a/app/src/main/java/li/songe/gkd/util/Upgrade.kt b/app/src/main/java/li/songe/gkd/util/Upgrade.kt index aeba96396..3860867d6 100644 --- a/app/src/main/java/li/songe/gkd/util/Upgrade.kt +++ b/app/src/main/java/li/songe/gkd/util/Upgrade.kt @@ -55,7 +55,7 @@ suspend fun checkUpdate(): NewVersion? { if (checkUpdatingFlow.value) return null checkUpdatingFlow.value = true try { - val newVersion = Singleton.client.get(UPDATE_URL).body() + val newVersion = client.get(UPDATE_URL).body() if (newVersion.versionCode > BuildConfig.VERSION_CODE) { newVersionFlow.value = newVersion.copy(versionLogs = newVersion.versionLogs.takeWhile { v -> v.code > BuildConfig.VERSION_CODE }) @@ -79,18 +79,17 @@ fun startDownload(newVersion: NewVersion) { var job: Job? = null job = appScope.launch(Dispatchers.IO) { try { - val channel = - Singleton.client.get(URI(UPDATE_URL).resolve(newVersion.downloadUrl).toString()) { - onDownload { bytesSentTotal, contentLength -> - // contentLength 在某些机型上概率错误 - val downloadStatus = downloadStatusFlow.value - if (downloadStatus is LoadStatus.Loading) { - downloadStatusFlow.value = LoadStatus.Loading( - bytesSentTotal.toFloat() / (newVersion.fileSize ?: contentLength) - ) - } else if (downloadStatus is LoadStatus.Failure) { - // 提前终止下载 - job?.cancel() + val channel = client.get(URI(UPDATE_URL).resolve(newVersion.downloadUrl).toString()) { + onDownload { bytesSentTotal, contentLength -> + // contentLength 在某些机型上概率错误 + val downloadStatus = downloadStatusFlow.value + if (downloadStatus is LoadStatus.Loading) { + downloadStatusFlow.value = LoadStatus.Loading( + bytesSentTotal.toFloat() / (newVersion.fileSize ?: contentLength) + ) + } else if (downloadStatus is LoadStatus.Failure) { + // 提前终止下载 + job?.cancel() } } }.bodyAsChannel() diff --git a/app/src/main/java/li/songe/gkd/util/kv.kt b/app/src/main/java/li/songe/gkd/util/kv.kt deleted file mode 100644 index 490248eb2..000000000 --- a/app/src/main/java/li/songe/gkd/util/kv.kt +++ /dev/null @@ -1,6 +0,0 @@ -package li.songe.gkd.util - -import com.tencent.mmkv.MMKV - - -val kv by lazy { MMKV.mmkvWithID("kv", MMKV.MULTI_PROCESS_MODE)!! }