From 43b0166a67699d3193719df3e3d48f786a4cd60b Mon Sep 17 00:00:00 2001 From: lisonge Date: Sat, 2 Sep 2023 18:09:08 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=8F=90=E4=BE=9B=E8=B0=83=E8=AF=95?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 18 ++- app/build.gradle.kts | 1 - app/src/main/java/li/songe/gkd/App.kt | 2 + .../main/java/li/songe/gkd/data/SubsItem.kt | 8 +- .../java/li/songe/gkd/data/SubscriptionRaw.kt | 10 +- app/src/main/java/li/songe/gkd/db/DbSet.kt | 1 + .../java/li/songe/gkd/debug/HttpService.kt | 104 ++++++++++++--- .../java/li/songe/gkd/debug/KtorCorsPlugin.kt | 24 ++++ .../{KtorPlugins.kt => KtorErrorPlugin.kt} | 6 +- app/src/main/java/li/songe/gkd/notif/Notif.kt | 10 ++ .../java/li/songe/gkd/notif/NotifChannel.kt | 6 + .../java/li/songe/gkd/service/GkdAbService.kt | 46 ++++++- .../main/java/li/songe/gkd/ui/AppItemPage.kt | 12 +- .../main/java/li/songe/gkd/ui/ControlPage.kt | 9 +- .../main/java/li/songe/gkd/ui/ControlVm.kt | 24 +++- .../main/java/li/songe/gkd/ui/DebugPage.kt | 2 +- .../main/java/li/songe/gkd/ui/SettingsPage.kt | 122 +++++++++++------- .../java/li/songe/gkd/ui/SubsManagePage.kt | 22 ++-- .../main/java/li/songe/gkd/ui/SubsManageVm.kt | 1 + app/src/main/java/li/songe/gkd/ui/SubsPage.kt | 4 +- .../main/java/li/songe/gkd/util/Singleton.kt | 1 - app/src/main/java/li/songe/gkd/util/Store.kt | 3 +- .../main/java/li/songe/gkd/util/SubsState.kt | 3 + 23 files changed, 320 insertions(+), 119 deletions(-) create mode 100644 app/src/main/java/li/songe/gkd/debug/KtorCorsPlugin.kt rename app/src/main/java/li/songe/gkd/debug/{KtorPlugins.kt => KtorErrorPlugin.kt} (79%) diff --git a/README.md b/README.md index 444e4e1..e934a8f 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,22 @@ # gkd -基于 *无障碍* + *高级选择器* + *订阅规则* 的自定义屏幕点击 APP +基于 **无障碍** + **高级选择器** + **订阅规则** 的自定义屏幕点击 APP -## feature +## 功能 -根据 [高级选择器](https://github.com/gkd-kit/selector) + [订阅规则](https://github.com/gkd-kit/subscription), 它可以实现 +根据 [高级选择器](https://github.com/gkd-kit/selector) + [订阅规则](https://github.com/gkd-kit/subscription), +它可以实现 - 点击跳过任意开屏广告/点击关闭应用内部任意弹窗广告, 如关闭百度贴吧帖子广告卡片/知乎回答底部推荐广告卡片 - 一些快捷操作, 如微信电脑登录自动同意/微信扫描登录自动同意/微信抢红包 + +| | | | | +| --------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- | +| ![image](https://github.com/gkd-kit/gkd/assets/38517192/ad1d9ec6-2694-4072-b56b-f031baaad89e) | ![image](https://github.com/gkd-kit/gkd/assets/38517192/cbc5815a-f07a-4aa7-8805-d0d17e6b5818) | ![image](https://github.com/gkd-kit/gkd/assets/38517192/cacd9bbf-5bc4-4345-84d8-97731c911cbd) | ![image](https://github.com/gkd-kit/gkd/assets/38517192/1b598dcb-1ad9-4ae5-9a6a-3a1b11492bfa) | +| ![image](https://github.com/gkd-kit/gkd/assets/38517192/8fa19f46-6757-4025-83f7-9eaee6ead52a) | ![image](https://github.com/gkd-kit/gkd/assets/38517192/ba30ef9a-83ae-4be2-8802-e150aacaf140) | ![image](https://github.com/gkd-kit/gkd/assets/38517192/e324eba1-af45-490b-bffe-d42585e11d17) | ![image](https://github.com/gkd-kit/gkd/assets/38517192/ca3323c2-436d-4571-bc5d-e72e5b7fcf6f) | + +## 效果 + +| | | | +| ---------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- | +| ![image](https://github.com/gkd-kit/gkd/assets/38517192/ec0e9465-13d2-422b-97e2-644357ea564b) 关闭微信朋友圈广告 | ![image](https://github.com/gkd-kit/gkd/assets/38517192/cd4554f3-dd9f-431b-8e6d-6cfe7ac430ec) 关闭酷安字节 SDK 广告 | ![image](https://github.com/gkd-kit/gkd/assets/38517192/576a7a6d-5196-4184-8b24-980434dfb15a) 关闭贴吧开屏广告及内部广告 | diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 35edf2e..9696395 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -149,7 +149,6 @@ dependencies { implementation(libs.ktor.server.core) implementation(libs.ktor.server.netty) - implementation(libs.ktor.server.cors) implementation(libs.ktor.server.content.negotiation) implementation(libs.ktor.client.core) diff --git a/app/src/main/java/li/songe/gkd/App.kt b/app/src/main/java/li/songe/gkd/App.kt index 44e23d7..8bc7ad6 100644 --- a/app/src/main/java/li/songe/gkd/App.kt +++ b/app/src/main/java/li/songe/gkd/App.kt @@ -9,6 +9,7 @@ import com.tencent.bugly.crashreport.CrashReport import com.tencent.mmkv.MMKV import dagger.hilt.android.HiltAndroidApp import kotlinx.coroutines.MainScope +import li.songe.gkd.debug.clearHttpSubs import li.songe.gkd.notif.initChannel import li.songe.gkd.util.initAppState import li.songe.gkd.util.initStore @@ -52,6 +53,7 @@ class App : Application() { initAppState() initSubsState() initUpgrade() + clearHttpSubs() } } } \ No newline at end of file diff --git a/app/src/main/java/li/songe/gkd/data/SubsItem.kt b/app/src/main/java/li/songe/gkd/data/SubsItem.kt index 93ad8e9..67c3968 100644 --- a/app/src/main/java/li/songe/gkd/data/SubsItem.kt +++ b/app/src/main/java/li/songe/gkd/data/SubsItem.kt @@ -7,6 +7,7 @@ import androidx.room.Dao import androidx.room.Delete import androidx.room.Entity import androidx.room.Insert +import androidx.room.OnConflictStrategy import androidx.room.PrimaryKey import androidx.room.Query import androidx.room.Update @@ -31,8 +32,7 @@ data class SubsItem( @ColumnInfo(name = "enable") val enable: Boolean = true, @ColumnInfo(name = "enable_update") val enableUpdate: Boolean = true, @ColumnInfo(name = "order") val order: Int, - - @ColumnInfo(name = "update_url") val updateUrl: String, + @ColumnInfo(name = "update_url") val updateUrl: String? = null, ) : Parcelable { @@ -55,10 +55,10 @@ data class SubsItem( suspend fun removeAssets() { DbSet.subsItemDao.delete(this) + DbSet.subsConfigDao.deleteSubs(id) withContext(IO) { subsFile.exists() && subsFile.delete() } - DbSet.subsConfigDao.deleteSubs(id) } companion object { @@ -83,7 +83,7 @@ data class SubsItem( @Update suspend fun update(vararg objects: SubsItem): Int - @Insert + @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insert(vararg users: SubsItem): List @Delete 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 3323ab2..049a8cd 100644 --- a/app/src/main/java/li/songe/gkd/data/SubscriptionRaw.kt +++ b/app/src/main/java/li/songe/gkd/data/SubscriptionRaw.kt @@ -1,6 +1,7 @@ package li.songe.gkd.data import android.os.Parcelable +import blue.endless.jankson.Jankson import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.Parcelize import kotlinx.serialization.SerialName @@ -19,7 +20,7 @@ data class SubscriptionRaw( @SerialName("version") val version: Int, @SerialName("author") val author: String? = null, @SerialName("updateUrl") val updateUrl: String? = null, - @SerialName("supportUrl") val supportUrl: String? = null, + @SerialName("supportUri") val supportUri: String? = null, @SerialName("apps") val apps: List = emptyList(), ) : Parcelable { @@ -200,6 +201,7 @@ data class SubscriptionRaw( 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 @@ -251,7 +253,7 @@ data class SubscriptionRaw( cd = getLong(groupsJson, "cd"), name = getString(groupsJson, "name"), desc = getString(groupsJson, "desc"), - enable = getBoolean(groupsJson, "boolean"), + enable = getBoolean(groupsJson, "enable"), key = getInt(groupsJson, "key") ?: groupIndex, rules = when (val rulesJson = groupsJson["rules"]) { null, JsonNull -> emptyList() @@ -297,7 +299,7 @@ data class SubscriptionRaw( version = getInt(rootJson, "version") ?: error("miss subscription.version"), author = getString(rootJson, "author"), updateUrl = getString(rootJson, "updateUrl"), - supportUrl = getString(rootJson, "supportUrl"), + supportUri = getString(rootJson, "supportUrl"), apps = rootJson["apps"]?.jsonArray?.mapIndexed { index, jsonElement -> jsonToAppRaw( jsonElement.jsonObject, index @@ -334,7 +336,7 @@ data class SubscriptionRaw( fun parse5(source: String): SubscriptionRaw { return parse( - Singleton.json5.load(source).toJson() + Jankson.builder().build().load(source).toJson() ) } } 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 f478b29..bb31b32 100644 --- a/app/src/main/java/li/songe/gkd/db/DbSet.kt +++ b/app/src/main/java/li/songe/gkd/db/DbSet.kt @@ -52,6 +52,7 @@ object DbSet { ) db.insert("subs_item", SQLiteDatabase.CONFLICT_IGNORE, subsItem.toContentValues()) appScope.launch(Dispatchers.IO) { + if (subsItem.updateUrl == null) return@launch try { val s = System.currentTimeMillis() val newSubsRaw = withTimeout(3000) { 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 260d681..10b2439 100644 --- a/app/src/main/java/li/songe/gkd/debug/HttpService.kt +++ b/app/src/main/java/li/songe/gkd/debug/HttpService.kt @@ -10,9 +10,10 @@ import io.ktor.server.application.call import io.ktor.server.application.install import io.ktor.server.engine.embeddedServer import io.ktor.server.netty.Netty +import io.ktor.server.netty.NettyApplicationEngine import io.ktor.server.plugins.contentnegotiation.ContentNegotiation -import io.ktor.server.plugins.cors.routing.CORS import io.ktor.server.request.receive +import io.ktor.server.request.receiveText import io.ktor.server.response.cacheControl import io.ktor.server.response.respond import io.ktor.server.response.respondFile @@ -23,27 +24,47 @@ import io.ktor.server.routing.routing import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.cancel +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map +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.DeviceInfo 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 +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.launchTry import li.songe.gkd.util.storeFlow +import li.songe.gkd.util.subsItemsFlow import java.io.File + class HttpService : CompositionService({ + val context = this val scope = CoroutineScope(Dispatchers.IO) - subsFlow.value = null - val server = - embeddedServer(Netty, storeFlow.value.httpServerPort, configure = { tcpKeepAlive = true }) { - install(CORS) { anyHost() } - install(RpcErrorHeaderPlugin) + + + val httpSubsItem = SubsItem( + id = -1L, + order = -1, + enableUpdate = false, + ) + + val httpSubsRawFlow = MutableStateFlow(null) + fun createServer(port: Int): NettyApplicationEngine { + return embeddedServer(Netty, port, configure = { tcpKeepAlive = true }) { + install(KtorCorsPlugin) + install(KtorErrorPlugin) install(ContentNegotiation) { json() } routing { @@ -77,40 +98,60 @@ class HttpService : CompositionService({ call.respond(DbSet.snapshotDao.query().first()) } get("/subsApps") { - call.respond(subsFlow.value?.apps ?: emptyList()) + call.respond(httpSubsRawFlow.value?.apps ?: emptyList()) } post("/updateSubsApps") { + val subsStr = - """{"name":"GKD-内存订阅","id":-1,"version":0,"author":"@gkd-kit/inspect","apps":${call.receive()}}""" + """{"name":"GKD-内存订阅","id":-1,"version":0,"author":"@gkd-kit/inspect","apps":${call.receiveText()}}""" try { - subsFlow.value = SubscriptionRaw.parse(subsStr) + val httpSubsRaw = SubscriptionRaw.parse(subsStr) + httpSubsItem.subsFile.writeText(SubscriptionRaw.stringify(httpSubsRaw)) + DbSet.subsItemDao.insert((subsItemsFlow.value.find { s -> s.id == httpSubsItem.id } + ?: httpSubsItem).copy(mtime = System.currentTimeMillis())) } catch (e: Exception) { throw RpcError(e.message ?: "未知") } - call.respond("") + call.respond(RpcOk()) + } + post("/execSelector") { + if (!GkdAbService.isRunning()) { + throw RpcError("无障碍没有运行") + } + val text = call.receive>().value + call.respond(RpcOk(GkdAbService.click(text))) } } } } - scope.launchTry(Dispatchers.IO) { - LogUtils.d(*getIpAddressInLocalNetwork().map { host -> "http://${host}:${storeFlow.value.httpServerPort}" } - .toList().toTypedArray()) - server.start(true) } + + var server: NettyApplicationEngine? = null + scope.launchTry(Dispatchers.IO) { + storeFlow.map { s -> s.httpServerPort }.collect { port -> + server?.stop() + server = createServer(port).apply { start() } + createNotif( + context, httpChannel.id, httpNotif.copy(text = "HTTP服务正在运行-端口$port") + ) + LogUtils.d(*getIpAddressInLocalNetwork().map { host -> "http://${host}:${port}" } + .toList().toTypedArray()) + } + } + + onDestroy { - subsFlow.value = null + httpSubsRawFlow.value = null scope.launchTry(Dispatchers.IO) { - server.stop() - LogUtils.d("http server is stopped") + server?.stop() + httpSubsItem.removeAssets() + delay(3000) scope.cancel() } } }) { companion object { - val subsFlow by lazy { MutableStateFlow(null) } - - fun isRunning() = ServiceUtils.isServiceRunning(HttpService::class.java) fun stop(context: Context = app) { if (isRunning()) { @@ -119,8 +160,29 @@ class HttpService : CompositionService({ } fun start(context: Context = app) { - context.startService(Intent(context, HttpService::class.java)) + context.startForegroundService(Intent(context, HttpService::class.java)) } } +} + +@Serializable +data class RpcOk( + val message: String? = null, +) + +@Serializable +data class Value(val value: T) + +fun clearHttpSubs() { + // 如果 app 被直接在任务列表划掉, HTTP订阅会没有清除, 所以在后续的第一次启动时清除 + if (HttpService.isRunning()) return + appScope.launchTry(Dispatchers.IO) { + delay(1000) + SubsItem( + id = -1L, + order = -1, + enableUpdate = false, + ).removeAssets() + } } \ No newline at end of file diff --git a/app/src/main/java/li/songe/gkd/debug/KtorCorsPlugin.kt b/app/src/main/java/li/songe/gkd/debug/KtorCorsPlugin.kt new file mode 100644 index 0000000..ed65817 --- /dev/null +++ b/app/src/main/java/li/songe/gkd/debug/KtorCorsPlugin.kt @@ -0,0 +1,24 @@ +package li.songe.gkd.debug + +import io.ktor.http.HttpHeaders +import io.ktor.http.HttpMethod +import io.ktor.server.application.createApplicationPlugin +import io.ktor.server.request.httpMethod +import io.ktor.server.response.header +import io.ktor.server.response.respond + +// allow all cors +val KtorCorsPlugin = createApplicationPlugin(name = "KtorCorsPlugin") { + onCallRespond { call, _ -> + call.response.header(HttpHeaders.AccessControlAllowOrigin, "*") + call.response.header(HttpHeaders.AccessControlAllowMethods, "*") + call.response.header(HttpHeaders.AccessControlAllowHeaders, "*") + call.response.header(HttpHeaders.AccessControlExposeHeaders, "*") + call.response.header("Access-Control-Allow-Private-Network", "true") + } + onCall { call -> + if (call.request.httpMethod == HttpMethod.Options) { + call.respond("all-cors-ok") + } + } +} \ No newline at end of file diff --git a/app/src/main/java/li/songe/gkd/debug/KtorPlugins.kt b/app/src/main/java/li/songe/gkd/debug/KtorErrorPlugin.kt similarity index 79% rename from app/src/main/java/li/songe/gkd/debug/KtorPlugins.kt rename to app/src/main/java/li/songe/gkd/debug/KtorErrorPlugin.kt index 7a585c9..d0dcc15 100644 --- a/app/src/main/java/li/songe/gkd/debug/KtorPlugins.kt +++ b/app/src/main/java/li/songe/gkd/debug/KtorErrorPlugin.kt @@ -10,7 +10,7 @@ import io.ktor.server.response.header import io.ktor.server.response.respond import li.songe.gkd.data.RpcError -val RpcErrorHeaderPlugin = createApplicationPlugin(name = "RpcErrorHeaderPlugin") { +val KtorErrorPlugin = createApplicationPlugin(name = "KtorErrorPlugin") { onCall { call -> Log.d("Ktor", "onCall: ${call.request.uri}") } @@ -34,8 +34,4 @@ val RpcErrorHeaderPlugin = createApplicationPlugin(name = "RpcErrorHeaderPlugin" } } } - onCallRespond { call, _ -> - call.response.header("Access-Control-Expose-Headers", "*") - call.response.header("Access-Control-Allow-Private-Network", "true") - } } \ No newline at end of file diff --git a/app/src/main/java/li/songe/gkd/notif/Notif.kt b/app/src/main/java/li/songe/gkd/notif/Notif.kt index 6e28106..0f6e92e 100644 --- a/app/src/main/java/li/songe/gkd/notif/Notif.kt +++ b/app/src/main/java/li/songe/gkd/notif/Notif.kt @@ -42,4 +42,14 @@ val floatingNotif by lazy { ongoing = true, autoCancel = false ) +} +val httpNotif by lazy { + Notif( + id = 103, + icon = SafeR.ic_launcher, + title = "搞快点", + text = "HTTP服务正在运行", + ongoing = true, + autoCancel = false + ) } \ No newline at end of file diff --git a/app/src/main/java/li/songe/gkd/notif/NotifChannel.kt b/app/src/main/java/li/songe/gkd/notif/NotifChannel.kt index 60d109d..f2b3963 100644 --- a/app/src/main/java/li/songe/gkd/notif/NotifChannel.kt +++ b/app/src/main/java/li/songe/gkd/notif/NotifChannel.kt @@ -24,9 +24,15 @@ val screenshotChannel by lazy { id = "screenshot", name = "截屏服务", desc = "用于捕获屏幕截屏生成快照" ) } +val httpChannel by lazy { + NotifChannel( + id = "http", name = "HTTP服务", desc = "用于连接Web端工具调试" + ) +} fun initChannel() { createChannel(app, defaultChannel) createChannel(app, floatingChannel) createChannel(app, screenshotChannel) + createChannel(app, httpChannel) } \ No newline at end of file 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 f50c224..dd525c1 100644 --- a/app/src/main/java/li/songe/gkd/service/GkdAbService.kt +++ b/app/src/main/java/li/songe/gkd/service/GkdAbService.kt @@ -17,13 +17,15 @@ import io.ktor.client.statement.bodyAsText import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.delay import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch 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.NodeInfo -import li.songe.gkd.data.SubscriptionRaw import li.songe.gkd.data.ClickLog +import li.songe.gkd.data.NodeInfo +import li.songe.gkd.data.RpcError +import li.songe.gkd.data.SubscriptionRaw import li.songe.gkd.db.DbSet import li.songe.gkd.debug.SnapshotExt import li.songe.gkd.shizuku.activityTaskManager @@ -36,6 +38,7 @@ import li.songe.gkd.util.launchWhileTry import li.songe.gkd.util.storeFlow import li.songe.gkd.util.subsIdToRawFlow import li.songe.gkd.util.subsItemsFlow +import li.songe.selector.Selector import kotlin.coroutines.resume import kotlin.coroutines.suspendCoroutine @@ -153,15 +156,33 @@ class GkdAbService : CompositionAbService({ } } + scope.launchTry { + storeFlow.map { s -> s.updateSubsInterval }.collect { updateSubsInterval -> + if (updateSubsInterval <= 0) { + // clear + } else { + // new task + updateSubsInterval.coerceAtLeast(60 * 60_000) + } + } + } + + var lastUpdateSubsTime = System.currentTimeMillis() scope.launchWhile(IO) { // 自动从网络更新订阅文件 - delay(storeFlow.value.autoUpdateSubsIntervalTimeMillis.coerceAtLeast(30 * 60_000)) - if (!NetworkUtils.isAvailable()) return@launchWhile - if (!storeFlow.value.autoUpdateSubs) return@launchWhile + delay(60_000) + if (!NetworkUtils.isAvailable() || storeFlow.value.updateSubsInterval <= 0) return@launchWhile + if (System.currentTimeMillis() - lastUpdateSubsTime < storeFlow.value.updateSubsInterval.coerceAtLeast( + 60 * 60_000 + ) + ) { + return@launchWhile + } subsItemsFlow.value.forEach { subsItem -> + if (subsItem.updateUrl == null) return@forEach try { - val text = Singleton.client.get(subsItem.updateUrl).bodyAsText() - val newSubsRaw = SubscriptionRaw.parse5(text) + val newSubsRaw = + SubscriptionRaw.parse5(Singleton.client.get(subsItem.updateUrl).bodyAsText()) if (newSubsRaw.id != subsItem.id) { return@forEach } @@ -184,6 +205,7 @@ class GkdAbService : CompositionAbService({ e.printStackTrace() } } + lastUpdateSubsTime = System.currentTimeMillis() } scope.launch { @@ -205,6 +227,16 @@ class GkdAbService : CompositionAbService({ private var service: GkdAbService? = null fun isRunning() = ServiceUtils.isServiceRunning(GkdAbService::class.java) + fun click(source: String): String? { + val serviceVal = service ?: throw RpcError("无障碍没有运行") + val selector = try { + Selector.parse(source) + } catch (e: Exception) { + throw RpcError("非法选择器") + } + return currentAbNode?.querySelector(selector)?.click(serviceVal) + } + val currentAbNode: AccessibilityNodeInfo? get() { 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 d2dc93f..0f9d6e3 100644 --- a/app/src/main/java/li/songe/gkd/ui/AppItemPage.kt +++ b/app/src/main/java/li/songe/gkd/ui/AppItemPage.kt @@ -14,6 +14,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.material.AlertDialog import androidx.compose.material.Scaffold import androidx.compose.material.Switch import androidx.compose.material.Text @@ -149,12 +150,11 @@ fun AppItemPage( showGroupItem?.let { showGroupItemVal -> - Dialog(onDismissRequest = { showGroupItem = null }) { - Text( - text = Singleton.json.encodeToString(showGroupItemVal), - modifier = Modifier.width(400.dp) - ) - } + AlertDialog(onDismissRequest = { showGroupItem = null }, title = { + Text(text = showGroupItemVal.name ?: "-") + }, text = { + Text(text = showGroupItemVal.desc ?: "-") + }, buttons = { }) } } diff --git a/app/src/main/java/li/songe/gkd/ui/ControlPage.kt b/app/src/main/java/li/songe/gkd/ui/ControlPage.kt index 2d9abc6..cf88d34 100644 --- a/app/src/main/java/li/songe/gkd/ui/ControlPage.kt +++ b/app/src/main/java/li/songe/gkd/ui/ControlPage.kt @@ -46,7 +46,8 @@ fun ControlPage() { val context = LocalContext.current as MainActivity val navController = LocalNavController.current val vm = hiltViewModel() - val latestRecordGroup by vm.latestRecordGroup.collectAsState() + val latestRecordDesc by vm.latestRecordDescFlow.collectAsState() + val subsStatus by vm.subsStatusFlow.collectAsState() val store by storeFlow.collectAsState() @@ -106,8 +107,10 @@ fun ControlPage() { text = subsStatus, fontSize = 18.sp ) Spacer(modifier = Modifier.height(2.dp)) - Text(text = latestRecordGroup?.name?.let { "最近点击: $it" } ?: "暂无记录", - fontSize = 14.sp) + Text( + text = if (latestRecordDesc != null) "最近点击: $latestRecordDesc" else "暂无记录", + fontSize = 14.sp + ) } Icon(imageVector = Icons.Default.KeyboardArrowRight, contentDescription = null) } diff --git a/app/src/main/java/li/songe/gkd/ui/ControlVm.kt b/app/src/main/java/li/songe/gkd/ui/ControlVm.kt index 8c7a2f6..6a35ade 100644 --- a/app/src/main/java/li/songe/gkd/ui/ControlVm.kt +++ b/app/src/main/java/li/songe/gkd/ui/ControlVm.kt @@ -8,8 +8,8 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.stateIn import li.songe.gkd.db.DbSet import li.songe.gkd.util.appIdToRulesFlow +import li.songe.gkd.util.appInfoCacheFlow import li.songe.gkd.util.clickCountFlow -import li.songe.gkd.util.storeFlow import li.songe.gkd.util.subsIdToRawFlow import javax.inject.Inject @@ -18,10 +18,24 @@ class ControlVm @Inject constructor() : ViewModel() { private val latestRecordFlow = DbSet.clickLogDb.clickLogDao().queryLatest() .stateIn(viewModelScope, SharingStarted.Eagerly, null) - val latestRecordGroup = - combine(latestRecordFlow, subsIdToRawFlow) { latestRecord, subsIdToRaw -> - subsIdToRaw[latestRecord?.subsId]?.apps?.find { a -> a.id == latestRecord?.appId }?.groups?.find { g -> g.key == latestRecord?.groupKey } - }.stateIn(viewModelScope, SharingStarted.Eagerly, null) + val latestRecordDescFlow = combine( + latestRecordFlow, subsIdToRawFlow, appInfoCacheFlow + ) { latestRecord, subsIdToRaw, appInfoCache -> + if (latestRecord == null) return@combine null + val groupName = + subsIdToRaw[latestRecord?.subsId]?.apps?.find { a -> a.id == latestRecord?.appId }?.groups?.find { g -> g.key == latestRecord?.groupKey }?.name + val appName = appInfoCache[latestRecord?.appId]?.name + val appShowName = appName ?: latestRecord?.appId ?: "" + if (groupName != null) { + if (groupName.contains(appShowName)) { + groupName + } else { + "$appShowName-$groupName" + } + } else { + appShowName + } + }.stateIn(viewModelScope, SharingStarted.Eagerly, null) val subsStatusFlow = combine(appIdToRulesFlow, clickCountFlow) { appIdToRules, clickCount -> val appSize = appIdToRules.keys.size diff --git a/app/src/main/java/li/songe/gkd/ui/DebugPage.kt b/app/src/main/java/li/songe/gkd/ui/DebugPage.kt index 50f7ad9..fadcfda 100644 --- a/app/src/main/java/li/songe/gkd/ui/DebugPage.kt +++ b/app/src/main/java/li/songe/gkd/ui/DebugPage.kt @@ -143,7 +143,7 @@ fun DebugPage() { // Build.VERSION.SDK_INT < Build.VERSION_CODES.R val screenshotRunning by usePollState { ScreenshotService.isRunning() } TextSwitch(name = "截屏服务", - desc = "生成快照需要获取屏幕截图", + desc = "生成快照需要获取屏幕截图,Android11无需开启", screenshotRunning, appScope.launchAsFn { if (it) { diff --git a/app/src/main/java/li/songe/gkd/ui/SettingsPage.kt b/app/src/main/java/li/songe/gkd/ui/SettingsPage.kt index a9af771..809e005 100644 --- a/app/src/main/java/li/songe/gkd/ui/SettingsPage.kt +++ b/app/src/main/java/li/songe/gkd/ui/SettingsPage.kt @@ -1,74 +1,53 @@ package li.songe.gkd.ui -import android.app.Activity -import android.content.Context import android.content.Intent -import android.media.projection.MediaProjectionManager import android.net.Uri -import android.os.Build -import android.provider.Settings -import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.clickable 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.foundation.rememberScrollState -import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.selection.selectable import androidx.compose.foundation.verticalScroll import androidx.compose.material.Divider -import androidx.compose.material.MaterialTheme.typography -import androidx.compose.material.OutlinedTextField +import androidx.compose.material.Icon +import androidx.compose.material.MaterialTheme +import androidx.compose.material.RadioButton import androidx.compose.material.Scaffold import androidx.compose.material.Text -import androidx.compose.material.TextButton import androidx.compose.material.TopAppBar import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Edit +import androidx.compose.material.icons.filled.KeyboardArrowRight 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.rememberCoroutineScope 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.platform.LocalContext -import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Dialog -import androidx.core.content.ContextCompat -import com.blankj.utilcode.util.RomUtils import com.blankj.utilcode.util.ToastUtils -import com.dylanc.activityresult.launcher.launchForResult import com.ramcosta.composedestinations.navigation.navigate import li.songe.gkd.MainActivity import li.songe.gkd.appScope -import li.songe.gkd.debug.FloatingService -import li.songe.gkd.debug.HttpService -import li.songe.gkd.debug.ScreenshotService -import li.songe.gkd.shizuku.shizukuIsSafeOK -import li.songe.gkd.ui.component.AuthCard import li.songe.gkd.ui.component.SettingItem import li.songe.gkd.ui.component.TextSwitch import li.songe.gkd.ui.destinations.AboutPageDestination -import li.songe.gkd.ui.destinations.ClickLogPageDestination import li.songe.gkd.ui.destinations.DebugPageDestination -import li.songe.gkd.ui.destinations.SnapshotPageDestination -import li.songe.gkd.util.Ext -import li.songe.gkd.util.LocalLauncher import li.songe.gkd.util.LocalNavController import li.songe.gkd.util.SafeR import li.songe.gkd.util.checkUpdate -import li.songe.gkd.util.launchAsFn import li.songe.gkd.util.launchTry import li.songe.gkd.util.storeFlow import li.songe.gkd.util.updateStorage -import li.songe.gkd.util.usePollState -import rikka.shizuku.Shizuku val settingsNav = BottomNavItem( label = "设置", icon = SafeR.ic_cog, route = "settings" @@ -77,12 +56,10 @@ val settingsNav = BottomNavItem( @Composable fun SettingsPage() { val context = LocalContext.current as MainActivity - val launcher = LocalLauncher.current val navController = LocalNavController.current - val scope = rememberCoroutineScope() val store by storeFlow.collectAsState() - var showPortDlg by remember { + var showSubsIntervalDlg by remember { mutableStateOf(false) } @@ -114,17 +91,26 @@ fun SettingsPage() { }) Divider() - Spacer(modifier = Modifier.height(5.dp)) - TextSwitch(name = "自动更新订阅", - desc = "每隔一段时间自动更新订阅规则文件", - checked = store.autoUpdateSubs, - onCheckedChange = { - updateStorage( - storeFlow, store.copy( - autoUpdateSubs = it - ) + Row(modifier = Modifier + .clickable { + showSubsIntervalDlg = true + } + .padding(10.dp, 15.dp), verticalAlignment = Alignment.CenterVertically) { + Text( + modifier = Modifier.weight(1f), text = "自动更新订阅", fontSize = 18.sp + ) + Row( + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = radioOptions.find { it.second == store.updateSubsInterval }?.first + ?: store.updateSubsInterval.toString(), fontSize = 14.sp ) - }) + Icon( + imageVector = Icons.Default.KeyboardArrowRight, contentDescription = "more" + ) + } + } Divider() TextSwitch(name = "自动更新应用", @@ -151,7 +137,7 @@ fun SettingsPage() { SettingItem(title = "问题反馈", onClick = { context.startActivity( Intent( - Intent.ACTION_VIEW, Uri.parse("https://github.com/gkd-kit/subscription") + Intent.ACTION_VIEW, Uri.parse("https://github.com/gkd-kit/gkd") ) ) }) @@ -169,4 +155,52 @@ fun SettingsPage() { } }) -} \ No newline at end of file + if (showSubsIntervalDlg) { + Dialog(onDismissRequest = { showSubsIntervalDlg = false }) { + Column( + modifier = Modifier + ) { + radioOptions.forEach { option -> + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .selectable(selected = (option.second == store.updateSubsInterval), + onClick = { + updateStorage( + storeFlow, + storeFlow.value.copy(updateSubsInterval = option.second) + ) + }) + .padding(horizontal = 16.dp) + ) { + RadioButton( + selected = (option.second == store.updateSubsInterval), + onClick = { + updateStorage( + storeFlow, + storeFlow.value.copy(updateSubsInterval = option.second) + ) + }) + Text( + text = option.first, + style = MaterialTheme.typography.body1.merge(), + modifier = Modifier.padding(start = 16.dp) + ) + } + } + } + } + } +} + +val radioOptions = listOf( + "暂停" to -1L, + "每小时" to 60 * 60_000L, + "每12小时" to 12 * 60 * 60_000L, + "每天" to 24 * 60 * 60_000L +) +//data class UpdateInterval( +// val timeMillis: Long, +// val desc: String, +//) diff --git a/app/src/main/java/li/songe/gkd/ui/SubsManagePage.kt b/app/src/main/java/li/songe/gkd/ui/SubsManagePage.kt index 4c90c0c..6e84f88 100644 --- a/app/src/main/java/li/songe/gkd/ui/SubsManagePage.kt +++ b/app/src/main/java/li/songe/gkd/ui/SubsManagePage.kt @@ -273,13 +273,15 @@ fun SubsManagePage() { .background(Color.White) .padding(8.dp) ) { - Text(text = "分享", modifier = Modifier - .clickable { - shareSubItem = menuSubItemVal - menuSubItem = null - } - .fillMaxWidth() - .padding(8.dp)) + if (menuSubItemVal.updateUrl != null) { + Text(text = "分享", modifier = Modifier + .clickable { + shareSubItem = menuSubItemVal + menuSubItem = null + } + .fillMaxWidth() + .padding(8.dp)) + } Text(text = "删除", modifier = Modifier .clickable { @@ -324,7 +326,7 @@ fun SubsManagePage() { verticalArrangement = Arrangement.spacedBy(8.dp) ) { if (subItems.all { it.id != 0L }) { - Text(text = "默认订阅", modifier = Modifier + Text(text = "导入默认订阅", modifier = Modifier .clickable { showAddDialog = false vm.addSubsFromUrl("https://registry.npmmirror.com/@gkd-kit/subscription/latest/files") @@ -334,7 +336,7 @@ fun SubsManagePage() { } Text( - text = "二维码", modifier = Modifier + text = "扫描二维码导入", modifier = Modifier .clickable(onClick = scope.launchAsFn { showAddDialog = false val qrCode = navigateForQrcodeResult() @@ -347,7 +349,7 @@ fun SubsManagePage() { .fillMaxWidth() .padding(8.dp) ) - Text(text = "链接", modifier = Modifier + Text(text = "输入链接导入", modifier = Modifier .clickable { showAddDialog = false showAddLinkDialog = true 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 999c646..ce61a2f 100644 --- a/app/src/main/java/li/songe/gkd/ui/SubsManageVm.kt +++ b/app/src/main/java/li/songe/gkd/ui/SubsManageVm.kt @@ -84,6 +84,7 @@ class SubsManageVm @Inject constructor() : ViewModel() { var errorNum = 0 val oldSubItems = subsItemsFlow.value val newSubsItems = oldSubItems.mapNotNull { oldItem -> + if (oldItem.updateUrl == null) return@mapNotNull null val oldSubsRaw = subsIdToRawFlow.value[oldItem.id] try { val newSubsRaw = SubscriptionRaw.parse5( 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 eb3e431..ffa488e 100644 --- a/app/src/main/java/li/songe/gkd/ui/SubsPage.kt +++ b/app/src/main/java/li/songe/gkd/ui/SubsPage.kt @@ -148,11 +148,11 @@ fun SubsPage( Text(text = "更新: " + formatTimeAgo(subsItem!!.mtime)) } }, confirmButton = { - if (subsRaw.supportUrl != null) { + if (subsRaw.supportUri != null) { TextButton(onClick = { context.startActivity( Intent( - Intent.ACTION_VIEW, Uri.parse(subsRaw.supportUrl) + Intent.ACTION_VIEW, Uri.parse(subsRaw.supportUri) ) ) }) { 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 5372664..448cc25 100644 --- a/app/src/main/java/li/songe/gkd/util/Singleton.kt +++ b/app/src/main/java/li/songe/gkd/util/Singleton.kt @@ -21,7 +21,6 @@ object Singleton { encodeDefaults = true } } - val json5: Jankson by lazy { Jankson.builder().build() } val client by lazy { HttpClient(OkHttp) { 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 9e73b68..8dc95ea 100644 --- a/app/src/main/java/li/songe/gkd/util/Store.kt +++ b/app/src/main/java/li/songe/gkd/util/Store.kt @@ -72,8 +72,7 @@ data class Store( val excludeFromRecents: Boolean = false, val captureScreenshot: Boolean = false, val httpServerPort: Int = 8888, - val autoUpdateSubsIntervalTimeMillis: Long = 60 * 60_000, - val autoUpdateSubs: Boolean = false, + val updateSubsInterval: Long = 60 * 60_000, val captureVolumeKey: Boolean = false, val autoCheckAppUpdate: Boolean = true, ) : Parcelable diff --git a/app/src/main/java/li/songe/gkd/util/SubsState.kt b/app/src/main/java/li/songe/gkd/util/SubsState.kt index 6bbc043..e27403e 100644 --- a/app/src/main/java/li/songe/gkd/util/SubsState.kt +++ b/app/src/main/java/li/songe/gkd/util/SubsState.kt @@ -1,10 +1,12 @@ package li.songe.gkd.util +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch import li.songe.gkd.appScope import li.songe.gkd.data.DeviceInfo import li.songe.gkd.data.Rule @@ -12,6 +14,7 @@ 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.gkd.debug.HttpService import li.songe.selector.Selector