feat: filter version (#483)

This commit is contained in:
lisonge 2024-02-10 00:16:50 +08:00
parent 9ce2846f92
commit 705d931dcf
27 changed files with 359 additions and 267 deletions

View File

@ -25,8 +25,6 @@ jobs:
- name: write secrets info
run: |
echo GKD_APP_CENTER_SECRET='${{ secrets.GKD_APP_CENTER_SECRET }}' >> gradle.properties
echo GKD_DEBUG_APP_CENTER_SECRET='${{ secrets.GKD_DEBUG_APP_CENTER_SECRET }}' >> gradle.properties
echo ${{ secrets.GKD_STORE_FILE_BASE64 }} | base64 --decode > ${{ github.workspace }}/key.jks
echo GKD_STORE_FILE='${{ github.workspace }}/key.jks' >> gradle.properties
echo GKD_STORE_PASSWORD='${{ secrets.GKD_STORE_PASSWORD }}' >> gradle.properties

View File

@ -18,8 +18,6 @@ jobs:
- name: write secrets info
run: |
echo GKD_APP_CENTER_SECRET='${{ secrets.GKD_APP_CENTER_SECRET }}' >> gradle.properties
echo GKD_DEBUG_APP_CENTER_SECRET='${{ secrets.GKD_DEBUG_APP_CENTER_SECRET }}' >> gradle.properties
echo ${{ secrets.GKD_STORE_FILE_BASE64 }} | base64 --decode > ${{ github.workspace }}/key.jks
echo GKD_STORE_FILE='${{ github.workspace }}/key.jks' >> gradle.properties
echo GKD_STORE_PASSWORD='${{ secrets.GKD_STORE_PASSWORD }}' >> gradle.properties

View File

@ -69,9 +69,6 @@ android {
"GIT_COMMIT_ID",
jsonStringOf(gitInfo?.commitId)
)
buildConfigField(
"String", "GKD_BUGLY_APP_ID", jsonStringOf(project.properties["GKD_BUGLY_APP_ID"])
)
resourceConfigurations.addAll(listOf("zh", "en"))
ndk {
// noinspection ChromeOsAbiSupport
@ -181,7 +178,6 @@ dependencies {
implementation(libs.rikka.shizuku.provider)
implementation(libs.lsposed.hiddenapibypass)
implementation(libs.tencent.bugly)
implementation(libs.tencent.mmkv)
implementation(libs.androidx.room.runtime)

View File

@ -4,12 +4,10 @@ import android.app.Application
import android.content.Context
import android.os.Build
import com.blankj.utilcode.util.LogUtils
import com.tencent.bugly.crashreport.CrashReport
import com.tencent.mmkv.MMKV
import dagger.hilt.android.HiltAndroidApp
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope
import li.songe.gkd.data.DeviceInfo
import li.songe.gkd.debug.clearHttpSubs
import li.songe.gkd.notif.initChannel
import li.songe.gkd.util.GIT_COMMIT_URL
@ -22,9 +20,9 @@ import org.lsposed.hiddenapibypass.HiddenApiBypass
val appScope by lazy { MainScope() }
private lateinit var _app: Application
private lateinit var innerApp: Application
val app: Application
get() = _app
get() = innerApp
@HiltAndroidApp
@ -38,27 +36,12 @@ class App : Application() {
override fun onCreate() {
super.onCreate()
_app = this
innerApp = this
@Suppress("SENSELESS_COMPARISON") if (BuildConfig.GKD_BUGLY_APP_ID != null) {
CrashReport.setDeviceModel(this, DeviceInfo.instance.model)
CrashReport.setIsDevelopmentDevice(this, BuildConfig.DEBUG)
CrashReport.initCrashReport(applicationContext,
BuildConfig.GKD_BUGLY_APP_ID,
BuildConfig.DEBUG,
CrashReport.UserStrategy(this).apply {
setCrashHandleCallback(object : CrashReport.CrashHandleCallback() {
override fun onCrashHandleStart(
p0: Int,
p1: String?,
p2: String?,
p3: String?,
): MutableMap<String, String> {
LogUtils.d(p0, p1, p2, p3) // 将报错日志输出到本地
return super.onCrashHandleStart(p0, p1, p2, p3)
}
})
})
val errorHandler = Thread.getDefaultUncaughtExceptionHandler()
Thread.setDefaultUncaughtExceptionHandler { t, e ->
LogUtils.d("UncaughtExceptionHandler", t, e)
errorHandler?.uncaughtException(t, e)
}
MMKV.initialize(this)

View File

@ -3,8 +3,8 @@ package li.songe.gkd
import android.app.ActivityManager
import android.content.Context
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Modifier
import androidx.lifecycle.lifecycleScope
import androidx.navigation.compose.rememberNavController
import com.dylanc.activityresult.launcher.PickContentLauncher
@ -31,6 +31,7 @@ import li.songe.gkd.util.storeFlow
@AndroidEntryPoint
class MainActivity : CompositionActivity({
this as MainActivity
useLifeCycleLog()
val launcher = StartActivityLauncher(this)
val pickContentLauncher = PickContentLauncher(this)
@ -62,7 +63,8 @@ class MainActivity : CompositionActivity({
LocalNavController provides navController
) {
DestinationsNavHost(
navGraph = NavGraphs.root, navController = navController, modifier = Modifier
navGraph = NavGraphs.root,
navController = navController
)
}
ConfirmDialog()
@ -70,7 +72,9 @@ class MainActivity : CompositionActivity({
UpgradeDialog()
}
}
})
}) {
val mainVm by viewModels<MainViewModel>()
}

View File

@ -0,0 +1,65 @@
package li.songe.gkd
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import li.songe.gkd.data.SubsItem
import li.songe.gkd.db.DbSet
import li.songe.gkd.util.authActionFlow
import li.songe.gkd.util.checkUpdate
import li.songe.gkd.util.initFolder
import li.songe.gkd.util.launchTry
import li.songe.gkd.util.logZipDir
import li.songe.gkd.util.newVersionApkDir
import li.songe.gkd.util.snapshotZipDir
import li.songe.gkd.util.storeFlow
class MainViewModel : ViewModel() {
init {
appScope.launchTry(Dispatchers.IO) {
val localSubsItem = SubsItem(
id = -2, order = -2, mtime = System.currentTimeMillis()
)
if (!DbSet.subsItemDao.query().first().any { s -> s.id == localSubsItem.id }) {
DbSet.subsItemDao.insert(localSubsItem)
}
}
viewModelScope.launchTry(Dispatchers.IO) {
// 每次进入删除缓存
listOf(snapshotZipDir, newVersionApkDir, logZipDir).forEach { dir ->
if (dir.isDirectory && dir.exists()) {
dir.listFiles()?.forEach { file ->
if (file.isFile) {
file.delete()
}
}
}
}
}
viewModelScope.launchTry(Dispatchers.IO) {
// 在某些机型由于未知原因创建失败
// 在此保证每次重新打开APP都能重新检测创建
initFolder()
}
if (storeFlow.value.autoCheckAppUpdate) {
appScope.launch {
try {
checkUpdate()
} catch (e: Exception) {
e.printStackTrace()
}
}
}
}
override fun onCleared() {
super.onCleared()
authActionFlow.value = null
}
}

View File

@ -7,6 +7,7 @@ class AppRule(
rawSubs: RawSubscription,
exclude: String?,
val app: RawSubscription.RawApp,
val appInfo: AppInfo?,
) : ResolvedRule(
rule = rule,
group = group,
@ -14,9 +15,29 @@ class AppRule(
rawSubs = rawSubs,
exclude = exclude,
) {
val enable = appInfo?.let {
if ((rule.excludeVersionCodes
?: group.excludeVersionCodes)?.contains(appInfo.versionCode) == true
) {
return@let false
}
if ((rule.excludeVersionNames
?: group.excludeVersionNames)?.contains(appInfo.versionName) == true
) {
return@let false
}
(rule.versionCodes ?: group.versionCodes)?.apply {
return@let contains(appInfo.versionCode)
}
(rule.versionNames ?: group.versionNames)?.apply {
return@let contains(appInfo.versionName)
}
null
} ?: true
val appId = app.id
val activityIds = getFixActivityIds(app.id, rule.activityIds ?: group.activityIds)
val excludeActivityIds =
private val activityIds = getFixActivityIds(app.id, rule.activityIds ?: group.activityIds)
private val excludeActivityIds =
(getFixActivityIds(
app.id,
rule.excludeActivityIds ?: group.excludeActivityIds
@ -25,6 +46,7 @@ class AppRule(
override val type = "app"
override fun matchActivity(appId: String, activityId: String?): Boolean {
if (!enable) return false
if (appId != app.id) return false
activityId ?: return true
if (excludeActivityIds.any { activityId.startsWith(it) }) return false

View File

@ -1,5 +1,6 @@
package li.songe.gkd.data
import kotlinx.collections.immutable.ImmutableMap
import li.songe.gkd.service.launcherAppId
import li.songe.gkd.util.systemAppsFlow
@ -16,6 +17,7 @@ class GlobalRule(
group: RawSubscription.RawGlobalGroup,
rawSubs: RawSubscription,
exclude: String?,
appInfoCache: ImmutableMap<String, AppInfo>,
) : ResolvedRule(
rule = rule,
group = group,
@ -23,17 +25,31 @@ class GlobalRule(
rawSubs = rawSubs,
exclude = exclude,
) {
val matchAnyApp = rule.matchAnyApp ?: group.matchAnyApp ?: true
val matchLauncher = rule.matchLauncher ?: group.matchLauncher ?: false
val matchSystemApp = rule.matchSystemApp ?: group.matchSystemApp ?: false
private val matchAnyApp = rule.matchAnyApp ?: group.matchAnyApp ?: true
private val matchLauncher = rule.matchLauncher ?: group.matchLauncher ?: false
private val matchSystemApp = rule.matchSystemApp ?: group.matchSystemApp ?: false
val apps = mutableMapOf<String, GlobalApp>().apply {
(rule.apps ?: group.apps ?: emptyList()).forEach { a ->
val enable = a.enable ?: appInfoCache[a.id]?.let { appInfo ->
if (a.excludeVersionCodes?.contains(appInfo.versionCode) == true) {
return@let false
}
if (a.excludeVersionNames?.contains(appInfo.versionName) == true) {
return@let false
}
a.versionCodes?.apply {
return@let contains(appInfo.versionCode)
}
a.versionNames?.apply {
return@let contains(appInfo.versionName)
}
null
} ?: true
this[a.id] = GlobalApp(
id = a.id,
enable = a.enable ?: true,
enable = enable,
activityIds = getFixActivityIds(a.id, a.activityIds),
excludeActivityIds = getFixActivityIds(a.id, a.excludeActivityIds)
excludeActivityIds = getFixActivityIds(a.id, a.excludeActivityIds),
)
}
}

View File

@ -1,5 +1,6 @@
package li.songe.gkd.data
import androidx.compose.runtime.Immutable
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonElement
@ -16,6 +17,7 @@ import li.songe.gkd.util.json
import li.songe.gkd.util.json5ToJson
import li.songe.selector.Selector
@Immutable
@Serializable
data class RawSubscription(
val id: Long,
@ -76,6 +78,7 @@ data class RawSubscription(
}
}
@Immutable
@Serializable
data class RawApp(
val id: String,
@ -83,6 +86,7 @@ data class RawSubscription(
val groups: List<RawAppGroup> = emptyList(),
)
@Immutable
@Serializable
data class RawCategory(val key: Int, val name: String, val enable: Boolean?)
@ -107,7 +111,7 @@ data class RawSubscription(
val key: Int?
val preKeys: List<Int>?
val action: String?
val matches: List<String>
val matches: List<String>?
val excludeMatches: List<String>?
}
@ -123,6 +127,11 @@ data class RawSubscription(
interface RawAppRuleProps {
val activityIds: List<String>?
val excludeActivityIds: List<String>?
val versionNames: List<String>?
val excludeVersionNames: List<String>?
val versionCodes: List<Long>?
val excludeVersionCodes: List<Long>?
}
interface RawGlobalRuleProps {
@ -132,16 +141,20 @@ data class RawSubscription(
val apps: List<RawGlobalApp>?
}
@Immutable
@Serializable
data class RawGlobalApp(
val id: String,
val enable: Boolean?,
override val activityIds: List<String>?,
override val excludeActivityIds: List<String>?,
override val versionNames: List<String>?,
override val excludeVersionNames: List<String>?,
override val versionCodes: List<Long>?,
override val excludeVersionCodes: List<Long>?,
) : RawAppRuleProps
@Immutable
@Serializable
data class RawGlobalGroup(
override val name: String,
@ -174,6 +187,7 @@ data class RawSubscription(
val allSelectorStrings by lazy {
rules.map { r -> r.matches + (r.excludeMatches ?: emptyList()) }.flatten()
}
val allSelector by lazy {
@ -206,6 +220,7 @@ data class RawSubscription(
}
}
@Immutable
@Serializable
data class RawGlobalRule(
override val actionCd: Long?,
@ -232,6 +247,7 @@ data class RawSubscription(
override val apps: List<RawGlobalApp>?
) : RawRuleProps, RawGlobalRuleProps
@Immutable
@Serializable
data class RawAppGroup(
override val name: String,
@ -254,10 +270,15 @@ data class RawSubscription(
override val activityIds: List<String>?,
override val excludeActivityIds: List<String>?,
override val rules: List<RawAppRule>,
override val versionNames: List<String>?,
override val excludeVersionNames: List<String>?,
override val versionCodes: List<Long>?,
override val excludeVersionCodes: List<Long>?,
) : RawGroupProps, RawAppRuleProps {
val allSelectorStrings by lazy {
rules.map { r -> r.matches + (r.excludeMatches ?: emptyList()) }.flatten()
rules.map { r -> (r.matches ?: emptyList()) + (r.excludeMatches ?: emptyList()) }
.flatten()
}
val allSelector by lazy {
@ -290,13 +311,14 @@ data class RawSubscription(
}
}
@Immutable
@Serializable
data class RawAppRule(
override val name: String?,
override val key: Int?,
override val preKeys: List<Int>?,
override val action: String?,
override val matches: List<String>,
override val matches: List<String>?,
override val excludeMatches: List<String>?,
override val actionCdKey: Int?,
@ -314,11 +336,19 @@ data class RawSubscription(
override val activityIds: List<String>?,
override val excludeActivityIds: List<String>?,
override val versionNames: List<String>?,
override val excludeVersionNames: List<String>?,
override val versionCodes: List<Long>?,
override val excludeVersionCodes: List<Long>?,
) : RawRuleProps, RawAppRuleProps
companion object {
private fun getStringIArray(json: JsonObject? = null, name: String): List<String>? {
private fun getStringIArray(
json: JsonObject? = null,
name: String
): List<String>? {
return when (val element = json?.get(name)) {
JsonNull, null -> null
is JsonObject -> error("Element ${this::class} can not be object")
@ -333,7 +363,6 @@ data class RawSubscription(
}
}
@Suppress("SameParameterValue")
private fun getIntIArray(json: JsonObject? = null, name: String): List<Int>? {
return when (val element = json?.get(name)) {
JsonNull, null -> null
@ -349,6 +378,21 @@ data class RawSubscription(
}
}
private fun getLongIArray(json: JsonObject? = null, name: String): List<Long>? {
return when (val element = json?.get(name)) {
JsonNull, null -> null
is JsonArray -> element.map {
when (it) {
is JsonObject, is JsonArray, JsonNull -> error("Element $it is not a int")
is JsonPrimitive -> it.long
}
}
is JsonPrimitive -> listOf(element.long)
else -> error("Element $element is not a Array")
}
}
private fun getString(json: JsonObject? = null, key: String): String? =
when (val p = json?.get(key)) {
JsonNull, null -> null
@ -402,9 +446,7 @@ data class RawSubscription(
return RawAppRule(
activityIds = getStringIArray(jsonObject, "activityIds"),
excludeActivityIds = getStringIArray(jsonObject, "excludeActivityIds"),
matches = (getStringIArray(
jsonObject, "matches"
) ?: emptyList()),
matches = getStringIArray(jsonObject, "matches"),
excludeMatches = getStringIArray(jsonObject, "excludeMatches"),
key = getInt(jsonObject, "key"),
name = getString(jsonObject, "name"),
@ -422,6 +464,10 @@ data class RawSubscription(
actionMaximumKey = getInt(jsonObject, "actionMaximumKey"),
actionCdKey = getInt(jsonObject, "actionCdKey"),
order = getInt(jsonObject, "order"),
versionCodes = getLongIArray(jsonObject, "versionCodes"),
excludeVersionCodes = getLongIArray(jsonObject, "excludeVersionCodes"),
versionNames = getStringIArray(jsonObject, "versionNames"),
excludeVersionNames = getStringIArray(jsonObject, "excludeVersionNames"),
)
}
@ -459,6 +505,10 @@ data class RawSubscription(
actionCdKey = getInt(jsonObject, "actionCdKey"),
order = getInt(jsonObject, "order"),
scopeKeys = getIntIArray(jsonObject, "scopeKeys"),
versionCodes = getLongIArray(jsonObject, "versionCodes"),
excludeVersionCodes = getLongIArray(jsonObject, "excludeVersionCodes"),
versionNames = getStringIArray(jsonObject, "versionNames"),
excludeVersionNames = getStringIArray(jsonObject, "excludeVersionNames"),
)
}
@ -483,6 +533,10 @@ data class RawSubscription(
enable = getBoolean(jsonObject, "enable"),
activityIds = getStringIArray(jsonObject, "activityIds"),
excludeActivityIds = getStringIArray(jsonObject, "excludeActivityIds"),
versionCodes = getLongIArray(jsonObject, "versionCodes"),
excludeVersionCodes = getLongIArray(jsonObject, "excludeVersionCodes"),
versionNames = getStringIArray(jsonObject, "versionNames"),
excludeVersionNames = getStringIArray(jsonObject, "excludeVersionNames"),
)
}
@ -542,7 +596,7 @@ data class RawSubscription(
jsonToGlobalApp(
jsonElement.jsonObject, index
)
},
} ?: emptyList(),
rules = jsonObject["rules"]?.jsonArray?.map { jsonElement ->
jsonToGlobalRule(jsonElement.jsonObject)
} ?: emptyList(),
@ -559,12 +613,12 @@ data class RawSubscription(
updateUrl = getString(rootJson, "updateUrl"),
supportUri = getString(rootJson, "supportUri"),
checkUpdateUrl = getString(rootJson, "checkUpdateUrl"),
apps = rootJson["apps"]?.jsonArray?.mapIndexed { index, jsonElement ->
apps = (rootJson["apps"]?.jsonArray?.mapIndexed { index, jsonElement ->
jsonToAppRaw(
jsonElement.jsonObject, index
)
} ?: emptyList(),
categories = rootJson["categories"]?.jsonArray?.mapIndexed { index, jsonElement ->
} ?: emptyList()),
categories = (rootJson["categories"]?.jsonArray?.mapIndexed { index, jsonElement ->
RawCategory(
key = getInt(jsonElement.jsonObject, "key")
?: error("miss categories[$index].key"),
@ -572,10 +626,11 @@ data class RawSubscription(
?: error("miss categories[$index].name"),
enable = getBoolean(jsonElement.jsonObject, "enable"),
)
} ?: emptyList(),
globalGroups = rootJson["globalGroups"]?.jsonArray?.mapIndexed { index, jsonElement ->
} ?: emptyList()),
globalGroups = (rootJson["globalGroups"]?.jsonArray?.mapIndexed { index, jsonElement ->
jsonToGlobalGroups(jsonElement.jsonObject, index)
} ?: emptyList())
)
}
private fun <T> List<T>.findDuplicatedItem(predicate: (T) -> Any?): T? {

View File

@ -16,24 +16,24 @@ sealed class ResolvedRule(
) {
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) }
private val preKeys = (rule.preKeys ?: emptyList()).toSet()
private val resetMatch = rule.resetMatch ?: group.resetMatch
private val matches = rule.matches?.map { s -> Selector.parse(s) } ?: emptyList()
private 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
private val matchTime = rule.matchTime ?: group.matchTime
private val quickFind = rule.quickFind ?: group.quickFind ?: false
val actionCdKey = rule.actionCdKey ?: group.actionCdKey
val actionCd = rule.actionCd ?: if (actionCdKey != null) {
private val actionCdKey = rule.actionCdKey ?: group.actionCdKey
private 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) {
private val actionMaximumKey = rule.actionMaximumKey ?: group.actionMaximumKey
private val actionMaximum = rule.actionMaximum ?: if (actionMaximumKey != null) {
group.rules.find { r -> r.key == actionMaximumKey }?.actionMaximum
} else {
null
@ -41,7 +41,7 @@ sealed class ResolvedRule(
val order = rule.order ?: group.order ?: 0
val slowSelectors by lazy {
private val slowSelectors by lazy {
(matches + excludeMatches).filterNot { s ->
((quickFind && s.canQf) || s.isMatchRoot) && !s.connectKeys.contains(
"<<"
@ -83,7 +83,7 @@ sealed class ResolvedRule(
}.toSet()
}
var preRules = emptySet<ResolvedRule>()
private var preRules = emptySet<ResolvedRule>()
val hasNext = group.rules.any { r -> r.preKeys?.any { k -> k == rule.key } == true }
var actionDelayTriggerTime = 0L
@ -96,7 +96,7 @@ sealed class ResolvedRule(
return false
}
var actionTriggerTime = Value(0L)
private var actionTriggerTime = Value(0L)
fun trigger() {
actionTriggerTime.value = System.currentTimeMillis()
lastTriggerTime = actionTriggerTime.value
@ -110,7 +110,7 @@ sealed class ResolvedRule(
var matchChangedTime = 0L
val matchLimitTime = (matchTime ?: 0) + matchDelay
private val matchLimitTime = (matchTime ?: 0) + matchDelay
val resetMatchTypeWhenActivity = when (resetMatch) {
"app" -> false
@ -193,8 +193,7 @@ fun getFixActivityIds(
appId: String,
activityIds: List<String>?,
): List<String> {
activityIds ?: return emptyList()
return activityIds.map { activityId ->
return (activityIds ?: emptyList()).map { activityId ->
if (activityId.startsWith('.')) { // .a.b.c -> com.x.y.x.a.b.c
appId + activityId
} else {

View File

@ -1,5 +1,8 @@
package li.songe.gkd.data
import androidx.compose.runtime.Stable
@Stable
data class Tuple3<T0, T1, T2>(
val t0: T0,
val t1: T1,
@ -7,13 +10,3 @@ data class Tuple3<T0, T1, T2>(
) {
override fun toString() = "($t0, $t1, $t2)"
}
data class Tuple4<T0, T1, T2, T3>(
val t0: T0,
val t1: T1,
val t2: T2,
val t3: T3,
) {
override fun toString() = "($t0, $t1, $t2, $t3)"
}

View File

@ -41,8 +41,8 @@ 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.SERVER_SCRIPT_URL
import li.songe.gkd.util.getIpAddressInLocalNetwork
import li.songe.gkd.util.keepNullJson
import li.songe.gkd.util.launchTry
import li.songe.gkd.util.map

View File

@ -12,8 +12,8 @@ import li.songe.gkd.data.GlobalRule
import li.songe.gkd.data.ResolvedRule
import li.songe.gkd.data.SubsConfig
import li.songe.gkd.db.DbSet
import li.songe.gkd.util.Ext.getDefaultLauncherAppId
import li.songe.gkd.util.RuleSummary
import li.songe.gkd.util.getDefaultLauncherAppId
import li.songe.gkd.util.increaseClickCount
import li.songe.gkd.util.launchTry
import li.songe.gkd.util.recordStoreFlow

View File

@ -316,9 +316,13 @@ fun AppItemPage(
.apply {
set(
indexOfFirst { a -> a.id == appRawVal.id },
appRawVal.copy(groups = appRawVal.groups.filter { g -> g.key != menuGroupRaw.key })
appRawVal.copy(
groups = appRawVal.groups
.filter { g -> g.key != menuGroupRaw.key }
)
)
}
)
})
updateSubscription(newSubsRaw)
DbSet.subsItemDao.update(subsItemVal.copy(mtime = System.currentTimeMillis()))
DbSet.subsConfigDao.delete(
@ -514,11 +518,11 @@ fun AppItemPage(
val newSubsRaw = subsRaw.copy(apps = subsRaw.apps.toMutableList().apply {
set(
indexOfFirst { a -> a.id == appRawVal.id },
appRawVal.copy(groups = appRawVal.groups + tempGroups.mapIndexed { i, g ->
appRawVal.copy(groups = (appRawVal.groups + tempGroups.mapIndexed { i, g ->
g.copy(
key = newKey + i
)
})
}))
)
})
vm.viewModelScope.launchTry(Dispatchers.IO) {

View File

@ -69,13 +69,13 @@ 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.SnapshotPageDestination
import li.songe.gkd.util.Ext
import li.songe.gkd.util.LocalLauncher
import li.songe.gkd.util.LocalNavController
import li.songe.gkd.util.ProfileTransitions
import li.songe.gkd.util.authActionFlow
import li.songe.gkd.util.canDrawOverlaysAuthAction
import li.songe.gkd.util.checkOrRequestNotifPermission
import li.songe.gkd.util.getIpAddressInLocalNetwork
import li.songe.gkd.util.launchAsFn
import li.songe.gkd.util.launchTry
import li.songe.gkd.util.navigate
@ -204,7 +204,7 @@ fun DebugPage() {
Spacer(modifier = Modifier.width(2.dp))
Text(text = "仅本设备可访问")
}
Ext.getIpAddressInLocalNetwork().forEach { host ->
getIpAddressInLocalNetwork().forEach { host ->
Text(
text = "http://${host}:${store.httpServerPort}",
color = MaterialTheme.colorScheme.primary,
@ -242,13 +242,13 @@ fun DebugPage() {
HorizontalDivider()
TextSwitch(
name = "自动清除内存订阅",
desc = "当HTTP服务关闭时,清除内存订阅",
checked = store.autoClearMemorySubs
name = "保留内存订阅",
desc = "当HTTP服务关闭时,保留内存订阅",
checked = !store.autoClearMemorySubs
) {
updateStorage(
storeFlow, store.copy(
autoClearMemorySubs = it
autoClearMemorySubs = !it
)
)
}

View File

@ -175,18 +175,13 @@ fun SubsPage(
}
IconButton(onClick = { expanded = true }) {
Icon(
imageVector = Icons.AutoMirrored.Filled.Sort,
contentDescription = null
imageVector = Icons.AutoMirrored.Filled.Sort, contentDescription = null
)
}
Box(
modifier = Modifier
.wrapContentSize(Alignment.TopStart)
) {
DropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false }
modifier = Modifier.wrapContentSize(Alignment.TopStart)
) {
DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
SortTypeOption.allSubObject.forEach { sortOption ->
DropdownMenuItem(
@ -194,11 +189,11 @@ fun SubsPage(
Row(
verticalAlignment = Alignment.CenterVertically
) {
RadioButton(selected = sortType == sortOption,
RadioButton(
selected = sortType == sortOption,
onClick = {
vm.sortTypeFlow.value = sortOption
}
)
})
Text(sortOption.label)
}
},
@ -213,9 +208,7 @@ fun SubsPage(
Row(
verticalAlignment = Alignment.CenterVertically
) {
Checkbox(
checked = showUninstallApp,
onCheckedChange = {
Checkbox(checked = showUninstallApp, onCheckedChange = {
vm.showUninstallAppFlow.value = it
})
Text("显示未安装应用")
@ -243,8 +236,7 @@ fun SubsPage(
},
) { padding ->
LazyColumn(
modifier = Modifier.padding(padding),
state = listState
modifier = Modifier.padding(padding), state = listState
) {
itemsIndexed(appAndConfigs, { i, a -> i.toString() + a.t0.id }) { _, a ->
val (appRaw, subsConfig, enableSize) = a
@ -440,8 +432,7 @@ fun SubsPage(
shape = RoundedCornerShape(16.dp),
) {
Column {
Text(
text = "复制", modifier = Modifier
Text(text = "复制", modifier = Modifier
.clickable {
ClipboardUtils.copyText(
json.encodeToJson5String(menuAppRawVal)
@ -451,8 +442,7 @@ fun SubsPage(
}
.fillMaxWidth()
.padding(16.dp))
Text(
text = "删除", modifier = Modifier
Text(text = "删除", modifier = Modifier
.clickable {
// 也许需要二次确认
vm.viewModelScope.launchTry(Dispatchers.IO) {

View File

@ -1,16 +1,21 @@
package li.songe.gkd.ui.home
import android.app.Activity
import android.content.Intent
import androidx.compose.material3.Icon
import androidx.compose.material3.NavigationBar
import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.hilt.navigation.compose.hiltViewModel
import com.blankj.utilcode.util.LogUtils
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootNavGraph
import li.songe.gkd.util.ProfileTransitions
@ -27,6 +32,13 @@ fun HomePage() {
val vm = hiltViewModel<HomeVm>()
val tab by vm.tabFlow.collectAsState()
val intent: Intent? = (LocalContext.current as Activity).intent
LaunchedEffect(key1 = intent, block = {
if (intent != null) {
LogUtils.d(intent)
}
})
val appListPage = useAppListPage()
val subsPage = useSubsManagePage()
val controlPage = useControlPage()

View File

@ -20,11 +20,9 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import li.songe.gkd.appScope
import li.songe.gkd.data.GithubPoliciesAsset
import li.songe.gkd.data.RawSubscription
import li.songe.gkd.data.RpcError
@ -35,18 +33,11 @@ import li.songe.gkd.util.FILE_UPLOAD_URL
import li.songe.gkd.util.LoadStatus
import li.songe.gkd.util.SortTypeOption
import li.songe.gkd.util.appInfoCacheFlow
import li.songe.gkd.util.authActionFlow
import li.songe.gkd.util.checkUpdate
import li.songe.gkd.util.clickCountFlow
import li.songe.gkd.util.client
import li.songe.gkd.util.initFolder
import li.songe.gkd.util.launchTry
import li.songe.gkd.util.logZipDir
import li.songe.gkd.util.newVersionApkDir
import li.songe.gkd.util.orderedAppInfosFlow
import li.songe.gkd.util.ruleSummaryFlow
import li.songe.gkd.util.snapshotZipDir
import li.songe.gkd.util.storeFlow
import li.songe.gkd.util.subsIdToRawFlow
import li.songe.gkd.util.subsItemsFlow
import li.songe.gkd.util.toast
@ -58,46 +49,6 @@ import javax.inject.Inject
class HomeVm @Inject constructor() : ViewModel() {
val tabFlow = MutableStateFlow(controlNav)
init {
appScope.launchTry(Dispatchers.IO) {
val localSubsItem = SubsItem(
id = -2, order = -2, mtime = System.currentTimeMillis()
)
if (!DbSet.subsItemDao.query().first().any { s -> s.id == localSubsItem.id }) {
DbSet.subsItemDao.insert(localSubsItem)
}
}
viewModelScope.launchTry(Dispatchers.IO) {
// 每次进入删除缓存
listOf(snapshotZipDir, newVersionApkDir, logZipDir).forEach { dir ->
if (dir.isDirectory && dir.exists()) {
dir.listFiles()?.forEach { file ->
if (file.isFile) {
file.delete()
}
}
}
}
}
viewModelScope.launchTry(Dispatchers.IO) {
// 在某些机型由于未知原因创建失败
// 在此保证每次重新打开APP都能重新检测创建
initFolder()
}
if (storeFlow.value.autoCheckAppUpdate) {
appScope.launch {
try {
checkUpdate()
} catch (e: Exception) {
e.printStackTrace()
}
}
}
}
val uploadStatusFlow = MutableStateFlow<LoadStatus<GithubPoliciesAsset>?>(null)
var uploadJob: Job? = null
@ -133,11 +84,6 @@ class HomeVm @Inject constructor() : ViewModel() {
}
}
override fun onCleared() {
super.onCleared()
authActionFlow.value = null
}
private val latestRecordFlow =
DbSet.clickLogDao.queryLatest().stateIn(viewModelScope, SharingStarted.Eagerly, null)
val latestRecordDescFlow = combine(

View File

@ -50,6 +50,7 @@ import androidx.compose.ui.window.Dialog
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.viewModelScope
import com.blankj.utilcode.util.ClipboardUtils
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import li.songe.gkd.data.SubsItem
@ -109,7 +110,7 @@ fun useSubsManagePage(): ScaffoldExt {
val state = rememberReorderableLazyListState(onMove = { from, to ->
orderSubItems.value = orderSubItems.value.toMutableList().apply {
add(to.index, removeAt(from.index))
}
}.toImmutableList()
}, onDragEnd = { _, _ ->
vm.viewModelScope.launch(Dispatchers.IO) {
val changeItems = mutableListOf<SubsItem>()

View File

@ -6,6 +6,8 @@ import android.content.Intent
import android.content.IntentFilter
import android.content.pm.PackageManager
import android.os.Build
import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.toImmutableMap
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.sync.Mutex
@ -15,10 +17,10 @@ import li.songe.gkd.appScope
import li.songe.gkd.data.AppInfo
import li.songe.gkd.data.toAppInfo
val appInfoCacheFlow = MutableStateFlow(mapOf<String, AppInfo>())
val appInfoCacheFlow = MutableStateFlow(emptyMap<String, AppInfo>().toImmutableMap())
val systemAppInfoCacheFlow =
appInfoCacheFlow.map(appScope) { c -> c.filter { a -> a.value.isSystem } }
appInfoCacheFlow.map(appScope) { c -> c.filter { a -> a.value.isSystem }.toImmutableMap() }
val systemAppsFlow = systemAppInfoCacheFlow.map(appScope) { c -> c.keys }
@ -35,7 +37,7 @@ val orderedAppInfosFlow = appInfoCacheFlow.map(appScope) { c ->
b.name
}
)
}
}.toImmutableList()
}
private val packageReceiver by lazy {
@ -95,7 +97,7 @@ fun updateAppInfo(appIds: List<String>) {
newMap.remove(appId)
}
}
appInfoCacheFlow.value = newMap
appInfoCacheFlow.value = newMap.toImmutableMap()
}
}
}
@ -112,7 +114,7 @@ fun initAppState() {
appMap[packageInfo.packageName] = info
}
}
appInfoCacheFlow.value = appMap
appInfoCacheFlow.value = appMap.toImmutableMap()
}
}
}

View File

@ -0,0 +1,8 @@
package li.songe.gkd.util
import android.graphics.Bitmap
fun Bitmap.isEmptyBitmap(): Boolean {
val emptyBitmap = Bitmap.createBitmap(width, height, config)
return this.sameAs(emptyBitmap)
}

View File

@ -1,40 +0,0 @@
package li.songe.gkd.util
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.Bitmap
import java.net.NetworkInterface
object Ext {
fun Bitmap.isEmptyBitmap(): Boolean {
val emptyBitmap = Bitmap.createBitmap(width, height, config)
return this.sameAs(emptyBitmap)
}
fun getIpAddressInLocalNetwork(): Sequence<String> {
val networkInterfaces = try {
// android.system.ErrnoException: getifaddrs failed: EACCES (Permission denied)
NetworkInterface.getNetworkInterfaces().iterator().asSequence()
} catch (e: Exception) {
toast("获取host失败:" + e.message)
emptySequence()
}
val localAddresses = networkInterfaces.flatMap {
it.inetAddresses.asSequence().filter { inetAddress ->
inetAddress.isSiteLocalAddress && !(inetAddress.hostAddress?.contains(":")
?: false) && inetAddress.hostAddress != "127.0.0.1"
}.map { inetAddress -> inetAddress.hostAddress }
}
return localAddresses
}
fun PackageManager.getDefaultLauncherAppId(): String? {
val intent = Intent(Intent.ACTION_MAIN)
intent.addCategory(Intent.CATEGORY_HOME)
val defaultLauncher = this.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY)
return defaultLauncher?.activityInfo?.packageName
}
}

View File

@ -0,0 +1,20 @@
package li.songe.gkd.util
import java.net.NetworkInterface
fun getIpAddressInLocalNetwork(): List<String> {
val networkInterfaces = try {
// android.system.ErrnoException: getifaddrs failed: EACCES (Permission denied)
NetworkInterface.getNetworkInterfaces().iterator().asSequence()
} catch (e: Exception) {
toast("获取host失败:" + e.message)
emptySequence()
}
val localAddresses = networkInterfaces.flatMap {
it.inetAddresses.asSequence().filter { inetAddress ->
inetAddress.isSiteLocalAddress && !(inetAddress.hostAddress?.contains(":")
?: false) && inetAddress.hostAddress != "127.0.0.1"
}.map { inetAddress -> inetAddress.hostAddress }
}
return localAddresses.toList()
}

View File

@ -0,0 +1,11 @@
package li.songe.gkd.util
import android.content.Intent
import android.content.pm.PackageManager
fun PackageManager.getDefaultLauncherAppId(): String? {
val intent = Intent(Intent.ACTION_MAIN)
intent.addCategory(Intent.CATEGORY_HOME)
val defaultLauncher = this.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY)
return defaultLauncher?.activityInfo?.packageName
}

View File

@ -16,7 +16,6 @@ import android.media.projection.MediaProjectionManager
import android.os.Handler
import android.os.Looper
import com.blankj.utilcode.util.ScreenUtils
import li.songe.gkd.util.Ext.isEmptyBitmap
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine
@ -46,7 +45,7 @@ class ScreenshotUtil(private val context: Context, private val screenshotIntent:
}
// TODO android13 上一半概率获取到全透明图片, android12 暂无此问题
suspend fun execute() = suspendCoroutine<Bitmap> { block ->
suspend fun execute() = suspendCoroutine { block ->
imageReader = ImageReader.newInstance(
width, height,
PixelFormat.RGBA_8888, 2

View File

@ -1,10 +1,17 @@
package li.songe.gkd.util
import com.blankj.utilcode.util.LogUtils
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.ImmutableMap
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.persistentMapOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.toImmutableMap
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
@ -19,11 +26,12 @@ import li.songe.gkd.data.SubsConfig
import li.songe.gkd.db.DbSet
val subsItemsFlow by lazy {
DbSet.subsItemDao.query().stateIn(appScope, SharingStarted.Eagerly, emptyList())
DbSet.subsItemDao.query().map { s -> s.toImmutableList() }
.stateIn(appScope, SharingStarted.Eagerly, persistentListOf())
}
val subsIdToRawFlow by lazy {
MutableStateFlow<Map<Long, RawSubscription>>(emptyMap())
MutableStateFlow<ImmutableMap<Long, RawSubscription>>(persistentMapOf())
}
private val updateSubsFileMutex by lazy { Mutex() }
@ -36,7 +44,7 @@ fun updateSubscription(subscription: RawSubscription) {
} else {
newMap[subscription.id] = subscription
}
subsIdToRawFlow.value = newMap
subsIdToRawFlow.value = newMap.toImmutableMap()
withContext(Dispatchers.IO) {
subsFolder.resolve("${subscription.id}.json")
.writeText(json.encodeToString(subscription))
@ -48,7 +56,7 @@ fun updateSubscription(subscription: RawSubscription) {
fun deleteSubscription(subsId: Long) {
val newMap = subsIdToRawFlow.value.toMutableMap()
newMap.remove(subsId)
subsIdToRawFlow.value = newMap
subsIdToRawFlow.value = newMap.toImmutableMap()
}
fun getGroupRawEnable(
@ -76,11 +84,11 @@ fun getGroupRawEnable(
}
data class RuleSummary(
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 appIdToAllGroups: Map<String, List<Pair<RawSubscription.RawAppGroup, Boolean>>> = emptyMap(),
val globalRules: ImmutableList<GlobalRule> = persistentListOf(),
val globalGroups: ImmutableList<RawSubscription.RawGlobalGroup> = persistentListOf(),
val appIdToRules: ImmutableMap<String, ImmutableList<AppRule>> = persistentMapOf(),
val appIdToGroups: ImmutableMap<String, ImmutableList<RawSubscription.RawAppGroup>> = persistentMapOf(),
val appIdToAllGroups: ImmutableMap<String, ImmutableList<Pair<RawSubscription.RawAppGroup, Boolean>>> = persistentMapOf(),
) {
private val appSize = appIdToRules.keys.size
private val appGroupSize = appIdToGroups.values.sumOf { s -> s.size }
@ -145,7 +153,8 @@ val ruleSummaryFlow by lazy {
group = groupRaw,
rawSubs = rawSubs,
subsItem = subsItem,
exclude = subGlobalSubsConfigs.find { c -> c.groupKey == groupRaw.key }?.exclude
exclude = subGlobalSubsConfigs.find { c -> c.groupKey == groupRaw.key }?.exclude,
appInfoCache = appInfoCache,
)
}
subGlobalGroupToRules[groupRaw] = subRules
@ -190,9 +199,10 @@ val ruleSummaryFlow by lazy {
app = appRaw,
rawSubs = rawSubs,
subsItem = subsItem,
exclude = appGroupConfigs.find { c -> c.groupKey == groupRaw.key }?.exclude
exclude = appGroupConfigs.find { c -> c.groupKey == groupRaw.key }?.exclude,
appInfo = appInfoCache[appRaw.id]
)
}
}.filter { r -> r.enable }
subAppGroupToRules[groupRaw] = subRules
if (subRules.isNotEmpty()) {
val rules = appRules[appRaw.id] ?: mutableListOf()
@ -212,11 +222,12 @@ val ruleSummaryFlow by lazy {
}
}
RuleSummary(
globalRules = globalRules,
globalGroups = globalGroups,
appIdToRules = appRules,
appIdToGroups = appGroups,
appIdToAllGroups = appAllGroups
globalRules = globalRules.toImmutableList(),
globalGroups = globalGroups.toImmutableList(),
appIdToRules = appRules.mapValues { e -> e.value.toImmutableList() }.toImmutableMap(),
appIdToGroups = appGroups.mapValues { e -> e.value.toImmutableList() }.toImmutableMap(),
appIdToAllGroups = appAllGroups.mapValues { e -> e.value.toImmutableList() }
.toImmutableMap()
)
}.stateIn(appScope, SharingStarted.Eagerly, RuleSummary())
}
@ -248,7 +259,7 @@ fun initSubsState() {
author = "gkd",
)
}
subsIdToRawFlow.value = newMap
subsIdToRawFlow.value = newMap.toImmutableMap()
}
}
}

View File

@ -17,7 +17,7 @@ dependencyResolutionManagement {
// https://youtrack.jetbrains.com/issue/KT-55620
// https://stackoverflow.com/questions/69163511
repositoriesMode.set(RepositoriesMode.PREFER_PROJECT)
// repositoriesMode.set(RepositoriesMode.PREFER_PROJECT)
repositories {
mavenLocal()
@ -65,9 +65,9 @@ dependencyResolutionManagement {
plugin("kotlin.android", "org.jetbrains.kotlin.android").version(kotlinVersion)
// compose 编译器的版本, 需要注意它与 compose 的版本没有关联
// https://mvnrepository.com/artifact/androidx.compose.compiler/compiler
version("compose.compilerVersion", "1.5.8")
val composeVersion = "1.6.0"
// https://developer.android.com/jetpack/androidx/releases/compose-compiler
version("compose.compilerVersion", "1.5.9")
val composeVersion = "1.6.1"
library("compose.ui", "androidx.compose.ui:ui:$composeVersion")
library("compose.preview", "androidx.compose.ui:ui-tooling-preview:$composeVersion")
library("compose.tooling", "androidx.compose.ui:ui-tooling:$composeVersion")
@ -78,13 +78,11 @@ dependencyResolutionManagement {
"compose.icons",
"androidx.compose.material:material-icons-extended:$composeVersion"
)
library("compose.material3", "androidx.compose.material3:material3:1.2.0-rc01")
library("compose.material3", "androidx.compose.material3:material3:1.2.0")
library("compose.activity", "androidx.activity:activity-compose:1.8.2")
// https://github.com/Tencent/MMKV/blob/master/README_CN.md
library("tencent.mmkv", "com.tencent:mmkv:1.3.3")
// https://bugly.qq.com/docs/user-guide/instruction-manual-android/
library("tencent.bugly", "com.tencent.bugly:crashreport:4.1.9.3")
// https://github.com/RikkaApps/HiddenApiRefinePlugin
val rikkaVersion = "4.4.0"
@ -140,7 +138,7 @@ dependencyResolutionManagement {
library("junit", "junit:junit:4.13.2")
val ktorVersion = "2.3.7"
val ktorVersion = "2.3.8"
// 请注意,当 client 和 server 版本不一致时, 会报错 socket hang up
library("ktor.server.core", "io.ktor:ktor-server-core:$ktorVersion")
library("ktor.server.cio", "io.ktor:ktor-server-cio:$ktorVersion")
@ -167,6 +165,7 @@ dependencyResolutionManagement {
)
// https://github.com/Kotlin/kotlinx.collections.immutable
// 仍然存在一些限制 kotlinx.serialization https://github.com/Kotlin/kotlinx.collections.immutable/issues/63
library(
"kotlinx.collections.immutable",
"org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.7"