mirror of
https://github.com/gkd-kit/gkd.git
synced 2024-11-16 11:42:22 +08:00
feat: multiplatform
This commit is contained in:
parent
fd999da16f
commit
007655206d
1
_assets/snapshot-1686629593092.json
Normal file
1
_assets/snapshot-1686629593092.json
Normal file
File diff suppressed because one or more lines are too long
|
@ -1,12 +1,17 @@
|
|||
import com.android.build.gradle.internal.cxx.json.jsonStringOf
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
id("kotlin-android")
|
||||
id("kotlin-parcelize")
|
||||
id("kotlin-kapt")
|
||||
id("org.jetbrains.kotlin.plugin.serialization")
|
||||
id("org.jetbrains.kotlin.android")
|
||||
kotlin("android")
|
||||
kotlin("plugin.serialization")
|
||||
id("com.google.devtools.ksp")
|
||||
id("dev.rikka.tools.refine")
|
||||
}
|
||||
|
||||
|
||||
@Suppress("UnstableApiUsage")
|
||||
android {
|
||||
namespace = "li.songe.gkd"
|
||||
|
@ -26,12 +31,17 @@ android {
|
|||
useSupportLibrary = true
|
||||
}
|
||||
|
||||
kapt {
|
||||
arguments {
|
||||
// room 依赖每次构建的产物来执行自动迁移
|
||||
arg("room.schemaLocation", "$projectDir/schemas")
|
||||
javaCompileOptions {
|
||||
annotationProcessorOptions {
|
||||
arguments += mapOf(
|
||||
"room.schemaLocation" to "$projectDir/schemas",
|
||||
"room.incremental" to "true"
|
||||
)
|
||||
}
|
||||
}
|
||||
val nowTime = System.currentTimeMillis()
|
||||
buildConfigField("Long", "BUILD_TIME", jsonStringOf(nowTime) + "L")
|
||||
buildConfigField("String", "BUILD_DATE", jsonStringOf(SimpleDateFormat("yyyy-MM-dd HH:mm:ss ZZ", Locale.SIMPLIFIED_CHINESE).format(nowTime)))
|
||||
}
|
||||
|
||||
lint {
|
||||
|
@ -47,18 +57,8 @@ android {
|
|||
}
|
||||
}
|
||||
|
||||
kotlin {
|
||||
sourceSets.debug {
|
||||
kotlin.srcDir("build/generated/ksp/debug/kotlin")
|
||||
}
|
||||
sourceSets.release {
|
||||
kotlin.srcDir("build/generated/ksp/release/kotlin")
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
manifestPlaceholders += mapOf()
|
||||
isMinifyEnabled = false
|
||||
setProguardFiles(
|
||||
listOf(
|
||||
|
@ -67,31 +67,31 @@ android {
|
|||
)
|
||||
)
|
||||
signingConfig = signingConfigs.getByName("release")
|
||||
manifestPlaceholders["appName"] = "搞快点"
|
||||
manifestPlaceholders["appName"] = "GKD"
|
||||
}
|
||||
debug {
|
||||
applicationIdSuffix = ".debug"
|
||||
signingConfig = signingConfigs.getByName("release")
|
||||
manifestPlaceholders["appName"] = "搞快点-dev"
|
||||
manifestPlaceholders["appName"] = "GKD-debug"
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
freeCompilerArgs = freeCompilerArgs + "-opt-in=kotlin.RequiresOptIn"
|
||||
jvmTarget = JavaVersion.VERSION_17.majorVersion
|
||||
freeCompilerArgs += "-opt-in=kotlin.RequiresOptIn"
|
||||
}
|
||||
buildFeatures {
|
||||
buildConfig = true
|
||||
compose = true
|
||||
}
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion = libs.versions.compose.compiler.get()
|
||||
kotlinCompilerExtensionVersion = libs.versions.compose.compilerVersion.get()
|
||||
}
|
||||
packagingOptions {
|
||||
packaging {
|
||||
resources {
|
||||
// Due to https://github.com/Kotlin/kotlinx.coroutines/issues/2023
|
||||
excludes += "META-INF/INDEX.LIST"
|
||||
|
@ -106,16 +106,20 @@ android {
|
|||
exclude("org.jetbrains.kotlinx", "kotlinx-coroutines-debug")
|
||||
}
|
||||
}
|
||||
|
||||
// ksp
|
||||
sourceSets.configureEach {
|
||||
kotlin.srcDir("$buildDir/generated/ksp/$name/kotlin/")
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(project(mapOf("path" to ":selector_core")))
|
||||
implementation(project(mapOf("path" to ":router")))
|
||||
|
||||
dependencies {
|
||||
|
||||
implementation(project(mapOf("path" to ":selector")))
|
||||
implementation(libs.androidx.appcompat)
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||
implementation(libs.androidx.localbroadcastmanager)
|
||||
|
||||
implementation(libs.compose.ui)
|
||||
implementation(libs.compose.material)
|
||||
|
@ -128,15 +132,18 @@ dependencies {
|
|||
androidTestImplementation(libs.androidx.junit)
|
||||
androidTestImplementation(libs.androidx.espresso)
|
||||
|
||||
|
||||
compileOnly(project(mapOf("path" to ":hidden_api")))
|
||||
implementation(libs.rikka.shizuku.api)
|
||||
implementation(libs.rikka.shizuku.provider)
|
||||
implementation(libs.lsposed.hiddenapibypass)
|
||||
|
||||
implementation(libs.tencent.bugly)
|
||||
implementation(libs.tencent.mmkv)
|
||||
|
||||
implementation(libs.androidx.room.runtime)
|
||||
kapt(libs.androidx.room.compiler)
|
||||
implementation(libs.androidx.room.ktx)
|
||||
ksp(libs.androidx.room.compiler)
|
||||
|
||||
implementation(libs.ktor.server.core)
|
||||
implementation(libs.ktor.server.netty)
|
||||
|
@ -144,12 +151,13 @@ dependencies {
|
|||
implementation(libs.ktor.server.content.negotiation)
|
||||
|
||||
implementation(libs.ktor.client.core)
|
||||
implementation(libs.ktor.client.cio)
|
||||
implementation(libs.ktor.client.android)
|
||||
implementation(libs.ktor.client.content.negotiation)
|
||||
implementation(libs.ktor.serialization.kotlinx.json)
|
||||
|
||||
implementation(libs.google.accompanist.drawablepainter)
|
||||
implementation(libs.google.accompanist.placeholder.material)
|
||||
implementation(libs.google.accompanist.systemuicontroller)
|
||||
|
||||
implementation(libs.kotlinx.serialization.json)
|
||||
implementation(libs.kotlinx.collections.immutable)
|
||||
|
@ -160,4 +168,9 @@ dependencies {
|
|||
implementation(libs.others.zxing.android.embedded)
|
||||
implementation(libs.others.floating.bubble.view)
|
||||
|
||||
implementation(libs.destinations.core)
|
||||
implementation(libs.destinations.animations)
|
||||
ksp(libs.destinations.ksp)
|
||||
|
||||
|
||||
}
|
|
@ -2,7 +2,7 @@
|
|||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 1,
|
||||
"identityHash": "2083d8585fffd897fde3733958e356f8",
|
||||
"identityHash": "f3feda76127233f3416d7570fca1615f",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "subs_item",
|
||||
|
@ -149,12 +149,116 @@
|
|||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "snapshot",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `app_id` TEXT, `activity_id` TEXT, `app_name` TEXT, `app_version_code` INTEGER, `app_version_name` TEXT, `screen_height` INTEGER NOT NULL, `screen_width` INTEGER NOT NULL, `is_landscape` INTEGER NOT NULL, `device` TEXT NOT NULL, `model` TEXT NOT NULL, `manufacturer` TEXT NOT NULL, `brand` TEXT NOT NULL, `sdk_int` INTEGER NOT NULL, `release` TEXT NOT NULL, PRIMARY KEY(`id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "appId",
|
||||
"columnName": "app_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "activityId",
|
||||
"columnName": "activity_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "appName",
|
||||
"columnName": "app_name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "appVersionCode",
|
||||
"columnName": "app_version_code",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "appVersionName",
|
||||
"columnName": "app_version_name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "screenHeight",
|
||||
"columnName": "screen_height",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "screenWidth",
|
||||
"columnName": "screen_width",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isLandscape",
|
||||
"columnName": "is_landscape",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "device",
|
||||
"columnName": "device",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "model",
|
||||
"columnName": "model",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "manufacturer",
|
||||
"columnName": "manufacturer",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "brand",
|
||||
"columnName": "brand",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "sdkInt",
|
||||
"columnName": "sdk_int",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "release",
|
||||
"columnName": "release",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
}
|
||||
],
|
||||
"views": [],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '2083d8585fffd897fde3733958e356f8')"
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'f3feda76127233f3416d7570fca1615f')"
|
||||
]
|
||||
}
|
||||
}
|
124
app/schemas/li.songe.gkd.db.SnapshotDb/1.json
Normal file
124
app/schemas/li.songe.gkd.db.SnapshotDb/1.json
Normal file
|
@ -0,0 +1,124 @@
|
|||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 1,
|
||||
"identityHash": "296a7b78252c48246f24767e66441c22",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "snapshot",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `app_id` TEXT, `activity_id` TEXT, `app_name` TEXT, `app_version_code` INTEGER, `app_version_name` TEXT, `screen_height` INTEGER NOT NULL, `screen_width` INTEGER NOT NULL, `is_landscape` INTEGER NOT NULL, `device` TEXT NOT NULL, `model` TEXT NOT NULL, `manufacturer` TEXT NOT NULL, `brand` TEXT NOT NULL, `sdk_int` INTEGER NOT NULL, `release` TEXT NOT NULL, `_1` TEXT NOT NULL, PRIMARY KEY(`id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "appId",
|
||||
"columnName": "app_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "activityId",
|
||||
"columnName": "activity_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "appName",
|
||||
"columnName": "app_name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "appVersionCode",
|
||||
"columnName": "app_version_code",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "appVersionName",
|
||||
"columnName": "app_version_name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "screenHeight",
|
||||
"columnName": "screen_height",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "screenWidth",
|
||||
"columnName": "screen_width",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isLandscape",
|
||||
"columnName": "is_landscape",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "device",
|
||||
"columnName": "device",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "model",
|
||||
"columnName": "model",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "manufacturer",
|
||||
"columnName": "manufacturer",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "brand",
|
||||
"columnName": "brand",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "sdkInt",
|
||||
"columnName": "sdk_int",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "release",
|
||||
"columnName": "release",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "nodes",
|
||||
"columnName": "_1",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
}
|
||||
],
|
||||
"views": [],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '296a7b78252c48246f24767e66441c22')"
|
||||
]
|
||||
}
|
||||
}
|
70
app/schemas/li.songe.gkd.db.SubsConfigDb/1.json
Normal file
70
app/schemas/li.songe.gkd.db.SubsConfigDb/1.json
Normal file
|
@ -0,0 +1,70 @@
|
|||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 1,
|
||||
"identityHash": "5ad1f90d8f2852410fde46463bf24322",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "subs_config",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `mtime` INTEGER NOT NULL, `type` INTEGER NOT NULL, `enable` INTEGER NOT NULL, `subs_item_id` INTEGER NOT NULL, `app_id` TEXT NOT NULL, `group_key` INTEGER NOT NULL, PRIMARY KEY(`id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "mtime",
|
||||
"columnName": "mtime",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "type",
|
||||
"columnName": "type",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "enable",
|
||||
"columnName": "enable",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "subsItemId",
|
||||
"columnName": "subs_item_id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "appId",
|
||||
"columnName": "app_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "groupKey",
|
||||
"columnName": "group_key",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
}
|
||||
],
|
||||
"views": [],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '5ad1f90d8f2852410fde46463bf24322')"
|
||||
]
|
||||
}
|
||||
}
|
88
app/schemas/li.songe.gkd.db.SubsItemDb/1.json
Normal file
88
app/schemas/li.songe.gkd.db.SubsItemDb/1.json
Normal file
|
@ -0,0 +1,88 @@
|
|||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 1,
|
||||
"identityHash": "b51332e64931ac0cef5774cb5df5b703",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "subs_item",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `mtime` INTEGER NOT NULL, `enable` INTEGER NOT NULL, `enable_update` INTEGER NOT NULL, `order` INTEGER NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `version` INTEGER NOT NULL, `update_url` TEXT NOT NULL, `support_url` TEXT NOT NULL, PRIMARY KEY(`id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "mtime",
|
||||
"columnName": "mtime",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "enable",
|
||||
"columnName": "enable",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "enableUpdate",
|
||||
"columnName": "enable_update",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "order",
|
||||
"columnName": "order",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "author",
|
||||
"columnName": "author",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "version",
|
||||
"columnName": "version",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "updateUrl",
|
||||
"columnName": "update_url",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "supportUrl",
|
||||
"columnName": "support_url",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
}
|
||||
],
|
||||
"views": [],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'b51332e64931ac0cef5774cb5df5b703')"
|
||||
]
|
||||
}
|
||||
}
|
34
app/schemas/li.songe.gkd.db.TriggerLogDb/1.json
Normal file
34
app/schemas/li.songe.gkd.db.TriggerLogDb/1.json
Normal file
|
@ -0,0 +1,34 @@
|
|||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 1,
|
||||
"identityHash": "e565cbca157f8ba6cecb6e7cd7cc6304",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "trigger_log",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, PRIMARY KEY(`id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
}
|
||||
],
|
||||
"views": [],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'e565cbca157f8ba6cecb6e7cd7cc6304')"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -17,16 +17,21 @@
|
|||
|
||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
|
||||
|
||||
|
||||
<!--
|
||||
APP 有两个进程, 主进程 + :remote 进程
|
||||
主进程: 主要是 activity 的前端界面
|
||||
remote进程: 主要是 service
|
||||
优点: 在最近任务界面删除当前APP的窗口记录时,不会让 remote进程里的 service 停止
|
||||
-->
|
||||
|
||||
<application
|
||||
android:name="li.songe.gkd.App"
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:icon="@drawable/ic_launcher"
|
||||
android:label="${appName}"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:roundIcon="@drawable/ic_launcher_round"
|
||||
android:supportsRtl="false"
|
||||
android:theme="@style/Theme.Gkd.NoActionBar">
|
||||
android:theme="@style/AppTheme">
|
||||
|
||||
<activity
|
||||
android:name="com.journeyapps.barcodescanner.CaptureActivity"
|
||||
|
@ -36,8 +41,7 @@
|
|||
<activity
|
||||
android:name="li.songe.gkd.MainActivity"
|
||||
android:configChanges="uiMode|screenSize|orientation|keyboardHidden|touchscreen|smallestScreenSize|screenLayout|navigation|mnc|mcc|locale|layoutDirection|keyboard|fontWeightAdjustment|fontScale|density|colorMode"
|
||||
android:exported="true"
|
||||
android:launchMode="singleInstance">
|
||||
android:exported="true">
|
||||
|
||||
<!--
|
||||
about android:configChanges
|
||||
|
@ -63,29 +67,39 @@
|
|||
<service
|
||||
android:name=".accessibility.GkdAbService"
|
||||
android:exported="false"
|
||||
android:label="@string/accessibility_service_label"
|
||||
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
|
||||
android:label="@string/ab_label"
|
||||
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"
|
||||
android:process=":remote">
|
||||
<intent-filter>
|
||||
<action android:name="android.accessibilityservice.AccessibilityService" />
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="android.accessibilityservice"
|
||||
android:resource="@xml/accessibility_service_description" />
|
||||
android:resource="@xml/ab_desc" />
|
||||
</service>
|
||||
<service
|
||||
android:name=".debug.ScreenshotService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="mediaProjection" />
|
||||
android:foregroundServiceType="mediaProjection"
|
||||
android:process=":remote" />
|
||||
<service
|
||||
android:name=".debug.HttpService"
|
||||
android:exported="false" />
|
||||
android:exported="false"
|
||||
android:process=":remote" />
|
||||
<service
|
||||
android:name=".debug.FloatingService"
|
||||
android:exported="false" />
|
||||
android:exported="false"
|
||||
android:process=":remote" />
|
||||
<service
|
||||
android:name=".accessibility.KeepAliveService"
|
||||
android:exported="false" /> <!-- This provider is required by Shizuku, remove this if your app only supports Sui -->
|
||||
android:exported="false"
|
||||
android:process=":remote" />
|
||||
<service
|
||||
android:name=".accessibility.ShizukuService"
|
||||
android:exported="false"
|
||||
android:process=":remote" />
|
||||
|
||||
<provider
|
||||
android:name="rikka.shizuku.ShizukuProvider"
|
||||
android:authorities="${applicationId}.shizuku"
|
||||
|
@ -93,6 +107,17 @@
|
|||
android:exported="true"
|
||||
android:multiprocess="false"
|
||||
android:permission="android.permission.INTERACT_ACROSS_USERS_FULL" />
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.provider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_paths" />
|
||||
</provider>
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
|
@ -1,16 +1,29 @@
|
|||
package li.songe.gkd
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import com.blankj.utilcode.util.LogUtils
|
||||
import com.tencent.bugly.crashreport.CrashReport
|
||||
import com.tencent.mmkv.MMKV
|
||||
import li.songe.gkd.util.Storage
|
||||
import li.songe.gkd.utils.Storage
|
||||
import org.lsposed.hiddenapibypass.HiddenApiBypass
|
||||
import rikka.shizuku.ShizukuProvider
|
||||
|
||||
class App : Application() {
|
||||
companion object {
|
||||
lateinit var context: Application
|
||||
}
|
||||
|
||||
override fun attachBaseContext(base: Context?) {
|
||||
super.attachBaseContext(base)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
HiddenApiBypass.addHiddenApiExemptions("L")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
context = this
|
||||
|
@ -24,6 +37,7 @@ class App : Application() {
|
|||
saveDays = 30
|
||||
LogUtils.getConfig().setConsoleSwitch(Storage.settings.enableConsoleLogOut)
|
||||
}
|
||||
ShizukuProvider.enableMultiProcessSupport(true)
|
||||
CrashReport.initCrashReport(applicationContext, "d0ce46b353", false)
|
||||
}
|
||||
}
|
|
@ -1,18 +1,24 @@
|
|||
package li.songe.gkd
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import android.os.Build
|
||||
import android.view.WindowManager
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.compose.material.icons.materialIcon
|
||||
import androidx.compose.material.icons.materialPath
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import com.blankj.utilcode.util.LogUtils
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat
|
||||
import com.dylanc.activityresult.launcher.StartActivityLauncher
|
||||
import com.ramcosta.composedestinations.DestinationsNavHost
|
||||
import li.songe.gkd.composition.CompositionActivity
|
||||
import li.songe.gkd.composition.CompositionExt.useLifeCycleLog
|
||||
import li.songe.gkd.ui.home.HomePage
|
||||
import li.songe.gkd.ui.theme.MainTheme
|
||||
import li.songe.gkd.util.Ext.LocalLauncher
|
||||
import li.songe.gkd.util.Storage
|
||||
import li.songe.gkd.util.UseHook
|
||||
import li.songe.router.RouterHost
|
||||
import li.songe.gkd.ui.NavGraphs
|
||||
import li.songe.gkd.ui.theme.AppTheme
|
||||
import li.songe.gkd.utils.LocalLauncher
|
||||
import li.songe.gkd.utils.LocalNavController
|
||||
import li.songe.gkd.utils.StackCacheProvider
|
||||
import li.songe.gkd.utils.Storage
|
||||
|
||||
|
||||
class MainActivity : CompositionActivity({
|
||||
|
@ -20,30 +26,63 @@ class MainActivity : CompositionActivity({
|
|||
|
||||
val launcher = StartActivityLauncher(this)
|
||||
onFinish { fs ->
|
||||
LogUtils.d(Storage.settings)
|
||||
if (Storage.settings.excludeFromRecents) {
|
||||
finishAndRemoveTask() // 会让miui桌面回退动画失效
|
||||
} else {
|
||||
fs()
|
||||
}
|
||||
}
|
||||
onConfigurationChanged { newConfig ->
|
||||
LogUtils.d(newConfig)
|
||||
UseHook.update(newConfig)
|
||||
|
||||
// https://juejin.cn/post/7169147194400833572
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
window.attributes.layoutInDisplayCutoutMode =
|
||||
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
|
||||
}
|
||||
// TextView[a==1||b==1||a==1||(a==1&&b==true)]
|
||||
// lifecycleScope.launchTry {
|
||||
// delay(1000)
|
||||
// WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
// val insetsController = WindowCompat.getInsetsController(window, window.decorView)
|
||||
// insetsController.hide(WindowInsetsCompat.Type.statusBars())
|
||||
// }
|
||||
|
||||
// var shizukuIsOK = false
|
||||
// val receivedListener: () -> Unit = {
|
||||
// shizukuIsOK = true
|
||||
// }
|
||||
// Shizuku.addBinderReceivedListenerSticky(receivedListener)
|
||||
// onDestroy {
|
||||
// Shizuku.removeBinderReceivedListener(receivedListener)
|
||||
// }
|
||||
// lifecycleScope.launchWhile {
|
||||
// if (shizukuIsOK) {
|
||||
// val top = activityTaskManager.getTasks(1, false, true)?.firstOrNull()
|
||||
// if (top!=null) {
|
||||
// LogUtils.d(top.topActivity?.packageName, top.topActivity?.className, top.topActivity?.shortClassName)
|
||||
// }
|
||||
// }
|
||||
// delay(5000)
|
||||
// }
|
||||
|
||||
|
||||
setContent {
|
||||
BackHandler {
|
||||
finish()
|
||||
}
|
||||
CompositionLocalProvider(LocalLauncher provides launcher) {
|
||||
MainTheme(false) {
|
||||
RouterHost(HomePage)
|
||||
val navController = rememberNavController()
|
||||
AppTheme(false) {
|
||||
CompositionLocalProvider(
|
||||
LocalLauncher provides launcher,
|
||||
LocalNavController provides navController
|
||||
) {
|
||||
StackCacheProvider(navController = navController) {
|
||||
DestinationsNavHost(
|
||||
navGraph = NavGraphs.root,
|
||||
navController = navController,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
128
app/src/main/java/li/songe/gkd/accessibility/AbExt.kt
Normal file
128
app/src/main/java/li/songe/gkd/accessibility/AbExt.kt
Normal file
|
@ -0,0 +1,128 @@
|
|||
package li.songe.gkd.accessibility
|
||||
|
||||
import android.accessibilityservice.AccessibilityService
|
||||
import android.accessibilityservice.GestureDescription
|
||||
import android.graphics.Path
|
||||
import android.graphics.Rect
|
||||
import android.view.accessibility.AccessibilityNodeInfo
|
||||
import li.songe.selector.Transform
|
||||
import li.songe.selector.Selector
|
||||
|
||||
fun AccessibilityNodeInfo.getIndex(): Int {
|
||||
parent?.forEachIndexed { index, accessibilityNodeInfo ->
|
||||
if (accessibilityNodeInfo == this) {
|
||||
return index
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
inline fun AccessibilityNodeInfo.forEachIndexed(action: (index: Int, childNode: AccessibilityNodeInfo?) -> Unit) {
|
||||
var index = 0
|
||||
val childCount = this.childCount
|
||||
while (index < childCount) {
|
||||
val child: AccessibilityNodeInfo? = getChild(index)
|
||||
action(index, child)
|
||||
index += 1
|
||||
}
|
||||
}
|
||||
|
||||
fun AccessibilityNodeInfo.click(service: AccessibilityService) = when {
|
||||
this.isClickable -> {
|
||||
this.performAction(AccessibilityNodeInfo.ACTION_CLICK)
|
||||
"self"
|
||||
}
|
||||
|
||||
else -> {
|
||||
val react = Rect()
|
||||
this.getBoundsInScreen(react)
|
||||
val x = react.left + 50f / 100f * (react.right - react.left)
|
||||
val y = react.top + 50f / 100f * (react.bottom - react.top)
|
||||
if (x >= 0 && y >= 0) {
|
||||
val gestureDescription = GestureDescription.Builder()
|
||||
val path = Path()
|
||||
path.moveTo(x, y)
|
||||
gestureDescription.addStroke(GestureDescription.StrokeDescription(path, 0, 300))
|
||||
service.dispatchGesture(gestureDescription.build(), null, null)
|
||||
"(50%, 50%)"
|
||||
} else {
|
||||
"($x, $y) no click"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun AccessibilityNodeInfo.getDepth(): Int {
|
||||
var p: AccessibilityNodeInfo? = this
|
||||
var depth = 0
|
||||
while (true) {
|
||||
val p2 = p?.parent
|
||||
if (p2 != null) {
|
||||
p = p2
|
||||
depth++
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
return depth
|
||||
}
|
||||
|
||||
|
||||
fun AccessibilityNodeInfo.querySelector(selector: Selector) =
|
||||
abTransform.querySelector(this, selector)
|
||||
|
||||
fun AccessibilityNodeInfo.querySelectorAll(selector: Selector) =
|
||||
abTransform.querySelectorAll(this, selector)
|
||||
|
||||
// 不可以在 多线程/不同协程作用域 里同时使用
|
||||
private val tempRect = Rect()
|
||||
private fun AccessibilityNodeInfo.getTempRect(): Rect {
|
||||
getBoundsInScreen(tempRect)
|
||||
return tempRect
|
||||
}
|
||||
|
||||
val abTransform = Transform<AccessibilityNodeInfo>(
|
||||
getAttr = { node, name ->
|
||||
when (name) {
|
||||
"id" -> node.viewIdResourceName
|
||||
"name" -> node.className
|
||||
"text" -> node.text
|
||||
"textLen" -> node.text?.length
|
||||
"desc" -> node.contentDescription
|
||||
"descLen" -> node.contentDescription?.length
|
||||
"childCount" -> node.childCount
|
||||
|
||||
"isEnabled" -> node.isEnabled
|
||||
"isClickable" -> node.isClickable
|
||||
"isChecked" -> node.isChecked
|
||||
"isCheckable" -> node.isCheckable
|
||||
"isFocused" -> node.isFocused
|
||||
"isFocusable" -> node.isFocusable
|
||||
"isVisibleToUser" -> node.isVisibleToUser
|
||||
|
||||
"left" -> node.getTempRect().left
|
||||
"top" -> node.getTempRect().top
|
||||
"right" -> node.getTempRect().right
|
||||
"bottom" -> node.getTempRect().bottom
|
||||
|
||||
"width" -> node.getTempRect().width()
|
||||
"height" -> node.getTempRect().height()
|
||||
|
||||
"index" -> node.getIndex()
|
||||
"depth" -> node.getDepth()
|
||||
else -> null
|
||||
}
|
||||
},
|
||||
getName = { node -> node.className },
|
||||
getChildren = { node ->
|
||||
sequence {
|
||||
repeat(node.childCount) { i ->
|
||||
yield(node.getChild(i))
|
||||
}
|
||||
}
|
||||
},
|
||||
getChild = { node, index -> node.getChild(index) },
|
||||
getParent = { node -> node.parent }
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -1,31 +1,40 @@
|
|||
package li.songe.gkd.accessibility
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.os.Build
|
||||
import android.view.Display
|
||||
import android.view.accessibility.AccessibilityEvent
|
||||
import android.view.accessibility.AccessibilityNodeInfo
|
||||
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 com.blankj.utilcode.util.ToastUtils
|
||||
import io.ktor.client.request.get
|
||||
import io.ktor.client.statement.bodyAsText
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.withContext
|
||||
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.Rule
|
||||
import li.songe.gkd.data.RuleManager
|
||||
import li.songe.gkd.data.SubscriptionRaw
|
||||
import li.songe.gkd.db.table.SubsItem
|
||||
import li.songe.gkd.db.util.RoomX
|
||||
import li.songe.gkd.debug.NodeSnapshot
|
||||
import li.songe.gkd.selector.click
|
||||
import li.songe.gkd.selector.querySelectorAll
|
||||
import li.songe.gkd.util.Ext.buildRuleManager
|
||||
import li.songe.gkd.util.Ext.getActivityIdByShizuku
|
||||
import li.songe.gkd.util.Ext.getSubsFileLastModified
|
||||
import li.songe.gkd.util.Ext.launchWhile
|
||||
import li.songe.gkd.util.Singleton
|
||||
import li.songe.gkd.util.Storage
|
||||
import li.songe.selector_core.Selector
|
||||
import java.io.File
|
||||
import li.songe.gkd.db.DbSet
|
||||
import li.songe.gkd.debug.SnapshotExt
|
||||
import li.songe.gkd.shizuku.activityTaskManager
|
||||
import li.songe.gkd.shizuku.shizukuIsSafeOK
|
||||
import li.songe.gkd.utils.Singleton
|
||||
import li.songe.gkd.utils.Storage
|
||||
import li.songe.gkd.utils.launchTry
|
||||
import li.songe.gkd.utils.launchWhile
|
||||
import li.songe.gkd.utils.launchWhileTry
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
class GkdAbService : CompositionAbService({
|
||||
useLifeCycleLog()
|
||||
|
@ -35,7 +44,11 @@ class GkdAbService : CompositionAbService({
|
|||
val scope = useScope()
|
||||
|
||||
service = context
|
||||
onDestroy { service = null }
|
||||
onDestroy {
|
||||
service = null
|
||||
currentAppId = null
|
||||
currentActivityId = null
|
||||
}
|
||||
|
||||
KeepAliveService.start(context)
|
||||
onDestroy {
|
||||
|
@ -46,100 +59,98 @@ class GkdAbService : CompositionAbService({
|
|||
onServiceConnected { serviceConnected = true }
|
||||
onInterrupt { serviceConnected = false }
|
||||
|
||||
onAccessibilityEvent { event ->
|
||||
val activityId = event?.className?.toString() ?: return@onAccessibilityEvent
|
||||
val rootAppId = rootInActiveWindow?.packageName?.toString() ?: return@onAccessibilityEvent
|
||||
when (event.eventType) {
|
||||
onAccessibilityEvent { event -> // 根据事件获取 activityId, 概率不准确
|
||||
when (event?.eventType) {
|
||||
AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED, AccessibilityEvent.TYPE_WINDOWS_CHANGED -> {
|
||||
// 在桌面和应用之间来回切换, 大概率导致识别失败
|
||||
if (!activityId.startsWith("android.") &&
|
||||
!activityId.startsWith("androidx.") &&
|
||||
!activityId.startsWith("com.android.")
|
||||
val activityId = event.className?.toString() ?: return@onAccessibilityEvent
|
||||
if (activityId == "com.miui.home.launcher.Launcher") { // 小米桌面 bug
|
||||
val appId =
|
||||
rootInActiveWindow?.packageName?.toString() ?: return@onAccessibilityEvent
|
||||
if (appId != "com.miui.home") {
|
||||
return@onAccessibilityEvent
|
||||
}
|
||||
}
|
||||
|
||||
if (activityId.startsWith("android.") ||
|
||||
activityId.startsWith("androidx.") ||
|
||||
activityId.startsWith("com.android.")
|
||||
) {
|
||||
if ((activityId == "com.miui.home.launcher.Launcher" && rootAppId != "com.miui.home")) {
|
||||
// 小米手机 上滑手势, 导致 活动名 不属于包名
|
||||
// 另外 微信扫码登录第三方网站 也会导致失败
|
||||
} else {
|
||||
if (activityId != nodeSnapshot.activityId) {
|
||||
nodeSnapshot = nodeSnapshot.copy(
|
||||
activityId = activityId
|
||||
)
|
||||
}
|
||||
}
|
||||
return@onAccessibilityEvent
|
||||
}
|
||||
currentActivityId = activityId
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
scope.launchWhile {
|
||||
delay(300)
|
||||
val activityId = getActivityIdByShizuku() ?: return@launchWhile
|
||||
if (activityId != nodeSnapshot.activityId) {
|
||||
nodeSnapshot = nodeSnapshot.copy(
|
||||
activityId = activityId
|
||||
)
|
||||
onAccessibilityEvent { event -> // 小米手机监听截屏保存快照
|
||||
if (!Storage.settings.enableCaptureSystemScreenshot) return@onAccessibilityEvent
|
||||
if (event?.packageName == null || event.className == null) return@onAccessibilityEvent
|
||||
if (event.eventType == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED && event.contentChangeTypes == AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED && event.packageName.contentEquals(
|
||||
"com.miui.screenshot"
|
||||
) && event.className!!.startsWith("android.") // android.widget.RelativeLayout
|
||||
) {
|
||||
scope.launchTry {
|
||||
val snapshot = SnapshotExt.captureSnapshot()
|
||||
ToastUtils.showShort("保存快照成功")
|
||||
LogUtils.d("截屏:保存快照", snapshot.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var subsFileLastModified = 0L
|
||||
scope.launchWhile { // 根据本地文件最新写入时间 决定 是否 更新数据
|
||||
val t = getSubsFileLastModified()
|
||||
if (t > subsFileLastModified) {
|
||||
subsFileLastModified = t
|
||||
ruleManager = buildRuleManager()
|
||||
LogUtils.d("读取本地规则")
|
||||
}
|
||||
delay(10_000)
|
||||
}
|
||||
|
||||
scope.launchWhile {
|
||||
delay(50)
|
||||
scope.launchWhile { // 屏幕无障碍信息轮询
|
||||
delay(200)
|
||||
if (!serviceConnected) return@launchWhile
|
||||
if (!Storage.settings.enableService || ScreenUtils.isScreenLock()) return@launchWhile
|
||||
|
||||
nodeSnapshot = nodeSnapshot.copy(
|
||||
root = rootInActiveWindow,
|
||||
)
|
||||
val shot = nodeSnapshot
|
||||
if (shot.root == null) return@launchWhile
|
||||
for (rule in ruleManager.match(shot.appId, shot.activityId)) {
|
||||
val target = rule.query(shot.root) ?: continue
|
||||
currentAppId = rootInActiveWindow?.packageName?.toString()
|
||||
var tempRules = rules
|
||||
var i = 0
|
||||
while (i < tempRules.size) {
|
||||
val rule = tempRules[i]
|
||||
i++
|
||||
if (!ruleManager.ruleIsAvailable(rule)) continue
|
||||
val frozenNode = rootInActiveWindow
|
||||
val target = rule.query(frozenNode)
|
||||
if (target != null) {
|
||||
val clickResult = target.click(context)
|
||||
ruleManager.trigger(rule)
|
||||
LogUtils.d(
|
||||
*rule.matches.toTypedArray(),
|
||||
NodeSnapshot.abNodeToNode(target),
|
||||
clickResult
|
||||
*rule.matches.toTypedArray(), NodeInfo.abNodeToNode(target), clickResult
|
||||
)
|
||||
}
|
||||
delay(150)
|
||||
delay(50)
|
||||
currentAppId = rootInActiveWindow?.packageName?.toString()
|
||||
if (tempRules != rules) {
|
||||
tempRules = rules
|
||||
i = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
scope.launchWhile {
|
||||
scope.launchWhile { // 自动从网络更新订阅文件
|
||||
delay(5000)
|
||||
RoomX.select<SubsItem>().map { subsItem ->
|
||||
if (!NetworkUtils.isAvailable()) return@map
|
||||
if (!NetworkUtils.isAvailable()) return@launchWhile
|
||||
DbSet.subsItemDao.query().first().forEach { subsItem ->
|
||||
try {
|
||||
val text = Singleton.client.get(subsItem.updateUrl).bodyAsText()
|
||||
val subscriptionRaw = SubscriptionRaw.parse5(text)
|
||||
if (subscriptionRaw.version <= subsItem.version) {
|
||||
return@map
|
||||
return@forEach
|
||||
}
|
||||
val newItem = subsItem.copy(
|
||||
updateUrl = subscriptionRaw.updateUrl
|
||||
?: subsItem.updateUrl,
|
||||
updateUrl = subscriptionRaw.updateUrl ?: subsItem.updateUrl,
|
||||
name = subscriptionRaw.name,
|
||||
mtime = System.currentTimeMillis()
|
||||
)
|
||||
RoomX.update(newItem)
|
||||
File(newItem.filePath).writeText(
|
||||
newItem.subsFile.writeText(
|
||||
SubscriptionRaw.stringify(
|
||||
subscriptionRaw
|
||||
)
|
||||
)
|
||||
LogUtils.d("更新订阅文件:${subsItem.name}")
|
||||
DbSet.subsItemDao.update(newItem)
|
||||
LogUtils.d("更新磁盘订阅文件:${subsItem.name}")
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
@ -147,29 +158,96 @@ class GkdAbService : CompositionAbService({
|
|||
delay(30 * 60_000)
|
||||
}
|
||||
|
||||
}) {
|
||||
private var nodeSnapshot = NodeSnapshot()
|
||||
set(value) {
|
||||
if (field.appId != value.appId || field.activityId != value.activityId) {
|
||||
LogUtils.d(
|
||||
value.appId,
|
||||
value.activityId,
|
||||
*ruleManager.match(value.appId, value.activityId).toList().toTypedArray()
|
||||
)
|
||||
scope.launchTry {
|
||||
DbSet.subsItemDao.query().flowOn(IO).collect {
|
||||
val subscriptionRawArray = withContext(IO) {
|
||||
it.filter { s -> s.enable }
|
||||
.mapNotNull { s -> s.subscriptionRaw }
|
||||
}
|
||||
ruleManager = RuleManager(*subscriptionRawArray.toTypedArray())
|
||||
}
|
||||
field = value
|
||||
}
|
||||
|
||||
private var ruleManager = RuleManager()
|
||||
scope.launchWhileTry(interval = 400) {
|
||||
if (shizukuIsSafeOK()) {
|
||||
val topActivity =
|
||||
activityTaskManager.getTasks(1, false, true)?.firstOrNull()?.topActivity
|
||||
if (topActivity != null) {
|
||||
currentAppId = topActivity.packageName
|
||||
currentActivityId = topActivity.className
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}) {
|
||||
|
||||
companion object {
|
||||
private var service: GkdAbService? = null
|
||||
fun isRunning() = ServiceUtils.isServiceRunning(GkdAbService::class.java)
|
||||
fun currentNodeSnapshot() = service?.nodeSnapshot
|
||||
fun match(selector: String) {
|
||||
val rootAbNode = service?.rootInActiveWindow ?: return
|
||||
val list = rootAbNode.querySelectorAll(Selector.parse(selector)).map { it.value }.toList()
|
||||
|
||||
private var ruleManager = RuleManager()
|
||||
set(value) {
|
||||
field = value
|
||||
rules = value.match(currentAppId, currentActivityId).toList()
|
||||
}
|
||||
private var rules = listOf<Rule>()
|
||||
set(value) {
|
||||
field = value
|
||||
LogUtils.d(
|
||||
"currentAppId: $currentAppId",
|
||||
"currentActivityId: $currentActivityId",
|
||||
*value.toTypedArray()
|
||||
)
|
||||
}
|
||||
|
||||
private var service: GkdAbService? = null
|
||||
var currentActivityId: String? = null
|
||||
set(value) {
|
||||
val oldValue = field
|
||||
field = value
|
||||
if (value != oldValue) {
|
||||
rules = ruleManager.match(currentAppId, value).toList()
|
||||
}
|
||||
}
|
||||
private var currentAppId: String? = null
|
||||
set(value) {
|
||||
val oldValue = field
|
||||
field = value
|
||||
if (value != oldValue) {
|
||||
rules = ruleManager.match(value, currentActivityId).toList()
|
||||
}
|
||||
}
|
||||
val currentAbNode: AccessibilityNodeInfo?
|
||||
get() {
|
||||
return service?.rootInActiveWindow
|
||||
}
|
||||
|
||||
suspend fun currentScreenshot() = service?.run {
|
||||
suspendCoroutine {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
takeScreenshot(Display.DEFAULT_DISPLAY,
|
||||
application.mainExecutor,
|
||||
object : TakeScreenshotCallback {
|
||||
override fun onSuccess(screenshot: ScreenshotResult) {
|
||||
it.resume(
|
||||
Bitmap.wrapHardwareBuffer(
|
||||
screenshot.hardwareBuffer, screenshot.colorSpace
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun onFailure(errorCode: Int) = it.resume(null)
|
||||
})
|
||||
} else {
|
||||
it.resume(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// fun match(selector: String) {
|
||||
// val rootAbNode = service?.rootInActiveWindow ?: return
|
||||
// val list =
|
||||
// rootAbNode.querySelectorAll(Selector.parse(selector)).map { it.value }.toList()
|
||||
// }
|
||||
|
||||
}
|
||||
}
|
|
@ -6,9 +6,8 @@ import kotlinx.coroutines.delay
|
|||
import li.songe.gkd.App
|
||||
import li.songe.gkd.composition.CompositionService
|
||||
import li.songe.gkd.composition.CompositionExt.useScope
|
||||
import li.songe.gkd.util.Ext.createNotificationChannel
|
||||
import li.songe.gkd.util.Ext.launchWhile
|
||||
|
||||
import li.songe.gkd.utils.launchWhile
|
||||
import li.songe.gkd.utils.Ext.createNotificationChannel
|
||||
|
||||
class KeepAliveService : CompositionService({
|
||||
createNotificationChannel(this)
|
||||
|
|
|
@ -1,10 +0,0 @@
|
|||
package li.songe.gkd.accessibility
|
||||
|
||||
import android.view.accessibility.AccessibilityNodeInfo
|
||||
|
||||
data class NodeSnapshot(
|
||||
val root: AccessibilityNodeInfo? = null,
|
||||
val activityId: String? = null,
|
||||
) {
|
||||
val appId by lazy { root?.packageName?.toString() }
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
package li.songe.gkd.accessibility
|
||||
|
||||
import li.songe.gkd.composition.CompositionService
|
||||
|
||||
class ShizukuService: CompositionService({
|
||||
|
||||
})
|
|
@ -6,14 +6,13 @@ import android.content.BroadcastReceiver
|
|||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||
import com.blankj.utilcode.util.LogUtils
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.encodeToString
|
||||
import li.songe.gkd.util.Singleton
|
||||
import li.songe.gkd.utils.Singleton
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
object CompositionExt {
|
||||
|
@ -40,15 +39,14 @@ object CompositionExt {
|
|||
}
|
||||
val filter = IntentFilter(packageName)
|
||||
|
||||
val broadcastManager = LocalBroadcastManager.getInstance(this)
|
||||
broadcastManager.registerReceiver(receiver, filter)
|
||||
registerReceiver(receiver, filter)
|
||||
val sendMessage: (InvokeMessage) -> Unit = { message ->
|
||||
broadcastManager.sendBroadcast(Intent(packageName).apply {
|
||||
sendBroadcast(Intent(packageName).apply {
|
||||
putExtra("__invoke", Singleton.json.encodeToString(message))
|
||||
})
|
||||
}
|
||||
onDestroy {
|
||||
broadcastManager.unregisterReceiver(receiver)
|
||||
unregisterReceiver(receiver)
|
||||
}
|
||||
val setter: ((InvokeMessage) -> Unit) -> Unit = { onMessage = it }
|
||||
return (setter to sendMessage)
|
||||
|
|
35
app/src/main/java/li/songe/gkd/data/AppInfo.kt
Normal file
35
app/src/main/java/li/songe/gkd/data/AppInfo.kt
Normal file
|
@ -0,0 +1,35 @@
|
|||
package li.songe.gkd.data
|
||||
|
||||
import android.content.pm.PackageManager
|
||||
import android.graphics.drawable.Drawable
|
||||
import li.songe.gkd.App
|
||||
import li.songe.gkd.utils.Ext.getApplicationInfoExt
|
||||
|
||||
data class AppInfo(
|
||||
val id: String,
|
||||
val name: String? = null,
|
||||
val icon: Drawable? = null,
|
||||
val installed: Boolean = true
|
||||
)
|
||||
|
||||
private val appInfoCache = mutableMapOf<String, AppInfo>()
|
||||
|
||||
fun getAppInfo(id: String): AppInfo {
|
||||
appInfoCache[id]?.let { return it }
|
||||
val packageManager = App.context.packageManager
|
||||
val info = try {
|
||||
// 需要权限
|
||||
val rawInfo = App.context.packageManager.getApplicationInfoExt(
|
||||
id, PackageManager.GET_META_DATA
|
||||
)
|
||||
AppInfo(
|
||||
id = id,
|
||||
name = packageManager.getApplicationLabel(rawInfo).toString(),
|
||||
icon = packageManager.getApplicationIcon(rawInfo),
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
return AppInfo(id = id, installed = false)
|
||||
}
|
||||
appInfoCache[id] = info
|
||||
return info
|
||||
}
|
|
@ -1,13 +1,13 @@
|
|||
package li.songe.gkd.debug
|
||||
package li.songe.gkd.data
|
||||
|
||||
import android.graphics.Rect
|
||||
import android.view.accessibility.AccessibilityNodeInfo
|
||||
import kotlinx.serialization.Serializable
|
||||
import li.songe.gkd.selector.getDepth
|
||||
import li.songe.gkd.selector.getIndex
|
||||
import li.songe.gkd.accessibility.getDepth
|
||||
import li.songe.gkd.accessibility.getIndex
|
||||
|
||||
@Serializable
|
||||
data class AttrSnapshot(
|
||||
data class AttrInfo(
|
||||
val id: String? = null,
|
||||
val name: String? = null,
|
||||
val text: String? = null,
|
||||
|
@ -30,9 +30,9 @@ data class AttrSnapshot(
|
|||
private val rect = Rect()
|
||||
fun info2data(
|
||||
nodeInfo: AccessibilityNodeInfo,
|
||||
): AttrSnapshot {
|
||||
): AttrInfo {
|
||||
nodeInfo.getBoundsInScreen(rect)
|
||||
return AttrSnapshot(
|
||||
return AttrInfo(
|
||||
id = nodeInfo.viewIdResourceName,
|
||||
name = nodeInfo.className?.toString(),
|
||||
text = nodeInfo.text?.toString(),
|
|
@ -1,25 +1,18 @@
|
|||
package li.songe.gkd.debug
|
||||
package li.songe.gkd.data
|
||||
|
||||
import android.os.Build
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class DeviceSnapshot(
|
||||
@SerialName("device")
|
||||
data class DeviceInfo(
|
||||
val device: String = Build.DEVICE,
|
||||
@SerialName("model")
|
||||
val model: String = Build.MODEL,
|
||||
@SerialName("manufacturer")
|
||||
val manufacturer: String = Build.MANUFACTURER,
|
||||
@SerialName("brand")
|
||||
val brand: String = Build.BRAND,
|
||||
@SerialName("sdkInt")
|
||||
val sdkInt: Int = Build.VERSION.SDK_INT,
|
||||
@SerialName("release")
|
||||
val release: String = Build.VERSION.RELEASE,
|
||||
){
|
||||
companion object{
|
||||
val instance by lazy { DeviceSnapshot() }
|
||||
val instance by lazy { DeviceInfo() }
|
||||
}
|
||||
}
|
|
@ -1,24 +1,17 @@
|
|||
package li.songe.gkd.debug
|
||||
package li.songe.gkd.data
|
||||
|
||||
import android.view.accessibility.AccessibilityNodeInfo
|
||||
import kotlinx.serialization.Serializable
|
||||
import li.songe.gkd.selector.forEachIndexed
|
||||
import li.songe.gkd.accessibility.forEachIndexed
|
||||
import java.util.ArrayDeque
|
||||
|
||||
|
||||
/**
|
||||
* api/node 返回列表
|
||||
*/
|
||||
|
||||
@Serializable
|
||||
data class NodeSnapshot(
|
||||
val id: Int,
|
||||
val pid: Int,
|
||||
val index: Int,
|
||||
data class NodeInfo(
|
||||
val id: Int, val pid: Int, val index: Int,
|
||||
/**
|
||||
* null: when getChild(i) return null
|
||||
*/
|
||||
val attr: AttrSnapshot?
|
||||
val attr: AttrInfo?
|
||||
) {
|
||||
companion object {
|
||||
fun abNodeToNode(
|
||||
|
@ -26,22 +19,17 @@ data class NodeSnapshot(
|
|||
id: Int = 0,
|
||||
pid: Int = -1,
|
||||
index: Int = 0,
|
||||
): NodeSnapshot {
|
||||
return NodeSnapshot(
|
||||
id,
|
||||
pid,
|
||||
index,
|
||||
nodeInfo?.let { AttrSnapshot.info2data(nodeInfo) }
|
||||
)
|
||||
): NodeInfo {
|
||||
return NodeInfo(id, pid, index, nodeInfo?.let { AttrInfo.info2data(nodeInfo) })
|
||||
}
|
||||
|
||||
fun info2nodeList(nodeInfo: AccessibilityNodeInfo?): List<NodeSnapshot> {
|
||||
fun info2nodeList(nodeInfo: AccessibilityNodeInfo?): List<NodeInfo> {
|
||||
if (nodeInfo == null) {
|
||||
return emptyList()
|
||||
}
|
||||
val stack = ArrayDeque<Pair<Int, AccessibilityNodeInfo?>>()
|
||||
stack.push(0 to nodeInfo)
|
||||
val list = mutableListOf<NodeSnapshot>()
|
||||
val list = mutableListOf<NodeInfo>()
|
||||
list.add(abNodeToNode(nodeInfo, index = 0))
|
||||
while (stack.isNotEmpty()) {
|
||||
val top = stack.pop()
|
|
@ -1,4 +1,4 @@
|
|||
package li.songe.gkd.debug
|
||||
package li.songe.gkd.data
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
|
@ -6,7 +6,7 @@ import kotlinx.serialization.Serializable
|
|||
data class RpcError(
|
||||
override val message: String = "unknown error",
|
||||
val code: Int = 0,
|
||||
val X_Rpc_Result: Boolean = true
|
||||
val X_Rpc_Result:String = "error"
|
||||
) : Exception(message) {
|
||||
companion object {
|
||||
const val HeaderKey = "X_Rpc_Result"
|
|
@ -1,8 +1,8 @@
|
|||
package li.songe.gkd.data
|
||||
|
||||
import android.view.accessibility.AccessibilityNodeInfo
|
||||
import li.songe.gkd.selector.querySelector
|
||||
import li.songe.selector_core.Selector
|
||||
import li.songe.gkd.accessibility.querySelector
|
||||
import li.songe.selector.Selector
|
||||
|
||||
data class Rule(
|
||||
/**
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
package li.songe.gkd.data
|
||||
|
||||
import li.songe.selector_core.Selector
|
||||
import li.songe.selector.Selector
|
||||
|
||||
class RuleManager(vararg subscriptionRawArray: SubscriptionRaw) {
|
||||
|
||||
|
@ -88,24 +88,31 @@ class RuleManager(vararg subscriptionRawArray: SubscriptionRaw) {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
fun match(appId: String? = null, activityId: String? = null) = sequence {
|
||||
if (appId == null) return@sequence
|
||||
val rules = appToRulesMap[appId] ?: return@sequence
|
||||
rules.forEach { rule ->
|
||||
if (!rule.active) return@forEach // 处于冷却时间
|
||||
|
||||
if (rule.excludeActivityIds.contains(activityId)) return@forEach // 是被排除的 界面 id
|
||||
|
||||
if (rule.preRules.isNotEmpty()) { // 需要提前触发某个规则
|
||||
val record = triggerLogQueue.lastOrNull() ?: return@forEach
|
||||
if (!rule.preRules.any { it == record.rule }) return@forEach // 上一个触发的规则不在当前需要触发的列表
|
||||
if (activityId == null) {
|
||||
yieldAll(rules)
|
||||
return@sequence
|
||||
}
|
||||
rules.forEach { rule ->
|
||||
if (rule.excludeActivityIds.any { activityId.startsWith(it) }) return@forEach // 是被排除的 界面 id
|
||||
|
||||
if (activityId == null || rule.matchAnyActivity // 全匹配
|
||||
|| rule.activityIds.contains(activityId) // 在匹配列表
|
||||
if (rule.matchAnyActivity || rule.activityIds.any { activityId.startsWith(it) } // 在匹配列表
|
||||
) {
|
||||
yield(rule)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun ruleIsAvailable(rule: Rule): Boolean {
|
||||
if (!rule.active) return false // 处于冷却时间
|
||||
if (rule.preKeys.isNotEmpty()) { // 需要提前触发某个规则
|
||||
if (rule.preRules.isEmpty()) return false // 声明了 preKeys 但是没有在当前列表找到
|
||||
val record = triggerLogQueue.lastOrNull() ?: return false
|
||||
if (!rule.preRules.any { it == record.rule }) return false // 上一个触发的规则不在当前需要触发的列表
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
82
app/src/main/java/li/songe/gkd/data/Snapshot.kt
Normal file
82
app/src/main/java/li/songe/gkd/data/Snapshot.kt
Normal file
|
@ -0,0 +1,82 @@
|
|||
package li.songe.gkd.data
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Delete
|
||||
import androidx.room.Entity
|
||||
import androidx.room.Insert
|
||||
import androidx.room.PrimaryKey
|
||||
import androidx.room.Query
|
||||
import androidx.room.TypeConverters
|
||||
import androidx.room.Update
|
||||
import com.blankj.utilcode.util.AppUtils
|
||||
import com.blankj.utilcode.util.ScreenUtils
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.serialization.Serializable
|
||||
import li.songe.gkd.accessibility.GkdAbService
|
||||
import li.songe.gkd.db.IgnoreConverters
|
||||
import li.songe.gkd.utils.Ext
|
||||
|
||||
@TypeConverters(IgnoreConverters::class)
|
||||
@Entity(
|
||||
tableName = "snapshot",
|
||||
)
|
||||
@Serializable
|
||||
data class Snapshot(
|
||||
@PrimaryKey @ColumnInfo(name = "id") val id: Long = System.currentTimeMillis(),
|
||||
@ColumnInfo(name = "app_id") val appId: String? = null,
|
||||
@ColumnInfo(name = "activity_id") val activityId: String? = null,
|
||||
@ColumnInfo(name = "app_name") val appName: String? = Ext.getAppName(appId),
|
||||
@ColumnInfo(name = "app_version_code") val appVersionCode: Int? = appId?.let {
|
||||
AppUtils.getAppVersionCode(
|
||||
appId
|
||||
)
|
||||
},
|
||||
@ColumnInfo(name = "app_version_name") val appVersionName: String? = appId?.let {
|
||||
AppUtils.getAppVersionName(
|
||||
appId
|
||||
)
|
||||
},
|
||||
|
||||
@ColumnInfo(name = "screen_height") val screenHeight: Int = ScreenUtils.getScreenHeight(),
|
||||
@ColumnInfo(name = "screen_width") val screenWidth: Int = ScreenUtils.getScreenWidth(),
|
||||
@ColumnInfo(name = "is_landscape") val isLandscape: Boolean = ScreenUtils.isLandscape(),
|
||||
|
||||
@ColumnInfo(name = "device") val device: String = DeviceInfo.instance.device,
|
||||
@ColumnInfo(name = "model") val model: String = DeviceInfo.instance.model,
|
||||
@ColumnInfo(name = "manufacturer") val manufacturer: String = DeviceInfo.instance.manufacturer,
|
||||
@ColumnInfo(name = "brand") val brand: String = DeviceInfo.instance.brand,
|
||||
@ColumnInfo(name = "sdk_int") val sdkInt: Int = DeviceInfo.instance.sdkInt,
|
||||
@ColumnInfo(name = "release") val release: String = DeviceInfo.instance.release,
|
||||
|
||||
@ColumnInfo(name = "_1") val nodes: List<NodeInfo> = emptyList(),
|
||||
) {
|
||||
companion object {
|
||||
fun current(includeNode: Boolean = true): Snapshot {
|
||||
val currentAbNode = GkdAbService.currentAbNode
|
||||
val appId = currentAbNode?.packageName?.toString()
|
||||
val currentActivityId = GkdAbService.currentActivityId
|
||||
return Snapshot(
|
||||
appId = appId,
|
||||
activityId = currentActivityId,
|
||||
nodes = if (includeNode) NodeInfo.info2nodeList(currentAbNode) else emptyList()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Dao
|
||||
@TypeConverters(IgnoreConverters::class)
|
||||
interface SnapshotDao {
|
||||
@Update
|
||||
suspend fun update(vararg objects: Snapshot): Int
|
||||
|
||||
@Insert
|
||||
suspend fun insert(vararg users: Snapshot): List<Long>
|
||||
|
||||
@Delete
|
||||
suspend fun delete(vararg users: Snapshot): Int
|
||||
|
||||
@Query("SELECT * FROM snapshot")
|
||||
fun query(): Flow<List<Snapshot>>
|
||||
}
|
||||
}
|
64
app/src/main/java/li/songe/gkd/data/SubsConfig.kt
Normal file
64
app/src/main/java/li/songe/gkd/data/SubsConfig.kt
Normal file
|
@ -0,0 +1,64 @@
|
|||
package li.songe.gkd.data
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.room.ColumnInfo
|
||||
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
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Entity(
|
||||
tableName = "subs_config",
|
||||
)
|
||||
@Parcelize
|
||||
data class SubsConfig(
|
||||
@PrimaryKey @ColumnInfo(name = "id") val id: Long = System.currentTimeMillis(),
|
||||
@ColumnInfo(name = "type") val type: Int = SubsType,
|
||||
@ColumnInfo(name = "enable") val enable: Boolean = true,
|
||||
|
||||
@ColumnInfo(name = "subs_item_id") val subsItemId: Long = -1,
|
||||
@ColumnInfo(name = "app_id") val appId: String = "",
|
||||
@ColumnInfo(name = "group_key") val groupKey: Int = -1,
|
||||
) : Parcelable {
|
||||
|
||||
companion object {
|
||||
const val SubsType = 0
|
||||
const val AppType = 1
|
||||
const val GroupType = 2
|
||||
}
|
||||
|
||||
@Dao
|
||||
interface SubsConfigDao {
|
||||
|
||||
@Update
|
||||
suspend fun update(vararg objects: SubsConfig): Int
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insert(vararg users: SubsConfig): List<Long>
|
||||
|
||||
@Delete
|
||||
suspend fun delete(vararg users: SubsConfig): Int
|
||||
|
||||
@Query("DELETE FROM subs_config WHERE subs_item_id=:subsItemId")
|
||||
suspend fun deleteSubs(subsItemId: Long): Int
|
||||
|
||||
@Query("SELECT * FROM subs_config")
|
||||
fun query(): Flow<List<SubsConfig>>
|
||||
|
||||
@Query("SELECT * FROM subs_config WHERE type=${SubsType}")
|
||||
fun querySubsTypeConfig(): Flow<List<SubsConfig>>
|
||||
|
||||
@Query("SELECT * FROM subs_config WHERE type=${AppType} and subs_item_id=:subsItemId")
|
||||
fun queryAppTypeConfig(subsItemId: Long): Flow<List<SubsConfig>>
|
||||
|
||||
@Query("SELECT * FROM subs_config WHERE type=${GroupType} and subs_item_id=:subsItemId and app_id=:appId")
|
||||
suspend fun queryGroupTypeConfig(subsItemId: Long, appId: String): List<SubsConfig>
|
||||
}
|
||||
|
||||
}
|
79
app/src/main/java/li/songe/gkd/data/SubsItem.kt
Normal file
79
app/src/main/java/li/songe/gkd/data/SubsItem.kt
Normal file
|
@ -0,0 +1,79 @@
|
|||
package li.songe.gkd.data
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Delete
|
||||
import androidx.room.Entity
|
||||
import androidx.room.Insert
|
||||
import androidx.room.PrimaryKey
|
||||
import androidx.room.Query
|
||||
import androidx.room.Update
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.parcelize.IgnoredOnParcel
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import li.songe.gkd.db.DbSet
|
||||
import li.songe.gkd.utils.FolderExt
|
||||
import java.io.File
|
||||
|
||||
@Entity(
|
||||
tableName = "subs_item",
|
||||
)
|
||||
@Parcelize
|
||||
data class SubsItem(
|
||||
@PrimaryKey @ColumnInfo(name = "id") val id: Long = System.currentTimeMillis(),
|
||||
@ColumnInfo(name = "mtime") val mtime: Long = System.currentTimeMillis(),
|
||||
@ColumnInfo(name = "enable") val enable: Boolean = true,
|
||||
@ColumnInfo(name = "enable_update") val enableUpdate: Boolean = true,
|
||||
@ColumnInfo(name = "order") val order: Int = 0,
|
||||
|
||||
// 订阅文件的根字段
|
||||
@ColumnInfo(name = "name") val name: String = "",
|
||||
@ColumnInfo(name = "author") val author: String = "",
|
||||
@ColumnInfo(name = "version") val version: Int = 0,
|
||||
@ColumnInfo(name = "update_url") val updateUrl: String = "",
|
||||
@ColumnInfo(name = "support_url") val supportUrl: String = "",
|
||||
|
||||
) : Parcelable {
|
||||
|
||||
@IgnoredOnParcel
|
||||
val subsFile by lazy {
|
||||
File(FolderExt.subsFolder.absolutePath.plus("/${id}.json"))
|
||||
}
|
||||
|
||||
@IgnoredOnParcel
|
||||
val subscriptionRaw by lazy {
|
||||
try {
|
||||
SubscriptionRaw.parse5(subsFile.readText())
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun removeAssets() {
|
||||
DbSet.subsItemDao.delete(this)
|
||||
withContext(IO) {
|
||||
subsFile.exists() && subsFile.delete()
|
||||
}
|
||||
DbSet.subsConfigDao.deleteSubs(id)
|
||||
}
|
||||
|
||||
|
||||
@Dao
|
||||
interface SubsItemDao {
|
||||
@Update
|
||||
suspend fun update(vararg objects: SubsItem): Int
|
||||
|
||||
@Insert
|
||||
suspend fun insert(vararg users: SubsItem): List<Long>
|
||||
|
||||
@Delete
|
||||
suspend fun delete(vararg users: SubsItem): Int
|
||||
|
||||
@Query("SELECT * FROM subs_item ORDER BY `order`")
|
||||
fun query(): Flow<List<SubsItem>>
|
||||
}
|
||||
}
|
|
@ -6,8 +6,8 @@ import kotlinx.serialization.SerialName
|
|||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.*
|
||||
import li.songe.gkd.util.Singleton
|
||||
import li.songe.selector_core.Selector
|
||||
import li.songe.gkd.utils.Singleton
|
||||
import li.songe.selector.Selector
|
||||
|
||||
|
||||
@Parcelize
|
||||
|
@ -35,6 +35,7 @@ data class SubscriptionRaw(
|
|||
@Serializable
|
||||
data class GroupRaw(
|
||||
@SerialName("name") val name: String? = null,
|
||||
@SerialName("desc") val desc: String? = null,
|
||||
@SerialName("key") val key: Int? = null,
|
||||
@SerialName("cd") val cd: Long? = null,
|
||||
@SerialName("activityIds") val activityIds: List<String>? = null,
|
||||
|
@ -78,13 +79,13 @@ data class SubscriptionRaw(
|
|||
JsonNull, null -> null
|
||||
is JsonArray -> element.map {
|
||||
when (it) {
|
||||
is JsonObject, is JsonArray, JsonNull -> error("Element ${this::class} is not a int")
|
||||
is JsonObject, is JsonArray, JsonNull -> error("Element $it is not a int")
|
||||
is JsonPrimitive -> it.int
|
||||
}
|
||||
}
|
||||
|
||||
is JsonPrimitive -> listOf(element.int)
|
||||
else -> error("")
|
||||
else -> error("Element $element is not a Array")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -95,11 +96,11 @@ data class SubscriptionRaw(
|
|||
if (p.isString) {
|
||||
p.content
|
||||
} else {
|
||||
error("")
|
||||
error("Element $p is not a string")
|
||||
}
|
||||
}
|
||||
|
||||
else -> error("")
|
||||
else -> error("Element $p is not a string")
|
||||
}
|
||||
|
||||
@Suppress("SameParameterValue")
|
||||
|
@ -110,7 +111,7 @@ data class SubscriptionRaw(
|
|||
p.long
|
||||
}
|
||||
|
||||
else -> error("")
|
||||
else -> error("Element $p is not a long")
|
||||
}
|
||||
|
||||
private fun getInt(json: JsonObject? = null, key: String = ""): Int? =
|
||||
|
@ -120,7 +121,7 @@ data class SubscriptionRaw(
|
|||
p.int
|
||||
}
|
||||
|
||||
else -> error("")
|
||||
else -> error("Element $p is not a int")
|
||||
}
|
||||
|
||||
private fun jsonToRuleRaw(rulesRawJson: JsonElement): RuleRaw {
|
||||
|
@ -134,12 +135,10 @@ data class SubscriptionRaw(
|
|||
excludeActivityIds = getStringIArray(rulesJson, "excludeActivityIds"),
|
||||
cd = getLong(rulesJson, "cd"),
|
||||
matches = (getStringIArray(
|
||||
rulesJson,
|
||||
"matches"
|
||||
rulesJson, "matches"
|
||||
) ?: emptyList()).onEach { Selector.parse(it) },
|
||||
excludeMatches = (getStringIArray(
|
||||
rulesJson,
|
||||
"excludeMatches"
|
||||
rulesJson, "excludeMatches"
|
||||
) ?: emptyList()).onEach { Selector.parse(it) },
|
||||
key = getInt(rulesJson, "key"),
|
||||
name = getString(rulesJson, "name"),
|
||||
|
@ -154,11 +153,11 @@ data class SubscriptionRaw(
|
|||
is JsonObject -> groupsRawJson
|
||||
is JsonPrimitive, is JsonArray -> JsonObject(mapOf("rules" to groupsRawJson))
|
||||
}
|
||||
return GroupRaw(
|
||||
activityIds = getStringIArray(groupsJson, "activityIds"),
|
||||
return GroupRaw(activityIds = getStringIArray(groupsJson, "activityIds"),
|
||||
excludeActivityIds = getStringIArray(groupsJson, "excludeActivityIds"),
|
||||
cd = getLong(groupsJson, "cd"),
|
||||
name = getString(groupsJson, "name"),
|
||||
desc = getString(groupsJson, "desc"),
|
||||
key = getInt(groupsJson, "key"),
|
||||
rules = when (val rulesJson = groupsJson["rules"]) {
|
||||
null, JsonNull -> emptyList()
|
||||
|
@ -166,13 +165,11 @@ data class SubscriptionRaw(
|
|||
is JsonArray -> rulesJson
|
||||
}.map {
|
||||
jsonToRuleRaw(it)
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
private fun jsonToAppRaw(appsJson: JsonObject): AppRaw {
|
||||
return AppRaw(
|
||||
activityIds = getStringIArray(appsJson, "activityIds"),
|
||||
return AppRaw(activityIds = getStringIArray(appsJson, "activityIds"),
|
||||
excludeActivityIds = getStringIArray(appsJson, "excludeActivityIds"),
|
||||
cd = getLong(appsJson, "cd"),
|
||||
id = getString(appsJson, "id") ?: error(""),
|
||||
|
@ -182,25 +179,22 @@ data class SubscriptionRaw(
|
|||
is JsonArray -> groupsJson
|
||||
}).map {
|
||||
jsonToGroupRaw(it)
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
private fun jsonToSubscriptionRaw(rootJson: JsonObject): SubscriptionRaw {
|
||||
return SubscriptionRaw(
|
||||
name = getString(rootJson, "name") ?: error(""),
|
||||
return SubscriptionRaw(name = getString(rootJson, "name") ?: error(""),
|
||||
version = getInt(rootJson, "version") ?: error(""),
|
||||
author = getString(rootJson, "author"),
|
||||
updateUrl = getString(rootJson, "updateUrl"),
|
||||
supportUrl = getString(rootJson, "supportUrl"),
|
||||
apps = rootJson["apps"]?.jsonArray?.map { jsonToAppRaw(it.jsonObject) }
|
||||
?: emptyList()
|
||||
)
|
||||
?: emptyList())
|
||||
}
|
||||
|
||||
fun stringify(source: SubscriptionRaw) = Singleton.json.encodeToString(source)
|
||||
|
||||
fun parse(source: String): SubscriptionRaw {
|
||||
private fun parse(source: String): SubscriptionRaw {
|
||||
return jsonToSubscriptionRaw(Singleton.json.parseToJsonElement(source).jsonObject)
|
||||
}
|
||||
|
||||
|
|
54
app/src/main/java/li/songe/gkd/data/TriggerLog.kt
Normal file
54
app/src/main/java/li/songe/gkd/data/TriggerLog.kt
Normal file
|
@ -0,0 +1,54 @@
|
|||
package li.songe.gkd.data
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Delete
|
||||
import androidx.room.Entity
|
||||
import androidx.room.Insert
|
||||
import androidx.room.PrimaryKey
|
||||
import androidx.room.Query
|
||||
import androidx.room.Update
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import java.nio.channels.Selector
|
||||
|
||||
@Entity(
|
||||
tableName = "trigger_log",
|
||||
)
|
||||
@Parcelize
|
||||
data class TriggerLog(
|
||||
/**
|
||||
* 此 id 与某个 snapshot id 一致, 表示 one to one
|
||||
*/
|
||||
@PrimaryKey @ColumnInfo(name = "id") val id: Long,
|
||||
/**
|
||||
* 订阅文件 id
|
||||
*/
|
||||
@ColumnInfo(name = "subs_id") val subsId: Long,
|
||||
/**
|
||||
* 触发的组 id
|
||||
*/
|
||||
@ColumnInfo(name = "group_key") val groupKey: Int,
|
||||
|
||||
/**
|
||||
* 触发的选择器
|
||||
*/
|
||||
@ColumnInfo(name = "match") val match: String,
|
||||
|
||||
) : Parcelable {
|
||||
@Dao
|
||||
interface TriggerLogDao {
|
||||
|
||||
@Update
|
||||
suspend fun update(vararg objects: TriggerLog): Int
|
||||
|
||||
@Insert
|
||||
suspend fun insert(vararg users: TriggerLog): List<Long>
|
||||
|
||||
@Delete
|
||||
suspend fun delete(vararg users: TriggerLog): Int
|
||||
|
||||
@Query("SELECT * FROM trigger_log")
|
||||
suspend fun query(): List<TriggerLog>
|
||||
}
|
||||
}
|
|
@ -1,42 +0,0 @@
|
|||
package li.songe.gkd.db
|
||||
|
||||
import androidx.room.Database
|
||||
import androidx.room.Room
|
||||
import androidx.room.RoomDatabase
|
||||
import com.blankj.utilcode.util.PathUtils
|
||||
import li.songe.gkd.App
|
||||
import li.songe.gkd.db.table.SubsConfig
|
||||
import li.songe.gkd.db.table.SubsItem
|
||||
import java.io.File
|
||||
|
||||
@Database(
|
||||
version = 1,
|
||||
entities = [SubsItem::class, SubsConfig::class],
|
||||
autoMigrations = [
|
||||
// AutoMigration(from = 1, to = 2),
|
||||
// AutoMigration(from = 2, to = 3),
|
||||
],
|
||||
// 自动迁移 https://developer.android.com/training/data-storage/room/migrating-db-versions#automated
|
||||
)
|
||||
abstract class AppDatabase : RoomDatabase() {
|
||||
abstract fun subsItemRoomDao(): SubsItem.RoomDao
|
||||
abstract fun subsConfigRoomDao(): SubsConfig.RoomDao
|
||||
|
||||
companion object {
|
||||
val db by lazy {
|
||||
File(PathUtils.getExternalAppFilesPath().plus("/db/")).apply {
|
||||
if (!exists()) {
|
||||
mkdir()
|
||||
}
|
||||
}
|
||||
val name = PathUtils.getExternalAppFilesPath().plus("/db/database.db")
|
||||
Room.databaseBuilder(
|
||||
App.context,
|
||||
AppDatabase::class.java,
|
||||
name
|
||||
)
|
||||
.fallbackToDestructiveMigration()
|
||||
.build()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,29 +0,0 @@
|
|||
package li.songe.gkd.db
|
||||
|
||||
import androidx.room.Delete
|
||||
import androidx.room.Insert
|
||||
import androidx.room.RawQuery
|
||||
import androidx.room.Update
|
||||
import androidx.sqlite.db.SupportSQLiteQuery
|
||||
|
||||
interface BaseDao<T : Any> {
|
||||
@Insert
|
||||
suspend fun insert(vararg objects: T): List<Long>
|
||||
|
||||
@Delete
|
||||
suspend fun delete(vararg objects: T): Int
|
||||
|
||||
@Update
|
||||
suspend fun update(vararg objects: T): Int
|
||||
|
||||
@RawQuery
|
||||
suspend fun query(sqLiteQuery: SupportSQLiteQuery): List<T>
|
||||
|
||||
@RawQuery
|
||||
suspend fun delete(sqLiteQuery: SupportSQLiteQuery): List<Int>
|
||||
|
||||
// https://developer.android.com/training/data-storage/room/async-queries#kotlin
|
||||
// you must set observedEntities in sub interface
|
||||
// @RawQuery
|
||||
// fun queryFlow(sqLiteQuery: SupportSQLiteQuery): Flow<List<T>>
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
package li.songe.gkd.db
|
||||
|
||||
interface BaseTable {
|
||||
val id: Long
|
||||
val ctime: Long
|
||||
val mtime: Long
|
||||
}
|
32
app/src/main/java/li/songe/gkd/db/DbSet.kt
Normal file
32
app/src/main/java/li/songe/gkd/db/DbSet.kt
Normal file
|
@ -0,0 +1,32 @@
|
|||
package li.songe.gkd.db
|
||||
|
||||
import androidx.room.Room
|
||||
import androidx.room.RoomDatabase
|
||||
import com.blankj.utilcode.util.PathUtils
|
||||
import li.songe.gkd.App
|
||||
import li.songe.gkd.utils.FolderExt
|
||||
import java.io.File
|
||||
|
||||
object DbSet {
|
||||
|
||||
|
||||
private fun <T : RoomDatabase> getDb(
|
||||
klass: Class<T>, name: String
|
||||
): T {
|
||||
return Room.databaseBuilder(
|
||||
App.context, klass, FolderExt.dbFolder.absolutePath.plus("/${name}.db")
|
||||
).fallbackToDestructiveMigration()
|
||||
.enableMultiInstanceInvalidation()
|
||||
.build()
|
||||
}
|
||||
|
||||
private val snapshotDb by lazy { getDb(SnapshotDb::class.java, "snapshot") }
|
||||
private val subsConfigDb by lazy { getDb(SubsConfigDb::class.java, "subsConfig") }
|
||||
private val subsItemDb by lazy { getDb(SubsItemDb::class.java, "subsItem") }
|
||||
private val triggerLogDb by lazy { getDb(TriggerLogDb::class.java, "triggerLog") }
|
||||
|
||||
val subsItemDao by lazy { subsItemDb.subsItemDao() }
|
||||
val subsConfigDao by lazy { subsConfigDb.subsConfigDao() }
|
||||
val snapshotDao by lazy { snapshotDb.snapshotDao() }
|
||||
val triggerLogDao by lazy { triggerLogDb.triggerLogDao() }
|
||||
}
|
14
app/src/main/java/li/songe/gkd/db/IgnoreConverters.kt
Normal file
14
app/src/main/java/li/songe/gkd/db/IgnoreConverters.kt
Normal file
|
@ -0,0 +1,14 @@
|
|||
package li.songe.gkd.db
|
||||
|
||||
import androidx.room.TypeConverter
|
||||
import li.songe.gkd.data.NodeInfo
|
||||
|
||||
object IgnoreConverters {
|
||||
@TypeConverter
|
||||
@JvmStatic
|
||||
fun listToCol(list: List<NodeInfo>): String? = null
|
||||
|
||||
@TypeConverter
|
||||
@JvmStatic
|
||||
fun colToList(value: String?): List<NodeInfo> = emptyList()
|
||||
}
|
|
@ -1,35 +0,0 @@
|
|||
package li.songe.gkd.db
|
||||
|
||||
import androidx.room.Database
|
||||
import androidx.room.Room
|
||||
import androidx.room.RoomDatabase
|
||||
import com.blankj.utilcode.util.PathUtils
|
||||
import li.songe.gkd.App
|
||||
import li.songe.gkd.db.table.TriggerLog
|
||||
import java.io.File
|
||||
|
||||
@Database(
|
||||
version = 1,
|
||||
entities = [TriggerLog::class],
|
||||
)
|
||||
abstract class LogDatabase : RoomDatabase() {
|
||||
abstract fun triggerLogRoomDao(): TriggerLog.RoomDao
|
||||
|
||||
companion object {
|
||||
val logDb by lazy {
|
||||
File(PathUtils.getExternalAppFilesPath().plus("/db/")).apply {
|
||||
if (!exists()) {
|
||||
mkdir()
|
||||
}
|
||||
}
|
||||
val name = PathUtils.getExternalAppFilesPath().plus("/db/log.db")
|
||||
Room.databaseBuilder(
|
||||
App.context,
|
||||
LogDatabase::class.java,
|
||||
name
|
||||
)
|
||||
.fallbackToDestructiveMigration()
|
||||
.build()
|
||||
}
|
||||
}
|
||||
}
|
13
app/src/main/java/li/songe/gkd/db/SnapshotDb.kt
Normal file
13
app/src/main/java/li/songe/gkd/db/SnapshotDb.kt
Normal file
|
@ -0,0 +1,13 @@
|
|||
package li.songe.gkd.db
|
||||
|
||||
import androidx.room.Database
|
||||
import androidx.room.RoomDatabase
|
||||
import li.songe.gkd.data.Snapshot
|
||||
|
||||
@Database(
|
||||
version = 1,
|
||||
entities = [Snapshot::class],
|
||||
)
|
||||
abstract class SnapshotDb: RoomDatabase() {
|
||||
abstract fun snapshotDao(): Snapshot.SnapshotDao
|
||||
}
|
13
app/src/main/java/li/songe/gkd/db/SubsConfigDb.kt
Normal file
13
app/src/main/java/li/songe/gkd/db/SubsConfigDb.kt
Normal file
|
@ -0,0 +1,13 @@
|
|||
package li.songe.gkd.db
|
||||
|
||||
import androidx.room.Database
|
||||
import androidx.room.RoomDatabase
|
||||
import li.songe.gkd.data.SubsConfig
|
||||
|
||||
@Database(
|
||||
version = 1,
|
||||
entities = [SubsConfig::class],
|
||||
)
|
||||
abstract class SubsConfigDb: RoomDatabase() {
|
||||
abstract fun subsConfigDao(): SubsConfig.SubsConfigDao
|
||||
}
|
13
app/src/main/java/li/songe/gkd/db/SubsItemDb.kt
Normal file
13
app/src/main/java/li/songe/gkd/db/SubsItemDb.kt
Normal file
|
@ -0,0 +1,13 @@
|
|||
package li.songe.gkd.db
|
||||
|
||||
import androidx.room.Database
|
||||
import androidx.room.RoomDatabase
|
||||
import li.songe.gkd.data.SubsItem
|
||||
|
||||
@Database(
|
||||
version = 1,
|
||||
entities = [SubsItem::class],
|
||||
)
|
||||
abstract class SubsItemDb: RoomDatabase() {
|
||||
abstract fun subsItemDao(): SubsItem.SubsItemDao
|
||||
}
|
13
app/src/main/java/li/songe/gkd/db/TriggerLogDb.kt
Normal file
13
app/src/main/java/li/songe/gkd/db/TriggerLogDb.kt
Normal file
|
@ -0,0 +1,13 @@
|
|||
package li.songe.gkd.db
|
||||
|
||||
import androidx.room.Database
|
||||
import androidx.room.RoomDatabase
|
||||
import li.songe.gkd.data.TriggerLog
|
||||
|
||||
@Database(
|
||||
version = 1,
|
||||
entities = [TriggerLog::class],
|
||||
)
|
||||
abstract class TriggerLogDb : RoomDatabase() {
|
||||
abstract fun triggerLogDao(): TriggerLog.TriggerLogDao
|
||||
}
|
|
@ -1,43 +0,0 @@
|
|||
package li.songe.gkd.db.table
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import li.songe.gkd.db.BaseDao
|
||||
import li.songe.gkd.db.BaseTable
|
||||
|
||||
@Entity(
|
||||
tableName = "subs_config",
|
||||
)
|
||||
@Parcelize
|
||||
data class SubsConfig(
|
||||
@PrimaryKey(autoGenerate = true) @ColumnInfo(name = "id") override val id: Long = 0,
|
||||
@ColumnInfo(name = "ctime") override val ctime: Long = System.currentTimeMillis(),
|
||||
@ColumnInfo(name = "mtime") override val mtime: Long = System.currentTimeMillis(),
|
||||
|
||||
/**
|
||||
* 0 - app
|
||||
* 1 - group
|
||||
* 2 - rule
|
||||
*/
|
||||
@ColumnInfo(name = "type") val type: Int = 0,
|
||||
@ColumnInfo(name = "enable") val enable: Boolean = true,
|
||||
|
||||
@ColumnInfo(name = "subs_item_id") val subsItemId: Long = -1,
|
||||
@ColumnInfo(name = "app_id") val appId: String = "",
|
||||
@ColumnInfo(name = "group_key") val groupKey: Int = -1,
|
||||
@ColumnInfo(name = "rule_key") val ruleKey: Int = -1,
|
||||
) : BaseTable, Parcelable {
|
||||
|
||||
companion object {
|
||||
const val AppType = 0
|
||||
const val GroupType = 1
|
||||
const val RuleType = 2
|
||||
}
|
||||
|
||||
@Dao
|
||||
interface RoomDao : BaseDao<SubsConfig>
|
||||
}
|
|
@ -1,57 +0,0 @@
|
|||
package li.songe.gkd.db.table
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Entity
|
||||
import androidx.room.Index
|
||||
import androidx.room.PrimaryKey
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import li.songe.gkd.db.BaseDao
|
||||
import li.songe.gkd.db.BaseTable
|
||||
|
||||
@Entity(
|
||||
tableName = "subs_item",
|
||||
indices = [Index(value = ["update_url"], unique = true)]
|
||||
)
|
||||
@Parcelize
|
||||
data class SubsItem(
|
||||
/**
|
||||
* 当主键是0时,autoGenerate将覆盖此字段,插入数据库后 需要用返回值手动更新此字段
|
||||
*/
|
||||
@PrimaryKey(autoGenerate = true) @ColumnInfo(name = "id") override val id: Long = 0,
|
||||
@ColumnInfo(name = "ctime") override val ctime: Long = System.currentTimeMillis(),
|
||||
@ColumnInfo(name = "mtime") override val mtime: Long = System.currentTimeMillis(),
|
||||
|
||||
@ColumnInfo(name = "enable") val enable: Boolean = true,
|
||||
|
||||
/**
|
||||
* 订阅文件 name 属性
|
||||
*/
|
||||
@ColumnInfo(name = "name") val name: String = "",
|
||||
|
||||
/**
|
||||
* 订阅文件下载地址,也是更新链接
|
||||
*/
|
||||
@ColumnInfo(name = "update_url") val updateUrl: String = "",
|
||||
|
||||
/**
|
||||
* 订阅文件下载地址,也是更新链接
|
||||
*/
|
||||
@ColumnInfo(name = "version") val version: Int = 0,
|
||||
|
||||
/**
|
||||
* 订阅文件下载后存放的路径
|
||||
*/
|
||||
@ColumnInfo(name = "file_path") val filePath: String = "",
|
||||
|
||||
/**
|
||||
* 顺序
|
||||
*/
|
||||
@ColumnInfo(name = "index") val index: Int = 0,
|
||||
|
||||
|
||||
) : Parcelable, BaseTable {
|
||||
@Dao
|
||||
interface RoomDao : BaseDao<SubsItem>
|
||||
}
|
|
@ -1,26 +0,0 @@
|
|||
package li.songe.gkd.db.table
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import li.songe.gkd.db.BaseDao
|
||||
import li.songe.gkd.db.BaseTable
|
||||
|
||||
@Entity(
|
||||
tableName = "trigger_log",
|
||||
)
|
||||
@Parcelize
|
||||
data class TriggerLog(
|
||||
@PrimaryKey(autoGenerate = true) @ColumnInfo(name = "id") override val id: Long = 0,
|
||||
@ColumnInfo(name = "ctime") override val ctime: Long = System.currentTimeMillis(),
|
||||
@ColumnInfo(name = "mtime") override val mtime: Long = System.currentTimeMillis(),
|
||||
@ColumnInfo(name = "app_id") val appId: String? = null,
|
||||
@ColumnInfo(name = "activity_id") val activityId: String? = null,
|
||||
@ColumnInfo(name = "selector") val selector: String = ""
|
||||
) : Parcelable, BaseTable {
|
||||
@Dao
|
||||
interface RoomDao : BaseDao<TriggerLog>
|
||||
}
|
|
@ -1,44 +0,0 @@
|
|||
package li.songe.gkd.db.util
|
||||
|
||||
import android.database.DatabaseUtils
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
data class Expression<L : Any, R, T : Any>(
|
||||
val left: L,
|
||||
val operator: String,
|
||||
val right: R,
|
||||
val tableClass: KClass<T>
|
||||
) {
|
||||
fun stringify(): String {
|
||||
val nameText = when (left) {
|
||||
is String -> left.toString()
|
||||
is Expression<*, *, *> -> left.stringify()
|
||||
else -> throw Exception("not support type : $left")
|
||||
}
|
||||
val valueText = when (right) {
|
||||
null -> "NULL"
|
||||
is Boolean -> (if (right) 0 else 1).toString()
|
||||
is String -> DatabaseUtils.sqlEscapeString(right.toString())
|
||||
is Byte, is UByte, is Short, is UShort, is Int, is UInt, is Long, is ULong, is Float, is Double -> right.toString()
|
||||
is List<*> -> "(" + right.joinToString(",\u0020") {
|
||||
if (it is String) {
|
||||
DatabaseUtils.sqlEscapeString(it)
|
||||
} else {
|
||||
it?.toString() ?: "NULL"
|
||||
}
|
||||
} + ")"
|
||||
is GlobString -> right.stringify()
|
||||
is LikeString -> right.stringify()
|
||||
is Expression<*, *, *> -> "(${right.stringify()})"
|
||||
else -> throw Exception("not support type : $right")
|
||||
}
|
||||
return "$nameText $operator $valueText"
|
||||
}
|
||||
|
||||
infix fun <L2 : Any, R2> and(other: Expression<L2, R2, T>) =
|
||||
Expression(this, "AND", other, tableClass)
|
||||
|
||||
infix fun <L2 : Any, R2> or(other: Expression<L2, R2, T>) =
|
||||
Expression(this, "OR", other, tableClass)
|
||||
|
||||
}
|
|
@ -1,22 +0,0 @@
|
|||
package li.songe.gkd.db.util
|
||||
|
||||
import android.database.DatabaseUtils
|
||||
|
||||
data class GlobString(val sqlString: String = "") {
|
||||
fun one() = GlobString("$sqlString?")
|
||||
fun any() = GlobString("$sqlString*")
|
||||
infix fun one(s: String) = GlobString("$sqlString?").str(s)
|
||||
infix fun any(s: String) = GlobString("$sqlString*").str(s)
|
||||
infix fun str(s: String) = GlobString(
|
||||
sqlString + s.replace("\\", "\\\\")
|
||||
.replace("*", "\\*")
|
||||
.replace("?", "\\?")
|
||||
)
|
||||
|
||||
fun stringify() = "${DatabaseUtils.sqlEscapeString(sqlString)} ESCAPE '\\'"
|
||||
|
||||
companion object {
|
||||
fun globString(value: String = "") = GlobString().str(value)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
package li.songe.gkd.db.util
|
||||
|
||||
import android.database.DatabaseUtils
|
||||
|
||||
|
||||
data class LikeString (val sqlString: String = "") {
|
||||
fun one() = LikeString("$sqlString?")
|
||||
fun any() = LikeString("$sqlString*")
|
||||
infix fun one(s: String) = LikeString("${sqlString}_").str(s)
|
||||
infix fun any(s: String) = LikeString("$sqlString%").str(s)
|
||||
infix fun str(s: String) = LikeString(
|
||||
sqlString + s.replace("\\", "\\\\")
|
||||
.replace("_", "\\_")
|
||||
.replace("%", "\\%")
|
||||
)
|
||||
|
||||
fun stringify() = "${DatabaseUtils.sqlEscapeString(sqlString)} ESCAPE '\\'"
|
||||
|
||||
companion object {
|
||||
fun likeString(value: String = "") = LikeString().str(value)
|
||||
}
|
||||
}
|
|
@ -1,66 +0,0 @@
|
|||
package li.songe.gkd.db.util
|
||||
|
||||
import kotlin.reflect.KMutableProperty1
|
||||
import kotlin.reflect.KProperty1
|
||||
|
||||
object Operator {
|
||||
infix fun <L1 : Any, R1, L2 : Any, R2, T : Any> Expression<L1, R1, T>.and(other: Expression<L2, R2, T>) =
|
||||
Expression(this, "AND", other, tableClass)
|
||||
|
||||
infix fun <L1 : Any, R1, L2 : Any, R2, T : Any> Expression<L1, R1, T>.or(other: Expression<L2, R2, T>) =
|
||||
Expression(this, "OR", other, tableClass)
|
||||
|
||||
|
||||
// TODO 当同时设置 Property1 时, 代码失效
|
||||
// 还需要写 Int, Long, String, Boolean 等多种类型的重载, 这种重复性很高,工作量指数级增长的工作确实需要联合类型
|
||||
inline fun <reified T : Any, V, V2> KMutableProperty1<T, V>.baseOperator(
|
||||
value: V2,
|
||||
operator: String,
|
||||
) =
|
||||
Expression(
|
||||
RoomAnnotation.getColumnName(T::class, name),
|
||||
operator,
|
||||
value,
|
||||
T::class
|
||||
)
|
||||
|
||||
inline infix fun <reified T : Any, reified V> KProperty1<T, V>.eq(value: V) =
|
||||
baseOperator(value, "==")
|
||||
|
||||
inline infix fun <reified T : Any, reified V> KProperty1<T, V>.neq(value: V) =
|
||||
baseOperator(value, "!=")
|
||||
|
||||
inline infix fun <reified T : Any, reified V> KProperty1<T, V>.less(value: V) =
|
||||
baseOperator(value, "<")
|
||||
|
||||
inline infix fun <reified T : Any, reified V> KProperty1<T, V>.lessEq(value: V) =
|
||||
baseOperator(value, "<=")
|
||||
|
||||
inline infix fun <reified T : Any, reified V> KProperty1<T, V>.greater(value: V) =
|
||||
baseOperator(value, ">")
|
||||
|
||||
inline infix fun <reified T : Any, reified V> KProperty1<T, V>.greaterEq(value: V) =
|
||||
baseOperator(value, ">=")
|
||||
|
||||
inline infix fun <reified T : Any, reified V> KProperty1<T, V>.inList(value: List<V>) =
|
||||
baseOperator(value, "IN")
|
||||
|
||||
inline infix fun <reified T : Any, reified V> KProperty1<T, V>.glob(value: GlobString) =
|
||||
baseOperator(value, "GLOB")
|
||||
|
||||
inline infix fun <reified T : Any, reified V> KProperty1<T, V>.like(value: LikeString) =
|
||||
baseOperator(value, "LIKE")
|
||||
|
||||
inline fun <reified T : Any, V, V2> KProperty1<T, V>.baseOperator(
|
||||
value: V2,
|
||||
operator: String,
|
||||
) =
|
||||
Expression(
|
||||
RoomAnnotation.getColumnName(T::class, name),
|
||||
operator,
|
||||
value,
|
||||
T::class
|
||||
)
|
||||
|
||||
|
||||
}
|
|
@ -1,56 +0,0 @@
|
|||
package li.songe.gkd.db.util
|
||||
|
||||
import li.songe.gkd.db.table.SubsConfig
|
||||
import li.songe.gkd.db.table.SubsItem
|
||||
import li.songe.gkd.db.table.TriggerLog
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
object RoomAnnotation {
|
||||
|
||||
fun getTableName(cls: KClass<*>): String = when (cls) {
|
||||
SubsConfig::class -> "subs_config"
|
||||
SubsItem::class -> "subs_item"
|
||||
TriggerLog::class -> "trigger_log"
|
||||
else -> throw Exception("""not found className : ${cls.qualifiedName}""")
|
||||
}
|
||||
|
||||
fun getColumnName(cls: KClass<*>, propertyName: String): String = when (cls) {
|
||||
SubsConfig::class -> when (propertyName) {
|
||||
SubsConfig::id.name -> "id"
|
||||
SubsConfig::ctime.name -> "ctime"
|
||||
SubsConfig::mtime.name -> "mtime"
|
||||
SubsConfig::type.name -> "type"
|
||||
SubsConfig::enable.name -> "enable"
|
||||
SubsConfig::subsItemId.name -> "subs_item_id"
|
||||
SubsConfig::appId.name -> "app_id"
|
||||
SubsConfig::groupKey.name -> "group_key"
|
||||
SubsConfig::ruleKey.name -> "rule_key"
|
||||
else -> error("""not found columnName : ${cls.qualifiedName}#$propertyName""")
|
||||
}
|
||||
|
||||
SubsItem::class -> when (propertyName) {
|
||||
SubsItem::id.name -> "id"
|
||||
SubsItem::ctime.name -> "ctime"
|
||||
SubsItem::mtime.name -> "mtime"
|
||||
SubsItem::enable.name -> "enable"
|
||||
SubsItem::name.name -> "name"
|
||||
SubsItem::updateUrl.name -> "update_url"
|
||||
SubsItem::filePath.name -> "file_path"
|
||||
SubsItem::index.name -> "index"
|
||||
else -> error("""not found columnName : ${cls.qualifiedName}#$propertyName""")
|
||||
}
|
||||
|
||||
TriggerLog::class -> when (propertyName) {
|
||||
TriggerLog::id.name -> "id"
|
||||
TriggerLog::ctime.name -> "ctime"
|
||||
TriggerLog::mtime.name -> "mtime"
|
||||
TriggerLog::appId.name -> "app_id"
|
||||
TriggerLog::activityId.name -> "activity_id"
|
||||
TriggerLog::selector.name -> "selector"
|
||||
else -> error("""not found columnName : ${cls.qualifiedName}#$propertyName""")
|
||||
}
|
||||
|
||||
else -> error("""not found className : ${cls.qualifiedName}""")
|
||||
}
|
||||
|
||||
}
|
|
@ -1,84 +0,0 @@
|
|||
package li.songe.gkd.db.util
|
||||
|
||||
import androidx.sqlite.db.SimpleSQLiteQuery
|
||||
import li.songe.gkd.db.AppDatabase.Companion.db
|
||||
import li.songe.gkd.db.BaseDao
|
||||
import li.songe.gkd.db.LogDatabase.Companion.logDb
|
||||
import li.songe.gkd.db.table.*
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
|
||||
object RoomX {
|
||||
// 把表类和具体数据库方法关联起来
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun <T : Any> getBaseDao(cls: KClass<T>) = when (cls) {
|
||||
SubsItem::class -> db.subsItemRoomDao()
|
||||
SubsConfig::class -> db.subsConfigRoomDao()
|
||||
TriggerLog::class -> logDb.triggerLogRoomDao()
|
||||
else -> error("not found class dao : ${cls::class.java.name}")
|
||||
} as BaseDao<T>
|
||||
|
||||
|
||||
suspend inline fun <reified T : Any> update(vararg objects: T): Int {
|
||||
return getBaseDao(T::class).update(*objects)
|
||||
}
|
||||
|
||||
/**
|
||||
* 插入成功后, 自动改变入参对象的 id
|
||||
*/
|
||||
suspend inline fun <reified T : Any> insert(vararg objects: T): List<Long> {
|
||||
return getBaseDao(T::class).insert(*objects)
|
||||
}
|
||||
|
||||
suspend inline fun <reified T : Any> delete(vararg objects: T) =
|
||||
getBaseDao(T::class).delete(*objects)
|
||||
|
||||
suspend inline fun <reified T : Any> select(
|
||||
limit: Int? = null,
|
||||
offset: Int? = null,
|
||||
noinline block: (() -> Expression<*, *, T>)? = null
|
||||
): List<T> {
|
||||
val expression = block?.invoke()
|
||||
val tableName = RoomAnnotation.getTableName(T::class)
|
||||
val sqlString = "SELECT * FROM $tableName" + (if (expression != null) {
|
||||
" WHERE ${expression.stringify()}"
|
||||
} else {
|
||||
""
|
||||
}) + (if (limit != null) {
|
||||
" LIMIT $limit"
|
||||
} else {
|
||||
""
|
||||
}) + (if (offset != null) {
|
||||
" OFFSET $offset"
|
||||
} else {
|
||||
""
|
||||
})
|
||||
val baseDao = getBaseDao(T::class)
|
||||
return baseDao.query(SimpleSQLiteQuery(sqlString))
|
||||
}
|
||||
|
||||
suspend inline fun <reified T : Any> delete(
|
||||
limit: Int? = null,
|
||||
offset: Int? = null,
|
||||
noinline block: (() -> Expression<*, *, T>)? = null
|
||||
): List<Int> {
|
||||
val expression = block?.invoke()
|
||||
val tableName = RoomAnnotation.getTableName(T::class)
|
||||
val sqlString = "DELETE FROM $tableName" + (if (expression != null) {
|
||||
" WHERE ${expression.stringify()}"
|
||||
} else {
|
||||
""
|
||||
}) + (if (limit != null) {
|
||||
" LIMIT $limit"
|
||||
} else {
|
||||
""
|
||||
}) + (if (offset != null) {
|
||||
" OFFSET $offset"
|
||||
} else {
|
||||
""
|
||||
})
|
||||
val baseDao = getBaseDao(T::class)
|
||||
return baseDao.delete(SimpleSQLiteQuery(sqlString))
|
||||
}
|
||||
}
|
||||
|
|
@ -8,11 +8,14 @@ import com.blankj.utilcode.util.ServiceUtils
|
|||
import com.torrydo.floatingbubbleview.FloatingBubble
|
||||
import li.songe.gkd.App
|
||||
import li.songe.gkd.R
|
||||
import li.songe.gkd.composition.CompositionExt.useLifeCycleLog
|
||||
import li.songe.gkd.composition.CompositionFbService
|
||||
import li.songe.gkd.composition.CompositionExt.useMessage
|
||||
import li.songe.gkd.composition.InvokeMessage
|
||||
import li.songe.gkd.utils.SafeR
|
||||
|
||||
class FloatingService : CompositionFbService({
|
||||
useLifeCycleLog()
|
||||
val context = this
|
||||
val (onMessage, sendMessage) = useMessage(this::class.simpleName)
|
||||
|
||||
|
@ -22,9 +25,8 @@ class FloatingService : CompositionFbService({
|
|||
"removeBubbles" -> context.removeBubbles()
|
||||
}
|
||||
}
|
||||
|
||||
setupBubble { _, resolve ->
|
||||
val builder = FloatingBubble.Builder(this).bubble(R.drawable.capture, 40, 40)
|
||||
val builder = FloatingBubble.Builder(this).bubble(SafeR.capture, 40, 40)
|
||||
.enableCloseBubble(false)
|
||||
.addFloatingBubbleListener(object : FloatingBubble.Listener {
|
||||
override fun onClick() {
|
||||
|
@ -38,16 +40,16 @@ class FloatingService : CompositionFbService({
|
|||
override fun setupNotificationBuilder(channelId: String): Notification {
|
||||
return NotificationCompat.Builder(this, channelId)
|
||||
.setOngoing(true)
|
||||
.setSmallIcon(R.drawable.ic_app_2)
|
||||
.setContentTitle("bubble is running")
|
||||
.setContentText("click to do nothing")
|
||||
.setSmallIcon(SafeR.ic_launcher)
|
||||
.setContentTitle("搞快点")
|
||||
.setContentText("正在显示悬浮窗按钮")
|
||||
.setPriority(NotificationCompat.PRIORITY_MIN)
|
||||
.setCategory(Notification.CATEGORY_SERVICE)
|
||||
.build()
|
||||
}
|
||||
|
||||
override fun channelId() = "your_channel_id"
|
||||
override fun channelName() = "your_channel_name"
|
||||
override fun channelId() = "service-floating"
|
||||
override fun channelName() = "悬浮窗按钮服务"
|
||||
override fun notificationId() = 69
|
||||
|
||||
companion object{
|
||||
|
|
|
@ -21,16 +21,24 @@ import io.ktor.server.routing.route
|
|||
import io.ktor.server.routing.routing
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.launch
|
||||
import li.songe.gkd.App
|
||||
import li.songe.gkd.composition.CompositionExt.useMessage
|
||||
import li.songe.gkd.composition.CompositionService
|
||||
import li.songe.gkd.composition.InvokeMessage
|
||||
import li.songe.gkd.data.DeviceInfo
|
||||
import li.songe.gkd.data.RpcError
|
||||
import li.songe.gkd.db.DbSet
|
||||
import li.songe.gkd.debug.SnapshotExt.captureSnapshot
|
||||
import li.songe.gkd.util.Ext.getIpAddressInLocalNetwork
|
||||
import li.songe.gkd.util.Storage
|
||||
import li.songe.gkd.utils.Ext.getIpAddressInLocalNetwork
|
||||
import li.songe.gkd.utils.Storage
|
||||
import li.songe.gkd.utils.launchTry
|
||||
import java.io.File
|
||||
|
||||
class HttpService : CompositionService({
|
||||
|
@ -71,30 +79,16 @@ class HttpService : CompositionService({
|
|||
|
||||
routing {
|
||||
route("/api") {
|
||||
get("/device") { call.respond(DeviceSnapshot.instance) }
|
||||
get("/snapshotIds") {
|
||||
call.respond(SnapshotExt.getSnapshotIds())
|
||||
}
|
||||
get("/device") { call.respond(DeviceInfo.instance) }
|
||||
get("/snapshot") {
|
||||
val id = call.request.queryParameters["id"]?.toLongOrNull()
|
||||
if (id != null) {
|
||||
?: throw RpcError("miss id")
|
||||
val fp = File(SnapshotExt.getSnapshotPath(id))
|
||||
if (!fp.exists()) {
|
||||
throw RpcError("对应快照不存在")
|
||||
}
|
||||
call.response.cacheControl(CacheControl.MaxAge(3600))
|
||||
call.response.cacheControl(CacheControl.MaxAge(3600 * 24 * 7))
|
||||
call.respondFile(fp)
|
||||
} else {
|
||||
removeBubbles()
|
||||
delay(200)
|
||||
try {
|
||||
call.respond(captureSnapshot())
|
||||
} catch (e: Exception) {
|
||||
throw e
|
||||
} finally {
|
||||
showBubbles()
|
||||
}
|
||||
}
|
||||
}
|
||||
get("/screenshot") {
|
||||
val id = call.request.queryParameters["id"]?.toLongOrNull()
|
||||
|
@ -103,22 +97,35 @@ class HttpService : CompositionService({
|
|||
if (!fp.exists()) {
|
||||
throw RpcError("对应截图不存在")
|
||||
}
|
||||
call.response.cacheControl(CacheControl.MaxAge(3600))
|
||||
call.response.cacheControl(CacheControl.MaxAge(3600 * 24 * 7))
|
||||
call.respondFile(fp)
|
||||
}
|
||||
get("/captureSnapshot") {
|
||||
removeBubbles()
|
||||
delay(200)
|
||||
val snapshot = try {
|
||||
captureSnapshot()
|
||||
} finally {
|
||||
showBubbles()
|
||||
}
|
||||
call.respond(snapshot)
|
||||
}
|
||||
get("/snapshots") {
|
||||
call.respond(DbSet.snapshotDao.query().first())
|
||||
}
|
||||
}
|
||||
}
|
||||
scope.launch {
|
||||
}
|
||||
scope.launchTry(Dispatchers.IO) {
|
||||
LogUtils.d(*getIpAddressInLocalNetwork().map { host -> "http://${host}:${Storage.settings.httpServerPort}" }
|
||||
.toList().toTypedArray())
|
||||
server.start(true)
|
||||
}
|
||||
onDestroy {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
server.stop(1000, 2000)
|
||||
scope.cancel()
|
||||
scope.launchTry(Dispatchers.IO) {
|
||||
server.stop()
|
||||
LogUtils.d("http server is stopped")
|
||||
scope.cancel()
|
||||
}
|
||||
}
|
||||
}) {
|
||||
|
|
|
@ -8,6 +8,7 @@ import io.ktor.server.application.hooks.CallFailed
|
|||
import io.ktor.server.request.uri
|
||||
import io.ktor.server.response.header
|
||||
import io.ktor.server.response.respond
|
||||
import li.songe.gkd.data.RpcError
|
||||
|
||||
val RpcErrorHeaderPlugin = createApplicationPlugin(name = "RpcErrorHeaderPlugin") {
|
||||
onCall { call ->
|
||||
|
@ -36,6 +37,7 @@ val RpcErrorHeaderPlugin = createApplicationPlugin(name = "RpcErrorHeaderPlugin"
|
|||
}
|
||||
onCallRespond { call, _ ->
|
||||
call.response.header("Access-Control-Expose-Headers", "*")
|
||||
call.response.header("Access-Control-Allow-Private-Network", "true")
|
||||
val status = call.response.status() ?: HttpStatusCode.OK
|
||||
if (status == HttpStatusCode.OK &&
|
||||
!call.response.headers.contains(
|
|
@ -6,11 +6,13 @@ import android.content.Intent
|
|||
import com.blankj.utilcode.util.LogUtils
|
||||
import com.blankj.utilcode.util.ServiceUtils
|
||||
import li.songe.gkd.App
|
||||
import li.songe.gkd.composition.CompositionExt.useLifeCycleLog
|
||||
import li.songe.gkd.composition.CompositionService
|
||||
import li.songe.gkd.util.Ext
|
||||
import li.songe.gkd.util.ScreenshotUtil
|
||||
import li.songe.gkd.utils.Ext
|
||||
import li.songe.gkd.utils.ScreenshotUtil
|
||||
|
||||
class ScreenshotService : CompositionService({
|
||||
useLifeCycleLog()
|
||||
Ext.createNotificationChannel(this, 110)
|
||||
|
||||
onStartCommand { intent, _, _ ->
|
||||
|
|
|
@ -1,32 +0,0 @@
|
|||
package li.songe.gkd.debug
|
||||
|
||||
import com.blankj.utilcode.util.ScreenUtils
|
||||
import kotlinx.serialization.Serializable
|
||||
import li.songe.gkd.accessibility.GkdAbService
|
||||
import li.songe.gkd.util.Ext
|
||||
|
||||
@Serializable
|
||||
data class Snapshot(
|
||||
val id: Long = System.currentTimeMillis(),
|
||||
val device: DeviceSnapshot? = DeviceSnapshot.instance,
|
||||
val screenHeight: Int = ScreenUtils.getScreenHeight(),
|
||||
val screenWidth: Int = ScreenUtils.getScreenWidth(),
|
||||
val appId: String? = null,
|
||||
val appName: String? = null,
|
||||
val activityId: String? = null,
|
||||
val nodes: List<NodeSnapshot>? = null,
|
||||
) {
|
||||
companion object {
|
||||
fun current(): Snapshot {
|
||||
val shot = GkdAbService.currentNodeSnapshot()
|
||||
return Snapshot(
|
||||
appId = shot?.appId,
|
||||
appName = if (shot?.appId != null) {
|
||||
Ext.getAppName(shot.appId)
|
||||
} else null,
|
||||
activityId = shot?.activityId,
|
||||
nodes = NodeSnapshot.info2nodeList(shot?.root),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,13 +2,20 @@ package li.songe.gkd.debug
|
|||
|
||||
import android.graphics.Bitmap
|
||||
import com.blankj.utilcode.util.LogUtils
|
||||
import com.blankj.utilcode.util.ScreenUtils
|
||||
import com.blankj.utilcode.util.ZipUtils
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import kotlinx.serialization.encodeToString
|
||||
import li.songe.gkd.App
|
||||
import li.songe.gkd.accessibility.GkdAbService
|
||||
import li.songe.gkd.util.Singleton
|
||||
import li.songe.gkd.data.RpcError
|
||||
import li.songe.gkd.data.Snapshot
|
||||
import li.songe.gkd.db.DbSet
|
||||
import li.songe.gkd.utils.Singleton
|
||||
import java.io.File
|
||||
|
||||
object SnapshotExt {
|
||||
|
@ -16,7 +23,15 @@ object SnapshotExt {
|
|||
App.context.getExternalFilesDir("snapshot")!!.apply { if (!exists()) mkdir() }
|
||||
}
|
||||
|
||||
private fun getSnapshotParentPath(snapshotId: Long) =
|
||||
private val emptyBitmap by lazy {
|
||||
Bitmap.createBitmap(
|
||||
ScreenUtils.getScreenWidth(),
|
||||
ScreenUtils.getScreenHeight(),
|
||||
Bitmap.Config.ARGB_8888
|
||||
)
|
||||
}
|
||||
|
||||
fun getSnapshotParentPath(snapshotId: Long) =
|
||||
"${snapshotDir.absolutePath}/${snapshotId}"
|
||||
|
||||
fun getSnapshotPath(snapshotId: Long) =
|
||||
|
@ -30,21 +45,51 @@ object SnapshotExt {
|
|||
?.mapNotNull { f -> f.name.toLongOrNull() } ?: emptyList()
|
||||
}
|
||||
|
||||
suspend fun getSnapshotZipFile(snapshotId: Long): File {
|
||||
val file = File(getSnapshotParentPath(snapshotId) + "/${snapshotId}.zip")
|
||||
if (!file.exists()) {
|
||||
withContext(Dispatchers.IO) {
|
||||
ZipUtils.zipFiles(
|
||||
listOf(
|
||||
getSnapshotPath(snapshotId),
|
||||
getScreenshotPath(snapshotId)
|
||||
), file.absolutePath
|
||||
)
|
||||
}
|
||||
}
|
||||
return file
|
||||
}
|
||||
|
||||
fun remove(id: Long) {
|
||||
File(getSnapshotParentPath(id)).apply {
|
||||
if (exists()) {
|
||||
deleteRecursively()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun captureSnapshot(): Snapshot {
|
||||
if (!GkdAbService.isRunning()) {
|
||||
throw RpcError("无障碍不可用")
|
||||
}
|
||||
|
||||
val snapshotDef = coroutineScope { async(Dispatchers.IO) { Snapshot.current() } }
|
||||
val bitmapDef = coroutineScope {
|
||||
async(Dispatchers.IO) {
|
||||
GkdAbService.currentScreenshot() ?: withTimeoutOrNull(3_000) {
|
||||
if (!ScreenshotService.isRunning()) {
|
||||
return@withTimeoutOrNull null
|
||||
}
|
||||
ScreenshotService.screenshot()
|
||||
} ?: emptyBitmap.apply {
|
||||
LogUtils.d("截屏不可用,即将使用空白图片")
|
||||
}
|
||||
val snapshot = Snapshot.current()
|
||||
val bitmap = withTimeoutOrNull(3_000) {
|
||||
ScreenshotService.screenshot()
|
||||
} ?: Bitmap.createBitmap(
|
||||
snapshot.screenWidth,
|
||||
snapshot.screenHeight,
|
||||
Bitmap.Config.ARGB_8888
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val bitmap = bitmapDef.await()
|
||||
val snapshot = snapshotDef.await()
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
File(getSnapshotParentPath(snapshot.id)).apply { if (!exists()) mkdirs() }
|
||||
val stream =
|
||||
|
@ -53,6 +98,7 @@ object SnapshotExt {
|
|||
stream.close()
|
||||
val text = Singleton.json.encodeToString(snapshot)
|
||||
File(getSnapshotPath(snapshot.id)).writeText(text)
|
||||
DbSet.snapshotDao.insert(snapshot)
|
||||
}
|
||||
return snapshot
|
||||
}
|
||||
|
|
24
app/src/main/java/li/songe/gkd/icon/AddIcon.kt
Normal file
24
app/src/main/java/li/songe/gkd/icon/AddIcon.kt
Normal file
|
@ -0,0 +1,24 @@
|
|||
package li.songe.gkd.icon
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.material.icons.materialIcon
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.addPathNodes
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
|
||||
// @DslMarker
|
||||
// https://github.com/JetBrains/kotlin-wrappers/blob/master/kotlin-react/src/jsMain/kotlin/react/ChildrenBuilder.kt
|
||||
val AddIcon = materialIcon(name = "add") {
|
||||
addPath(
|
||||
pathData = addPathNodes("M18,13h-5v5c0,0.55 -0.45,1 -1,1s-1,-0.45 -1,-1v-5H6c-0.55,0 -1,-0.45 -1,-1s0.45,-1 1,-1h5V6c0,-0.55 0.45,-1 1,-1s1,0.45 1,1v5h5c0.55,0 1,0.45 1,1s-0.45,1 -1,1z"),
|
||||
fill = Brush.linearGradient(listOf(Color.Black, Color.Black))
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun PreviewIconAdd() {
|
||||
Image(imageVector = AddIcon, contentDescription = null)
|
||||
}
|
31
app/src/main/java/li/songe/gkd/icon/TestDsl.kt
Normal file
31
app/src/main/java/li/songe/gkd/icon/TestDsl.kt
Normal file
|
@ -0,0 +1,31 @@
|
|||
package li.songe.gkd.icon
|
||||
|
||||
import android.graphics.drawable.BitmapDrawable
|
||||
import android.graphics.drawable.Drawable
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.google.accompanist.drawablepainter.rememberDrawablePainter
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun PreviewTestDsl() {
|
||||
val vectorString = """
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M20,11H7.83l5.59,-5.59L12,4l-8,8l8,8l1.41,-1.41L7.83,13H20v-2z" />
|
||||
</vector>
|
||||
""".trim()
|
||||
val drawable = Drawable.createFromStream(vectorString.byteInputStream(), "ic_back")
|
||||
if (drawable != null) {
|
||||
Image(painter = rememberDrawablePainter(drawable = drawable), contentDescription = null)
|
||||
} else {
|
||||
Text(text = "null drawable")
|
||||
}
|
||||
}
|
|
@ -1,41 +0,0 @@
|
|||
package li.songe.gkd.selector
|
||||
|
||||
import android.view.accessibility.AccessibilityNodeInfo
|
||||
import li.songe.selector_core.NodeExt
|
||||
|
||||
@JvmInline
|
||||
value class AbNode(val value: AccessibilityNodeInfo) : NodeExt {
|
||||
override val parent: NodeExt?
|
||||
get() = value.parent?.let { AbNode(it) }
|
||||
override val children: Sequence<NodeExt?>
|
||||
get() = sequence {
|
||||
repeat(value.childCount) { i ->
|
||||
val child = value.getChild(i)
|
||||
if (child != null) {
|
||||
yield(AbNode(child))
|
||||
} else {
|
||||
yield(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getChild(offset: Int) = value.getChild(offset)?.let { AbNode(it) }
|
||||
|
||||
override val name: CharSequence
|
||||
get() = value.className
|
||||
|
||||
override fun attr(name: String): Any? = when (name) {
|
||||
"id" -> value.viewIdResourceName
|
||||
"name" -> value.className
|
||||
"text" -> value.text
|
||||
"textLen" -> value.text?.length
|
||||
"desc" -> value.contentDescription
|
||||
"descLen" -> value.contentDescription?.length
|
||||
"isClickable" -> value.isClickable
|
||||
"childCount" -> value.childCount
|
||||
"index" -> value.getIndex()
|
||||
"depth" -> value.getDepth()
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
|
@ -1,77 +0,0 @@
|
|||
package li.songe.gkd.selector
|
||||
|
||||
import android.accessibilityservice.AccessibilityService
|
||||
import android.accessibilityservice.GestureDescription
|
||||
import android.graphics.Path
|
||||
import android.graphics.Rect
|
||||
import android.view.accessibility.AccessibilityNodeInfo
|
||||
import li.songe.selector_core.Selector
|
||||
|
||||
fun AccessibilityNodeInfo.getIndex(): Int {
|
||||
parent?.forEachIndexed { index, accessibilityNodeInfo ->
|
||||
if (accessibilityNodeInfo == this) {
|
||||
return index
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
inline fun AccessibilityNodeInfo.forEachIndexed(action: (index: Int, childNode: AccessibilityNodeInfo?) -> Unit) {
|
||||
var index = 0
|
||||
val childCount = this.childCount
|
||||
while (index < childCount) {
|
||||
val child: AccessibilityNodeInfo? = getChild(index)
|
||||
action(index, child)
|
||||
index += 1
|
||||
}
|
||||
}
|
||||
|
||||
fun AccessibilityNodeInfo.querySelector(selector: Selector): AccessibilityNodeInfo? {
|
||||
val ab = AbNode(this)
|
||||
val result = (ab.querySelector(selector) as AbNode?) ?: return null
|
||||
return result.value
|
||||
}
|
||||
|
||||
fun AccessibilityNodeInfo.querySelectorAll(selector: Selector): Sequence<AbNode> {
|
||||
val ab = AbNode(this)
|
||||
return ab.querySelectorAll(selector) as Sequence<AbNode>
|
||||
}
|
||||
|
||||
fun AccessibilityNodeInfo.click(service: AccessibilityService) = when {
|
||||
this.isClickable -> {
|
||||
this.performAction(AccessibilityNodeInfo.ACTION_CLICK)
|
||||
"self"
|
||||
}
|
||||
|
||||
else -> {
|
||||
val react = Rect()
|
||||
this.getBoundsInScreen(react)
|
||||
val x = react.left + 50f / 100f * (react.right - react.left)
|
||||
val y = react.top + 50f / 100f * (react.bottom - react.top)
|
||||
if (x >= 0 && y >= 0) {
|
||||
val gestureDescription = GestureDescription.Builder()
|
||||
val path = Path()
|
||||
path.moveTo(x, y)
|
||||
gestureDescription.addStroke(GestureDescription.StrokeDescription(path, 0, 300))
|
||||
service.dispatchGesture(gestureDescription.build(), null, null)
|
||||
"(50%, 50%)"
|
||||
} else {
|
||||
"($x, $y) no click"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun AccessibilityNodeInfo.getDepth(): Int {
|
||||
var p: AccessibilityNodeInfo? = this
|
||||
var depth = 0
|
||||
while (true) {
|
||||
val p2 = p?.parent
|
||||
if (p2 != null) {
|
||||
p = p2
|
||||
depth++
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
return depth
|
||||
}
|
|
@ -11,27 +11,9 @@ class AutoStartReceiver : BroadcastReceiver() {
|
|||
Shizuku.addBinderReceivedListenerSticky(oneShotBinderReceivedListener)
|
||||
}
|
||||
}
|
||||
|
||||
private val oneShotBinderReceivedListener = object : Shizuku.OnBinderReceivedListener {
|
||||
override fun onBinderReceived() {
|
||||
// AutomatorViewModel.get().run {
|
||||
// app.openFileOutput("on_boot", Context.MODE_PRIVATE).bufferedWriter().apply {
|
||||
// write("binder received")
|
||||
// newLine()
|
||||
// write("permission granted: ${Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED}")
|
||||
// newLine()
|
||||
// write("is_binding: ${isBinding.value}")
|
||||
// newLine()
|
||||
// write("is_running: ${isRunning.value}")
|
||||
// newLine()
|
||||
// if (Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED &&
|
||||
// isBinding.value != true && isRunning.value != true
|
||||
// ) {
|
||||
// write("starting service...")
|
||||
// toggleService()
|
||||
// isAutoStarted.value = true
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
Shizuku.removeBinderReceivedListener(this)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,168 +0,0 @@
|
|||
package li.songe.gkd.shizuku
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import java.io.*
|
||||
import java.nio.charset.Charset
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.security.DigestInputStream
|
||||
import java.security.MessageDigest
|
||||
import java.util.zip.CRC32
|
||||
|
||||
|
||||
object IOUtils {
|
||||
private const val TAG = "IOUtils"
|
||||
@Throws(IOException::class)
|
||||
fun copyStream(from: InputStream, to: OutputStream) {
|
||||
val buf = ByteArray(1024 * 1024)
|
||||
var len: Int
|
||||
while (from.read(buf).also { len = it } > 0) {
|
||||
to.write(buf, 0, len)
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun copyFile(original: File?, destination: File?) {
|
||||
FileInputStream(original).use { inputStream ->
|
||||
FileOutputStream(destination).use { outputStream ->
|
||||
copyStream(
|
||||
inputStream,
|
||||
outputStream
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun copyFileFromAssets(context: Context, assetFileName: String?, destination: File?) {
|
||||
context.assets.open(assetFileName!!).use { inputStream ->
|
||||
FileOutputStream(destination).use { outputStream ->
|
||||
copyStream(
|
||||
inputStream,
|
||||
outputStream
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteRecursively(f: File) {
|
||||
if (f.isDirectory) {
|
||||
val files = f.listFiles()
|
||||
if (files != null) {
|
||||
for (child in files) deleteRecursively(child)
|
||||
}
|
||||
}
|
||||
f.delete()
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun calculateFileCrc32(file: File?): Long {
|
||||
return calculateCrc32(FileInputStream(file))
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun calculateBytesCrc32(bytes: ByteArray?): Long {
|
||||
return calculateCrc32(ByteArrayInputStream(bytes))
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun calculateCrc32(inputStream: InputStream): Long {
|
||||
inputStream.use { `in` ->
|
||||
val crc32 = CRC32()
|
||||
val buffer = ByteArray(1024 * 1024)
|
||||
var read: Int
|
||||
while (`in`.read(buffer).also { read = it } > 0) crc32.update(buffer, 0, read)
|
||||
return crc32.value
|
||||
}
|
||||
}
|
||||
|
||||
fun writeStreamToStringBuilder(builder: StringBuilder, inputStream: InputStream?): Thread {
|
||||
val t = Thread {
|
||||
try {
|
||||
val buf = CharArray(1024)
|
||||
var len: Int
|
||||
val reader =
|
||||
BufferedReader(InputStreamReader(inputStream))
|
||||
while (reader.read(buf).also { len = it } > 0) builder.append(buf, 0, len)
|
||||
reader.close()
|
||||
} catch (e: Exception) {
|
||||
Log.wtf(TAG, e)
|
||||
}
|
||||
}
|
||||
t.start()
|
||||
return t
|
||||
}
|
||||
|
||||
/**
|
||||
* Read contents of input stream to a byte array and close it
|
||||
*
|
||||
* @param inputStream
|
||||
* @return contents of input stream
|
||||
* @throws IOException
|
||||
*/
|
||||
@Throws(IOException::class)
|
||||
fun readStream(inputStream: InputStream): ByteArray {
|
||||
inputStream.use { `in` -> return readStreamNoClose(`in`) }
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun readStream(inputStream: InputStream, charset: Charset?): String {
|
||||
return String(readStream(inputStream), charset!!)
|
||||
}
|
||||
|
||||
/**
|
||||
* Read contents of input stream to a byte array, but don't close the stream
|
||||
*
|
||||
* @param inputStream
|
||||
* @return contents of input stream
|
||||
* @throws IOException
|
||||
*/
|
||||
@Throws(IOException::class)
|
||||
fun readStreamNoClose(inputStream: InputStream): ByteArray {
|
||||
val buffer = ByteArrayOutputStream()
|
||||
copyStream(inputStream, buffer)
|
||||
return buffer.toByteArray()
|
||||
}
|
||||
|
||||
fun closeSilently(closeable: Closeable?) {
|
||||
if (closeable == null) return
|
||||
try {
|
||||
closeable.close()
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, String.format("Unable to close %s", closeable.javaClass.canonicalName), e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hashes stream content using passed [MessageDigest], closes the stream and returns digest bytes
|
||||
*
|
||||
* @param inputStream
|
||||
* @param messageDigest
|
||||
* @return
|
||||
* @throws IOException
|
||||
*/
|
||||
@Throws(IOException::class)
|
||||
fun hashStream(inputStream: InputStream?, messageDigest: MessageDigest): ByteArray {
|
||||
DigestInputStream(inputStream, messageDigest).use { digestInputStream ->
|
||||
val buffer = ByteArray(1024 * 64)
|
||||
var read: Int
|
||||
while (digestInputStream.read(buffer).also { read = it } > 0) {
|
||||
//Do nothing
|
||||
}
|
||||
return messageDigest.digest()
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun hashString(s: String, messageDigest: MessageDigest): ByteArray {
|
||||
return hashStream(
|
||||
ByteArrayInputStream(s.toByteArray(StandardCharsets.UTF_8)),
|
||||
messageDigest
|
||||
)
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun readFile(file: File?): ByteArray {
|
||||
FileInputStream(file).use { `in` -> return readStream(`in`) }
|
||||
}
|
||||
}
|
|
@ -1,71 +0,0 @@
|
|||
package li.songe.gkd.shizuku
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import java.io.InputStream
|
||||
import java.util.*
|
||||
|
||||
|
||||
interface Shell {
|
||||
val isAvailable: Boolean
|
||||
|
||||
fun exec(command: Command): Result
|
||||
fun exec(command: Command, inputPipe: InputStream): Result
|
||||
fun makeLiteral(arg: String): String
|
||||
class Command(command: String, vararg args: String) {
|
||||
private val mArgs = mutableListOf<String>()
|
||||
fun toStringArray(): Array<String?> {
|
||||
val array = arrayOfNulls<String>(mArgs.size)
|
||||
for (i in mArgs.indices) array[i] = mArgs[i]
|
||||
return array
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
val sb = StringBuilder()
|
||||
for (i in mArgs.indices) {
|
||||
val arg = mArgs[i]
|
||||
sb.append(arg)
|
||||
if (i < mArgs.size - 1) sb.append(" ")
|
||||
}
|
||||
return sb.toString()
|
||||
}
|
||||
|
||||
class Builder(command: String, vararg args: String) {
|
||||
private val mCommand: Command = Command(command, *args)
|
||||
fun addArg(argument: String): Builder {
|
||||
mCommand.mArgs.add(argument)
|
||||
return this
|
||||
}
|
||||
|
||||
fun build(): Command {
|
||||
return mCommand
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
init {
|
||||
mArgs.add(command)
|
||||
mArgs.addAll(args)
|
||||
}
|
||||
}
|
||||
|
||||
open class Result(
|
||||
var cmd: Command,
|
||||
var exitCode: Int,
|
||||
var out: String,
|
||||
var err: String
|
||||
) {
|
||||
val isSuccessful: Boolean
|
||||
get() = exitCode == 0
|
||||
|
||||
@SuppressLint("DefaultLocale")
|
||||
override fun toString(): String {
|
||||
return String.format(
|
||||
"Command: %s\nExit code: %d\nOut:\n%s\n=============\nErr:\n%s",
|
||||
cmd,
|
||||
exitCode,
|
||||
out,
|
||||
err
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
12
app/src/main/java/li/songe/gkd/shizuku/ShizukuExt.kt
Normal file
12
app/src/main/java/li/songe/gkd/shizuku/ShizukuExt.kt
Normal file
|
@ -0,0 +1,12 @@
|
|||
package li.songe.gkd.shizuku
|
||||
|
||||
import android.content.pm.PackageManager
|
||||
import rikka.shizuku.Shizuku
|
||||
|
||||
fun shizukuIsSafeOK(): Boolean {
|
||||
return try {
|
||||
Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED
|
||||
} catch (_: Exception) {
|
||||
false
|
||||
}
|
||||
}
|
|
@ -1,89 +0,0 @@
|
|||
package li.songe.gkd.shizuku
|
||||
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import rikka.shizuku.Shizuku
|
||||
import java.io.InputStream
|
||||
|
||||
|
||||
/**
|
||||
* https://github.com/Aefyr/SAI/blob/master/app/src/main/java/com/aefyr/sai/shell/ShizukuShell.java
|
||||
*/
|
||||
class ShizukuShell private constructor() : Shell {
|
||||
override val isAvailable: Boolean
|
||||
get() = if (!Shizuku.pingBinder()) false else try {
|
||||
exec(Shell.Command("echo", "test")).isSuccessful
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Unable to access shizuku: ")
|
||||
Log.w(TAG, e)
|
||||
false
|
||||
}
|
||||
|
||||
override fun exec(command: Shell.Command): Shell.Result {
|
||||
return execInternal(command, null)
|
||||
}
|
||||
|
||||
override fun exec(command: Shell.Command, inputPipe: InputStream): Shell.Result {
|
||||
return execInternal(command, inputPipe)
|
||||
}
|
||||
|
||||
override fun makeLiteral(arg: String): String {
|
||||
return "'" + arg.replace("'", "'\\''") + "'"
|
||||
}
|
||||
|
||||
private fun execInternal(command: Shell.Command, inputPipe: InputStream?): Shell.Result {
|
||||
val stdOutSb = StringBuilder()
|
||||
val stdErrSb = StringBuilder()
|
||||
return try {
|
||||
val shCommand = Shell.Command.Builder("sh", "-c", command.toString())
|
||||
val process = Shizuku.newProcess(shCommand.build().toStringArray(), null, null)
|
||||
val stdOutD: Thread = IOUtils.writeStreamToStringBuilder(stdOutSb, process.inputStream)
|
||||
val stdErrD: Thread = IOUtils.writeStreamToStringBuilder(stdErrSb, process.errorStream)
|
||||
if (inputPipe != null) {
|
||||
try {
|
||||
process.outputStream.use { outputStream ->
|
||||
inputPipe.use { inputStream ->
|
||||
IOUtils.copyStream(
|
||||
inputStream,
|
||||
outputStream
|
||||
)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
stdOutD.interrupt()
|
||||
stdErrD.interrupt()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
process.destroyForcibly()
|
||||
} else {
|
||||
process.destroy()
|
||||
}
|
||||
throw RuntimeException(e)
|
||||
}
|
||||
}
|
||||
process.waitFor()
|
||||
stdOutD.join()
|
||||
stdErrD.join()
|
||||
Shell.Result(
|
||||
command,
|
||||
process.exitValue(),
|
||||
stdOutSb.toString().trim { it <= ' ' },
|
||||
stdErrSb.toString().trim { it <= ' ' })
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Unable execute command: ")
|
||||
Log.w(TAG, e)
|
||||
Shell.Result(
|
||||
command, -1, stdOutSb.toString().trim { it <= ' ' },
|
||||
"""$stdErrSb
|
||||
|
||||
<!> SAI ShizukuShell Java exception: ${Utils.throwableToString(e)}"""
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "ShizukuShell"
|
||||
val instance by lazy { ShizukuShell() }
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
package li.songe.gkd.shizuku
|
||||
|
||||
|
||||
import android.app.IActivityTaskManager
|
||||
import android.content.pm.IPackageManager
|
||||
import rikka.shizuku.ShizukuBinderWrapper
|
||||
import rikka.shizuku.SystemServiceHelper
|
||||
|
||||
val activityTaskManager: IActivityTaskManager by lazy {
|
||||
SystemServiceHelper.getSystemService("activity_task")
|
||||
.let(::ShizukuBinderWrapper)
|
||||
.let(IActivityTaskManager.Stub::asInterface)
|
||||
}
|
||||
|
||||
val iPackageManager: IPackageManager by lazy {
|
||||
SystemServiceHelper.getSystemService("package")
|
||||
.let(::ShizukuBinderWrapper)
|
||||
.let(IPackageManager.Stub::asInterface)
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
package li.songe.gkd.shizuku
|
||||
|
||||
import java.io.PrintWriter
|
||||
import java.io.StringWriter
|
||||
|
||||
|
||||
object Utils {
|
||||
fun throwableToString(throwable: Throwable): String {
|
||||
val sw = StringWriter(1024)
|
||||
val pw = PrintWriter(sw)
|
||||
|
||||
throwable.printStackTrace(pw)
|
||||
pw.close()
|
||||
|
||||
return sw.toString()
|
||||
}
|
||||
}
|
86
app/src/main/java/li/songe/gkd/ui/AboutPage.kt
Normal file
86
app/src/main/java/li/songe/gkd/ui/AboutPage.kt
Normal file
|
@ -0,0 +1,86 @@
|
|||
package li.songe.gkd.ui
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.Scaffold
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.TopAppBar
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.annotation.RootNavGraph
|
||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||
import li.songe.gkd.BuildConfig
|
||||
import li.songe.gkd.utils.SafeR
|
||||
|
||||
@RootNavGraph
|
||||
@Destination
|
||||
@Composable
|
||||
fun AboutPage(navigator: DestinationsNavigator) {
|
||||
// val systemUiController = rememberSystemUiController()
|
||||
// val context = LocalContext.current as ComponentActivity
|
||||
// DisposableEffect(systemUiController) {
|
||||
// val oldVisible = systemUiController.isStatusBarVisible
|
||||
// systemUiController.isStatusBarVisible = false
|
||||
// WindowCompat.setDecorFitsSystemWindows(context.window, false)
|
||||
// onDispose {
|
||||
// systemUiController.isStatusBarVisible = oldVisible
|
||||
// WindowCompat.setDecorFitsSystemWindows(context.window, true)
|
||||
// }
|
||||
// }
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
backgroundColor = Color(0xfff8f9f9),
|
||||
navigationIcon = {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize(),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = SafeR.ic_back),
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.size(30.dp)
|
||||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = rememberRipple(bounded = false),
|
||||
) {
|
||||
navigator.popBackStack()
|
||||
}
|
||||
)
|
||||
}
|
||||
},
|
||||
title = { Text(text = "关于") }
|
||||
)
|
||||
},
|
||||
content = { contentPadding ->
|
||||
Column(
|
||||
Modifier
|
||||
.padding(contentPadding)
|
||||
.padding(10.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp)
|
||||
) {
|
||||
Text(text = "版本代码: " + BuildConfig.VERSION_CODE)
|
||||
Text(text = "版本名称: " + BuildConfig.VERSION_NAME)
|
||||
Text(text = "构建时间: " + BuildConfig.BUILD_DATE)
|
||||
Text(text = "构建类型: " + BuildConfig.BUILD_TYPE)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
}
|
|
@ -16,6 +16,7 @@ import androidx.compose.foundation.lazy.LazyColumn
|
|||
import androidx.compose.foundation.shape.GenericShape
|
||||
import androidx.compose.material.Switch
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
|
@ -30,82 +31,69 @@ import androidx.compose.ui.platform.LocalDensity
|
|||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import com.google.accompanist.placeholder.PlaceholderHighlight
|
||||
import com.google.accompanist.placeholder.material.fade
|
||||
import com.google.accompanist.placeholder.material.placeholder
|
||||
import kotlinx.coroutines.delay
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.annotation.RootNavGraph
|
||||
import kotlinx.serialization.encodeToString
|
||||
import li.songe.gkd.data.SubsConfig
|
||||
import li.songe.gkd.data.SubscriptionRaw
|
||||
import li.songe.gkd.db.table.SubsConfig
|
||||
import li.songe.gkd.db.util.Operator.eq
|
||||
import li.songe.gkd.db.util.RoomX
|
||||
import li.songe.gkd.ui.component.StatusBar
|
||||
import li.songe.gkd.util.ThrottleState
|
||||
import li.songe.router.LocalRoute
|
||||
import li.songe.router.LocalRouter
|
||||
import li.songe.router.Page
|
||||
|
||||
data class AppItemPageParams(
|
||||
val subsApp: SubscriptionRaw.AppRaw,
|
||||
val subsConfig: SubsConfig,
|
||||
val appName: String,
|
||||
)
|
||||
|
||||
val AppItemPage = Page {
|
||||
|
||||
// https://developer.android.com/jetpack/compose/modifiers-list
|
||||
|
||||
val router = LocalRouter.current
|
||||
|
||||
val params = LocalRoute.current.data as AppItemPageParams
|
||||
import li.songe.gkd.data.getAppInfo
|
||||
import li.songe.gkd.db.DbSet
|
||||
import li.songe.gkd.utils.Singleton
|
||||
import li.songe.gkd.utils.launchAsFn
|
||||
|
||||
@RootNavGraph
|
||||
@Destination
|
||||
@Composable
|
||||
fun AppItemPage(
|
||||
subsApp: SubscriptionRaw.AppRaw,
|
||||
subsConfig: SubsConfig,
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
// val context = LocalContext.current
|
||||
|
||||
var subsConfigList: List<SubsConfig?>? by remember { mutableStateOf(null) }
|
||||
|
||||
val changeItemThrottle = ThrottleState.use(scope)
|
||||
var subsConfigs: List<SubsConfig?>? by remember { mutableStateOf(null) }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
delay(400)
|
||||
val config = params.subsConfig
|
||||
val mutableSet =
|
||||
RoomX.select { (SubsConfig::type eq SubsConfig.GroupType) and (SubsConfig::subsItemId eq config.subsItemId) and (SubsConfig::appId eq config.appId) }
|
||||
.toMutableSet()
|
||||
val mutableSet = DbSet.subsConfigDao.queryGroupTypeConfig(subsConfig.subsItemId, subsApp.id)
|
||||
val list = mutableListOf<SubsConfig?>()
|
||||
params.subsApp.groups.forEach { group ->
|
||||
subsApp.groups.forEach { group ->
|
||||
if (group.key == null) {
|
||||
list.add(null)
|
||||
} else {
|
||||
val item = mutableSet.find { s -> s.groupKey == group.key } ?: SubsConfig(
|
||||
subsItemId = config.subsItemId,
|
||||
appId = config.appId,
|
||||
val item = mutableSet.find { s -> s.groupKey == group.key }
|
||||
?: SubsConfig(
|
||||
subsItemId = subsConfig.subsItemId,
|
||||
appId = subsConfig.appId,
|
||||
groupKey = group.key,
|
||||
type = SubsConfig.GroupType
|
||||
)
|
||||
list.add(item)
|
||||
}
|
||||
}
|
||||
subsConfigList = list
|
||||
subsConfigs = list
|
||||
}
|
||||
|
||||
var showGroupItem: SubscriptionRaw.GroupRaw? by remember { mutableStateOf(null) }
|
||||
|
||||
LazyColumn(modifier = Modifier.fillMaxSize()) {
|
||||
item {
|
||||
Column {
|
||||
StatusBar()
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(10.dp, 0.dp)
|
||||
) {
|
||||
Text(
|
||||
text = params.appName,
|
||||
text = getAppInfo(subsApp.id).name ?: "-",
|
||||
maxLines = 1,
|
||||
softWrap = false,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
Spacer(modifier = Modifier.width(10.dp))
|
||||
Text(
|
||||
text = params.subsApp.id,
|
||||
text = subsApp.id,
|
||||
maxLines = 1,
|
||||
softWrap = false,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
|
@ -113,20 +101,13 @@ val AppItemPage = Page {
|
|||
}
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
}
|
||||
}
|
||||
|
||||
items(params.subsApp.groups.size) { i ->
|
||||
val group = params.subsApp.groups[i]
|
||||
items(subsApp.groups.size) { i ->
|
||||
val group = subsApp.groups[i]
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.clickable {
|
||||
// router.navigate(
|
||||
// GroupItemPage, GroupItemPage.Params(
|
||||
// group = group,
|
||||
// subsConfig = subsConfigList?.get(i),
|
||||
// appName = params.appName
|
||||
// )
|
||||
// )
|
||||
showGroupItem = group
|
||||
}
|
||||
.padding(10.dp, 6.dp)
|
||||
.fillMaxWidth()
|
||||
|
@ -149,7 +130,7 @@ val AppItemPage = Page {
|
|||
.fillMaxWidth()
|
||||
)
|
||||
Text(
|
||||
text = group.activityIds?.joinToString() ?: "",
|
||||
text = group.desc ?: "-",
|
||||
maxLines = 1,
|
||||
softWrap = false,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
|
@ -163,10 +144,10 @@ val AppItemPage = Page {
|
|||
if (group.key != null) {
|
||||
val crPx = with(LocalDensity.current) { 4.dp.toPx() }
|
||||
Switch(
|
||||
checked = subsConfigList?.get(i)?.enable ?: true,
|
||||
checked = subsConfigs?.get(i)?.enable != false,
|
||||
modifier = Modifier
|
||||
.placeholder(
|
||||
subsConfigList == null,
|
||||
subsConfigs == null,
|
||||
highlight = PlaceholderHighlight.fade(),
|
||||
shape = GenericShape { size, _ ->
|
||||
val cr = CornerRadius(crPx, crPx)
|
||||
|
@ -184,16 +165,12 @@ val AppItemPage = Page {
|
|||
)
|
||||
}
|
||||
),
|
||||
// 当 onCheckedChange 是 null 时, size 是长方形, 反之是 正方形
|
||||
onCheckedChange = changeItemThrottle.invoke { enable ->
|
||||
val list = subsConfigList ?: return@invoke
|
||||
val newItem = list[i]?.copy(enable = enable) ?: return@invoke
|
||||
if (newItem.id == 0L) {
|
||||
RoomX.insert(newItem)
|
||||
} else {
|
||||
RoomX.update(newItem)
|
||||
}
|
||||
subsConfigList = list.toMutableList().apply {
|
||||
onCheckedChange = scope.launchAsFn { enable ->
|
||||
val subsConfigsVal = subsConfigs ?: return@launchAsFn
|
||||
val newItem =
|
||||
subsConfigsVal[i]?.copy(enable = enable) ?: return@launchAsFn
|
||||
DbSet.subsConfigDao.insert(newItem)
|
||||
subsConfigs = subsConfigsVal.toMutableList().apply {
|
||||
set(i, newItem)
|
||||
}
|
||||
}
|
||||
|
@ -210,5 +187,15 @@ val AppItemPage = Page {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
showGroupItem?.let { showGroupItemVal ->
|
||||
Dialog(onDismissRequest = { showGroupItem = null }) {
|
||||
Text(
|
||||
text = Singleton.json.encodeToString(showGroupItemVal),
|
||||
modifier = Modifier.width(400.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,151 +0,0 @@
|
|||
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.provider.Settings
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.text.selection.SelectionContainer
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.blankj.utilcode.util.ToastUtils
|
||||
import com.dylanc.activityresult.launcher.launchForResult
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import li.songe.gkd.accessibility.GkdAbService
|
||||
import li.songe.gkd.debug.FloatingService
|
||||
import li.songe.gkd.debug.ScreenshotService
|
||||
import li.songe.gkd.debug.HttpService
|
||||
import li.songe.gkd.ui.component.StatusBar
|
||||
import li.songe.gkd.ui.component.TextSwitch
|
||||
import li.songe.gkd.util.Ext
|
||||
import li.songe.gkd.util.Ext.LocalLauncher
|
||||
import li.songe.gkd.util.Ext.usePollState
|
||||
import li.songe.gkd.util.Storage
|
||||
import li.songe.router.Page
|
||||
|
||||
|
||||
val DebugPage = Page {
|
||||
val context = LocalContext.current as ComponentActivity
|
||||
val launcher = LocalLauncher.current
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
val httpServerRunning by usePollState { HttpService.isRunning() }
|
||||
val screenshotRunning by usePollState { ScreenshotService.isRunning() }
|
||||
val gkdAccessRunning by usePollState { GkdAbService.isRunning() }
|
||||
val floatingRunning by usePollState {
|
||||
FloatingService.isRunning() && Settings.canDrawOverlays(
|
||||
context
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
val debugAvailable by remember {
|
||||
derivedStateOf { httpServerRunning }
|
||||
}
|
||||
|
||||
val serverUrl by remember {
|
||||
derivedStateOf {
|
||||
if (debugAvailable) {
|
||||
Ext.getIpAddressInLocalNetwork()
|
||||
.map { host -> "http://${host}:${Storage.settings.httpServerPort}" }
|
||||
.joinToString("\n")
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.verticalScroll(
|
||||
state = rememberScrollState()
|
||||
)
|
||||
.padding(20.dp)
|
||||
) {
|
||||
StatusBar()
|
||||
|
||||
Text("调试模式需要WIFI和另一台设备\n您可以一台设备开热点,另一台设备连入\n满足以上外部条件后, 本机需要开启以下服务")
|
||||
|
||||
TextSwitch("HTTP服务(需WIFI)", httpServerRunning) {
|
||||
if (it) {
|
||||
HttpService.start()
|
||||
} else {
|
||||
HttpService.stop()
|
||||
}
|
||||
}
|
||||
|
||||
TextSwitch("截屏服务", screenshotRunning) {
|
||||
if (it) {
|
||||
scope.launch {
|
||||
val mediaProjectionManager =
|
||||
context.getSystemService(Context.MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
|
||||
val activityResult =
|
||||
launcher.launchForResult(mediaProjectionManager.createScreenCaptureIntent())
|
||||
if (activityResult.resultCode == Activity.RESULT_OK && activityResult.data != null) {
|
||||
ScreenshotService.start(intent = activityResult.data!!)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ScreenshotService.stop()
|
||||
}
|
||||
}
|
||||
|
||||
TextSwitch("无障碍服务", gkdAccessRunning) {
|
||||
if (it) {
|
||||
scope.launch {
|
||||
ToastUtils.showShort("请先启动无障碍服务")
|
||||
delay(500)
|
||||
val intent = Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS)
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
context.startActivity(intent)
|
||||
}
|
||||
} else {
|
||||
ToastUtils.showLong("无障碍服务不可在调试模式中关闭")
|
||||
}
|
||||
}
|
||||
|
||||
TextSwitch("悬浮窗服务", floatingRunning) {
|
||||
if (it) {
|
||||
if (Settings.canDrawOverlays(context)) {
|
||||
val intent = Intent(context, FloatingService::class.java)
|
||||
ContextCompat.startForegroundService(context, intent)
|
||||
} else {
|
||||
val intent = Intent(
|
||||
Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
|
||||
Uri.parse("package:$context.packageName")
|
||||
)
|
||||
launcher.launch(intent) { resultCode, _ ->
|
||||
if (resultCode != ComponentActivity.RESULT_OK) return@launch
|
||||
if (!Settings.canDrawOverlays(context)) return@launch
|
||||
val intent1 = Intent(context, FloatingService::class.java)
|
||||
ContextCompat.startForegroundService(context, intent1)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
FloatingService.stop(context)
|
||||
}
|
||||
}
|
||||
|
||||
if (debugAvailable && serverUrl != null) {
|
||||
Text("调试模式可用, 请使用同一局域网的另一台设备打开链接")
|
||||
SelectionContainer {
|
||||
Text("长按可复制: " + serverUrl!!)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
50
app/src/main/java/li/songe/gkd/ui/ImagePreviewPage.kt
Normal file
50
app/src/main/java/li/songe/gkd/ui/ImagePreviewPage.kt
Normal file
|
@ -0,0 +1,50 @@
|
|||
package li.songe.gkd.ui
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.runtime.Composable
|
||||
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.Modifier
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import com.blankj.utilcode.util.ToastUtils
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.annotation.RootNavGraph
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.withContext
|
||||
import li.songe.gkd.utils.LaunchedEffectTry
|
||||
|
||||
@RootNavGraph
|
||||
@Destination
|
||||
@Composable
|
||||
fun ImagePreviewPage(
|
||||
filePath: String?
|
||||
) {
|
||||
val context = LocalContext.current as ComponentActivity
|
||||
val scope = rememberCoroutineScope()
|
||||
var bitmap by remember {
|
||||
mutableStateOf<Bitmap?>(null)
|
||||
}
|
||||
LaunchedEffectTry {
|
||||
if (filePath != null) {
|
||||
bitmap = withContext(IO) { BitmapFactory.decodeFile(filePath) }
|
||||
} else {
|
||||
ToastUtils.showShort("图片路径缺失")
|
||||
}
|
||||
}
|
||||
|
||||
bitmap?.let { bitmapVal ->
|
||||
Image(
|
||||
bitmap = bitmapVal.asImageBitmap(),
|
||||
contentDescription = null,
|
||||
Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
}
|
13
app/src/main/java/li/songe/gkd/ui/RecordPage.kt
Normal file
13
app/src/main/java/li/songe/gkd/ui/RecordPage.kt
Normal file
|
@ -0,0 +1,13 @@
|
|||
package li.songe.gkd.ui
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.annotation.RootNavGraph
|
||||
import li.songe.gkd.db.DbSet
|
||||
|
||||
@RootNavGraph
|
||||
@Destination
|
||||
@Composable
|
||||
fun RecordPage() {
|
||||
DbSet.triggerLogDao
|
||||
}
|
163
app/src/main/java/li/songe/gkd/ui/SnapshotPage.kt
Normal file
163
app/src/main/java/li/songe/gkd/ui/SnapshotPage.kt
Normal file
|
@ -0,0 +1,163 @@
|
|||
package li.songe.gkd.ui
|
||||
|
||||
import android.content.Intent
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
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.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.core.content.FileProvider
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.annotation.RootNavGraph
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.withContext
|
||||
import li.songe.gkd.data.Snapshot
|
||||
import li.songe.gkd.db.DbSet
|
||||
import li.songe.gkd.debug.SnapshotExt
|
||||
import li.songe.gkd.ui.component.StatusBar
|
||||
import li.songe.gkd.utils.launchAsFn
|
||||
import li.songe.gkd.utils.Singleton
|
||||
|
||||
@RootNavGraph
|
||||
@Destination
|
||||
@Composable
|
||||
fun SnapshotPage() {
|
||||
val context = LocalContext.current as ComponentActivity
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
var snapshots by remember {
|
||||
mutableStateOf(listOf<Snapshot>())
|
||||
}
|
||||
var selectedSnapshot by remember {
|
||||
mutableStateOf<Snapshot?>(null)
|
||||
}
|
||||
LaunchedEffect(Unit) {
|
||||
DbSet.snapshotDao.query().flowOn(Dispatchers.IO).collect {
|
||||
snapshots = it.reversed()
|
||||
}
|
||||
}
|
||||
LazyColumn(
|
||||
modifier = Modifier.padding(10.dp, 0.dp, 10.dp, 0.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp),
|
||||
) {
|
||||
item {
|
||||
Text(text = "存在 ${snapshots.size} 条快照记录")
|
||||
}
|
||||
items(snapshots.size) { i ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.border(BorderStroke(1.dp, Color.Black))
|
||||
.clickable {
|
||||
selectedSnapshot = snapshots[i]
|
||||
}
|
||||
) {
|
||||
Row {
|
||||
Text(
|
||||
text = Singleton.simpleDateFormat.format(snapshots[i].id),
|
||||
fontFamily = FontFamily.Monospace
|
||||
)
|
||||
Spacer(modifier = Modifier.width(10.dp))
|
||||
Text(text = snapshots[i].appName ?: "")
|
||||
}
|
||||
Spacer(modifier = Modifier.width(10.dp))
|
||||
Text(text = snapshots[i].appId ?: "")
|
||||
Spacer(modifier = Modifier.width(10.dp))
|
||||
Text(text = snapshots[i].activityId ?: "")
|
||||
}
|
||||
}
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
}
|
||||
}
|
||||
selectedSnapshot?.let { snapshot ->
|
||||
Dialog(
|
||||
onDismissRequest = { selectedSnapshot = null }
|
||||
) {
|
||||
Box(
|
||||
Modifier
|
||||
.width(200.dp)
|
||||
.background(Color.White)
|
||||
.padding(8.dp)
|
||||
) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
val modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(8.dp)
|
||||
Text(
|
||||
text = "查看", modifier = Modifier
|
||||
.clickable(onClick = scope.launchAsFn {
|
||||
// router.navigate(
|
||||
// ImagePreviewPage,
|
||||
// SnapshotExt.getScreenshotPath(snapshot.id)
|
||||
// )
|
||||
selectedSnapshot = null
|
||||
})
|
||||
.then(modifier)
|
||||
)
|
||||
Text(
|
||||
text = "分享", modifier = Modifier
|
||||
.clickable(onClick = scope.launchAsFn {
|
||||
val zipFile = SnapshotExt.getSnapshotZipFile(snapshot.id)
|
||||
val uri = FileProvider.getUriForFile(
|
||||
context,
|
||||
"${context.packageName}.provider",
|
||||
zipFile
|
||||
)
|
||||
val intent = Intent().apply {
|
||||
action = Intent.ACTION_SEND
|
||||
putExtra(Intent.EXTRA_STREAM, uri)
|
||||
type = "application/zip"
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
}
|
||||
context.startActivity(Intent.createChooser(intent, "分享zip文件"))
|
||||
})
|
||||
.then(modifier)
|
||||
)
|
||||
Text(
|
||||
text = "删除", modifier = Modifier
|
||||
.clickable(onClick = scope.launchAsFn {
|
||||
DbSet.snapshotDao.delete(snapshot)
|
||||
withContext(IO) {
|
||||
SnapshotExt.remove(snapshot.id)
|
||||
}
|
||||
selectedSnapshot = null
|
||||
})
|
||||
.then(modifier)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1,162 +1,96 @@
|
|||
package li.songe.gkd.ui
|
||||
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.content.pm.PackageManager
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.wrapContentSize
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.Composable
|
||||
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.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
import com.google.accompanist.placeholder.PlaceholderHighlight
|
||||
import com.google.accompanist.placeholder.material.fade
|
||||
import com.google.accompanist.placeholder.material.placeholder
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.annotation.RootNavGraph
|
||||
import com.ramcosta.composedestinations.navigation.navigate
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.cancellable
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.withContext
|
||||
import li.songe.gkd.R
|
||||
import li.songe.gkd.data.SubsConfig
|
||||
import li.songe.gkd.data.SubsItem
|
||||
import li.songe.gkd.data.SubscriptionRaw
|
||||
import li.songe.gkd.db.table.SubsConfig
|
||||
import li.songe.gkd.db.table.SubsItem
|
||||
import li.songe.gkd.db.util.Operator.eq
|
||||
import li.songe.gkd.db.util.RoomX
|
||||
import li.songe.gkd.ui.component.StatusBar
|
||||
import li.songe.gkd.data.getAppInfo
|
||||
import li.songe.gkd.db.DbSet
|
||||
import li.songe.gkd.ui.component.SubsAppCard
|
||||
import li.songe.gkd.ui.component.SubsAppCardData
|
||||
import li.songe.gkd.util.Ext.getApplicationInfoExt
|
||||
import li.songe.gkd.util.Status
|
||||
import li.songe.gkd.util.ThrottleState
|
||||
import li.songe.router.LocalRoute
|
||||
import li.songe.router.LocalRouter
|
||||
import li.songe.router.Page
|
||||
import java.io.File
|
||||
|
||||
|
||||
val SubsPage = Page {
|
||||
val router = LocalRouter.current
|
||||
val subsItem = LocalRoute.current.data as SubsItem
|
||||
import li.songe.gkd.ui.destinations.AppItemPageDestination
|
||||
import li.songe.gkd.utils.LaunchedEffectTry
|
||||
import li.songe.gkd.utils.LocalNavController
|
||||
import li.songe.gkd.utils.launchAsFn
|
||||
import li.songe.gkd.utils.rememberCache
|
||||
import li.songe.gkd.utils.useTask
|
||||
|
||||
@RootNavGraph
|
||||
@Destination
|
||||
@Composable
|
||||
fun SubsPage(
|
||||
subsItem: SubsItem
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
val context = LocalContext.current
|
||||
val navController = LocalNavController.current
|
||||
|
||||
var sub: SubscriptionRaw? by remember { mutableStateOf(null) }
|
||||
var subStatus: Status<SubscriptionRaw> by remember { mutableStateOf(Status.Progress()) }
|
||||
var subsAppCardDataList: List<SubsAppCardData>? by remember { mutableStateOf(null) }
|
||||
val placeholderList: List<SubsAppCardData> = remember {
|
||||
mutableListOf<SubsAppCardData>().apply {
|
||||
repeat(5) {
|
||||
add(
|
||||
SubsAppCardData(
|
||||
appName = "" + it,
|
||||
icon = ResourcesCompat.getDrawable(
|
||||
context.resources,
|
||||
R.drawable.ic_app_2,
|
||||
context.theme
|
||||
)!!,
|
||||
subsConfig = SubsConfig(
|
||||
subsItemId = it.toLong(),
|
||||
appId = "" + it
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
var subsAppCardDataListStatus: Status<List<SubsAppCardData>> by remember {
|
||||
mutableStateOf(Status.Progress())
|
||||
}
|
||||
var sub: SubscriptionRaw? by rememberCache { mutableStateOf(null) }
|
||||
var subsAppCards: List<SubsAppCardData>? by rememberCache { mutableStateOf(null) }
|
||||
|
||||
val changeItemThrottle = ThrottleState.use(scope)
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
val st = System.currentTimeMillis()
|
||||
val file = File(subsItem.filePath)
|
||||
if (!(file.exists() && file.isFile)) {
|
||||
subStatus = Status.Error("在本地存储没有找到订阅文件")
|
||||
return@LaunchedEffect
|
||||
LaunchedEffectTry(Unit) {
|
||||
scope.launchAsFn { }
|
||||
val newSub = if (sub === null) {
|
||||
SubscriptionRaw.parse5(subsItem.subsFile.readText()).apply {
|
||||
withContext(IO) {
|
||||
apps.forEach {
|
||||
getAppInfo(it.id)
|
||||
}
|
||||
val rawText = try {
|
||||
withContext(IO) { file.readText() }
|
||||
} catch (e: Exception) {
|
||||
subStatus = Status.Error("读取文件失败:$e")
|
||||
return@LaunchedEffect
|
||||
}
|
||||
val newSub = try {
|
||||
SubscriptionRaw.parse5(rawText)
|
||||
} catch (e: Exception) {
|
||||
subStatus = Status.Error("序列化失败:$e")
|
||||
return@LaunchedEffect
|
||||
}
|
||||
subStatus = Status.Success(newSub)
|
||||
|
||||
val mutableSet =
|
||||
RoomX.select { (SubsConfig::type eq SubsConfig.AppType) and (SubsConfig::subsItemId eq subsItem.id) }
|
||||
.toMutableSet()
|
||||
|
||||
val packageManager = context.packageManager
|
||||
val defaultIcon = ResourcesCompat.getDrawable(
|
||||
context.resources,
|
||||
R.drawable.ic_app_2,
|
||||
context.theme
|
||||
)!!
|
||||
val defaultName = "-"
|
||||
val newSubsAppCardDataList = (subStatus as Status.Success).value.apps.map { appRaw ->
|
||||
} else {
|
||||
sub!!
|
||||
}
|
||||
sub = newSub
|
||||
DbSet.subsConfigDao.queryAppTypeConfig(subsItem.id).flowOn(IO).cancellable().collect {
|
||||
val mutableSet = it.toMutableSet()
|
||||
val newSubsAppCards = newSub.apps.map { appRaw ->
|
||||
mutableSet.firstOrNull { v ->
|
||||
v.appId == appRaw.id
|
||||
}.apply {
|
||||
if (this != null) {
|
||||
mutableSet.remove(this)
|
||||
}
|
||||
} ?: SubsConfig(
|
||||
subsItemId = subsItem.id,
|
||||
appId = appRaw.id,
|
||||
type = SubsConfig.AppType
|
||||
)
|
||||
}.map { subsConfig ->
|
||||
async(IO) {
|
||||
val info: ApplicationInfo = try {
|
||||
packageManager.getApplicationInfoExt(
|
||||
subsConfig.appId,
|
||||
PackageManager.GET_META_DATA
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
return@async SubsAppCardData(
|
||||
defaultName,
|
||||
defaultIcon,
|
||||
subsConfig
|
||||
}.mapIndexed { index, subsConfig ->
|
||||
SubsAppCardData(
|
||||
subsConfig,
|
||||
newSub.apps[index]
|
||||
)
|
||||
}
|
||||
return@async SubsAppCardData(
|
||||
packageManager.getApplicationLabel(info).toString(),
|
||||
packageManager.getApplicationIcon(info),
|
||||
subsConfig
|
||||
)
|
||||
subsAppCards = newSubsAppCards
|
||||
}
|
||||
}.awaitAll()
|
||||
subsAppCardDataListStatus = Status.Success(newSubsAppCardDataList)
|
||||
delay(400 - (System.currentTimeMillis() - st))
|
||||
sub = newSub
|
||||
subsAppCardDataList = newSubsAppCardDataList
|
||||
}
|
||||
|
||||
val openAppPage = scope.useTask().launchAsFn<SubsAppCardData> {
|
||||
navController.navigate(AppItemPageDestination(it.appRaw, it.subsConfig))
|
||||
}
|
||||
|
||||
LazyColumn(
|
||||
|
@ -165,8 +99,6 @@ val SubsPage = Page {
|
|||
.fillMaxSize()
|
||||
) {
|
||||
item {
|
||||
Column {
|
||||
StatusBar()
|
||||
val textModifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.placeholder(visible = sub == null, highlight = PlaceholderHighlight.fade())
|
||||
|
@ -188,90 +120,37 @@ val SubsPage = Page {
|
|||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
subsAppCardDataList?.let { cardDataList ->
|
||||
items(cardDataList.size) { i ->
|
||||
AnimatedVisibility(visible = true) {
|
||||
Box(modifier = Modifier
|
||||
.wrapContentSize()
|
||||
.clickable {
|
||||
router.navigate(
|
||||
AppItemPage,
|
||||
AppItemPageParams(
|
||||
sub?.apps?.get(i)!!,
|
||||
cardDataList[i].subsConfig,
|
||||
cardDataList[i].appName
|
||||
)
|
||||
)
|
||||
}) {
|
||||
subsAppCards?.let { subsAppCardsVal ->
|
||||
items(subsAppCardsVal.size) { i ->
|
||||
SubsAppCard(
|
||||
sub = cardDataList[i],
|
||||
onValueChange = changeItemThrottle.invoke { enable ->
|
||||
val newItem = cardDataList[i].subsConfig.copy(
|
||||
sub = subsAppCardsVal[i],
|
||||
onClick = {
|
||||
openAppPage(subsAppCardsVal[i])
|
||||
},
|
||||
onValueChange = scope.launchAsFn { enable ->
|
||||
val newItem = subsAppCardsVal[i].subsConfig.copy(
|
||||
enable = enable
|
||||
)
|
||||
if (newItem.id == 0L) {
|
||||
RoomX.insert(newItem)
|
||||
} else {
|
||||
RoomX.update(newItem)
|
||||
}
|
||||
subsAppCardDataList = cardDataList.toMutableList().apply {
|
||||
set(i, cardDataList[i].copy(subsConfig = newItem))
|
||||
}
|
||||
DbSet.subsConfigDao.insert(newItem)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (subsAppCardDataList == null) {
|
||||
items(placeholderList.size) { i ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.wrapContentSize()
|
||||
) {
|
||||
SubsAppCard(loading = true, sub = placeholderList[i])
|
||||
Text(text = "")
|
||||
}
|
||||
}
|
||||
}
|
||||
// if (subsAppCards == null) {
|
||||
// items(placeholderList.size) { i ->
|
||||
// Box(
|
||||
// modifier = Modifier
|
||||
// .wrapContentSize()
|
||||
// ) {
|
||||
// SubsAppCard(loading = true, sub = placeholderList[i])
|
||||
// Text(text = "")
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
item(true) {
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
if (subStatus !is Status.Success || subsAppCardDataListStatus !is Status.Success) {
|
||||
when (val s = subStatus) {
|
||||
is Status.Success -> {
|
||||
when (val s2 = subsAppCardDataListStatus) {
|
||||
is Status.Error -> {
|
||||
Dialog(onDismissRequest = { router.back() }) {
|
||||
Text(text = s2.value.toString())
|
||||
}
|
||||
}
|
||||
|
||||
is Status.Progress -> {
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
is Status.Error -> {
|
||||
Dialog(onDismissRequest = { router.back() }) {
|
||||
Text(text = s.value.toString())
|
||||
}
|
||||
}
|
||||
|
||||
is Status.Progress -> {
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
32
app/src/main/java/li/songe/gkd/ui/component/SnapshotCard.kt
Normal file
32
app/src/main/java/li/songe/gkd/ui/component/SnapshotCard.kt
Normal file
|
@ -0,0 +1,32 @@
|
|||
package li.songe.gkd.ui.component
|
||||
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Composable
|
||||
fun SnapshotCard() {
|
||||
Row {
|
||||
Text(text = "06-02 20:47:48")
|
||||
Spacer(modifier = Modifier.size(10.dp))
|
||||
Text(text = "酷安")
|
||||
Spacer(modifier = Modifier.size(10.dp))
|
||||
Text(text = "查看")
|
||||
Spacer(modifier = Modifier.size(10.dp))
|
||||
Text(text = "分享")
|
||||
Spacer(modifier = Modifier.size(10.dp))
|
||||
Text(text = "删除")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun PreviewSnapshotCard() {
|
||||
SnapshotCard()
|
||||
}
|
|
@ -12,7 +12,7 @@ import com.blankj.utilcode.util.BarUtils
|
|||
import com.blankj.utilcode.util.ConvertUtils
|
||||
|
||||
@Composable
|
||||
fun StatusBar(color: Color = Color.White) {
|
||||
fun StatusBar(color: Color = Color.Transparent) {
|
||||
Spacer(
|
||||
modifier = Modifier
|
||||
.height(statusBarHeight)
|
||||
|
|
|
@ -2,6 +2,7 @@ package li.songe.gkd.ui.component
|
|||
|
||||
import android.graphics.drawable.Drawable
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
|
@ -18,31 +19,43 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.google.accompanist.drawablepainter.rememberDrawablePainter
|
||||
import com.google.accompanist.placeholder.PlaceholderHighlight
|
||||
import com.google.accompanist.placeholder.material.fade
|
||||
import com.google.accompanist.placeholder.material.placeholder
|
||||
import li.songe.gkd.db.table.SubsConfig
|
||||
import li.songe.gkd.R
|
||||
import li.songe.gkd.data.SubsConfig
|
||||
import li.songe.gkd.data.SubscriptionRaw
|
||||
import li.songe.gkd.data.getAppInfo
|
||||
import li.songe.gkd.utils.SafeR
|
||||
|
||||
|
||||
@Composable
|
||||
fun SubsAppCard(
|
||||
loading: Boolean = false,
|
||||
sub: SubsAppCardData,
|
||||
onClick: (() -> Unit)? = null,
|
||||
onValueChange: ((Boolean) -> Unit)? = null
|
||||
) {
|
||||
val info = getAppInfo(sub.appRaw.id)
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.height(60.dp)
|
||||
.clickable {
|
||||
onClick?.invoke()
|
||||
}
|
||||
.padding(4.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
|
||||
Image(
|
||||
painter = rememberDrawablePainter(sub.icon),
|
||||
painter = if (info.icon != null) rememberDrawablePainter(info.icon) else painterResource(
|
||||
SafeR.ic_app_2
|
||||
),
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.fillMaxHeight()
|
||||
|
@ -60,7 +73,7 @@ fun SubsAppCard(
|
|||
verticalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text(
|
||||
text = sub.appName, maxLines = 1,
|
||||
text = info.name ?: "-", maxLines = 1,
|
||||
softWrap = false,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier
|
||||
|
@ -68,7 +81,7 @@ fun SubsAppCard(
|
|||
.placeholder(loading, highlight = PlaceholderHighlight.fade())
|
||||
)
|
||||
Text(
|
||||
text = sub.subsConfig.appId, maxLines = 1,
|
||||
text = sub.appRaw.groups.size.toString() + "组规则", maxLines = 1,
|
||||
softWrap = false,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier
|
||||
|
@ -88,8 +101,7 @@ fun SubsAppCard(
|
|||
}
|
||||
|
||||
data class SubsAppCardData(
|
||||
val appName: String,
|
||||
val icon: Drawable,
|
||||
val subsConfig: SubsConfig,
|
||||
val appRaw: SubscriptionRaw.AppRaw
|
||||
)
|
||||
|
||||
|
|
|
@ -21,15 +21,14 @@ import androidx.compose.ui.res.painterResource
|
|||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import li.songe.gkd.R
|
||||
import li.songe.gkd.db.table.SubsItem
|
||||
import li.songe.gkd.util.Singleton
|
||||
import li.songe.gkd.data.SubsItem
|
||||
import li.songe.gkd.utils.SafeR
|
||||
import li.songe.gkd.utils.Singleton
|
||||
|
||||
@Composable
|
||||
fun SubsItemCard(
|
||||
subsItem: SubsItem,
|
||||
onShareClick: (() -> Unit)? = null,
|
||||
onEditClick: (() -> Unit)? = null,
|
||||
onDelClick: (() -> Unit)? = null,
|
||||
onRefreshClick: (() -> Unit)? = null,
|
||||
) {
|
||||
|
@ -49,16 +48,25 @@ fun SubsItemCard(
|
|||
softWrap = false,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
Row {
|
||||
Text(
|
||||
text = dateStr,
|
||||
maxLines = 1,
|
||||
softWrap = false,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
Spacer(modifier = Modifier.width(10.dp))
|
||||
Text(
|
||||
text = "版本:" + subsItem.version,
|
||||
maxLines = 1,
|
||||
softWrap = false,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.width(5.dp))
|
||||
Image(
|
||||
painter = painterResource(R.drawable.ic_refresh),
|
||||
painter = painterResource(SafeR.ic_refresh),
|
||||
contentDescription = "refresh",
|
||||
modifier = Modifier
|
||||
.clickable {
|
||||
|
@ -69,7 +77,7 @@ fun SubsItemCard(
|
|||
)
|
||||
Spacer(modifier = Modifier.width(5.dp))
|
||||
Image(
|
||||
painter = painterResource(R.drawable.ic_share),
|
||||
painter = painterResource(SafeR.ic_share),
|
||||
contentDescription = "share",
|
||||
modifier = Modifier
|
||||
.clickable {
|
||||
|
@ -80,18 +88,7 @@ fun SubsItemCard(
|
|||
)
|
||||
Spacer(modifier = Modifier.width(5.dp))
|
||||
Image(
|
||||
painter = painterResource(R.drawable.ic_create_round),
|
||||
contentDescription = "edit",
|
||||
modifier = Modifier
|
||||
.clickable {
|
||||
onEditClick?.invoke()
|
||||
}
|
||||
.padding(4.dp)
|
||||
.size(20.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(5.dp))
|
||||
Image(
|
||||
painter = painterResource(R.drawable.ic_del),
|
||||
painter = painterResource(SafeR.ic_del),
|
||||
contentDescription = "edit",
|
||||
modifier = Modifier
|
||||
.clickable {
|
||||
|
@ -109,7 +106,6 @@ fun PreviewSubscriptionItemCard() {
|
|||
Surface(modifier = Modifier.width(300.dp)) {
|
||||
SubsItemCard(
|
||||
SubsItem(
|
||||
filePath = "filepath",
|
||||
updateUrl = "https://raw.githubusercontents.com/lisonge/gkd-subscription/main/src/ad-startup.gkd.json",
|
||||
name = "APP工具箱"
|
||||
)
|
||||
|
|
|
@ -1,33 +1,41 @@
|
|||
package li.songe.gkd.ui.component
|
||||
|
||||
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.width
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.material.Switch
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
@Composable
|
||||
fun TextSwitch(
|
||||
text: String,
|
||||
checked: Boolean,
|
||||
name: String = "",
|
||||
desc: String = "",
|
||||
checked: Boolean = true,
|
||||
onCheckedChange: ((Boolean) -> Unit)? = null,
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
val animatedColor = (
|
||||
Color(
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
(0xFF * (if (checked) 1f else .3f)).toInt()
|
||||
)
|
||||
)
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text,
|
||||
color = animatedColor
|
||||
name,
|
||||
fontSize = 18.sp
|
||||
)
|
||||
Spacer(modifier = Modifier.height(2.dp))
|
||||
Text(
|
||||
desc,
|
||||
fontSize = 14.sp
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.width(10.dp))
|
||||
Switch(
|
||||
checked,
|
||||
onCheckedChange,
|
||||
|
@ -38,7 +46,7 @@ fun TextSwitch(
|
|||
@Preview
|
||||
@Composable
|
||||
fun PreviewTextSwitch() {
|
||||
Surface {
|
||||
TextSwitch("text", true)
|
||||
Surface(modifier = Modifier.width(300.dp)) {
|
||||
TextSwitch("隐藏后台", "在最近任务列表中隐藏", true)
|
||||
}
|
||||
}
|
|
@ -2,6 +2,7 @@ package li.songe.gkd.ui.home
|
|||
|
||||
import androidx.annotation.DrawableRes
|
||||
import li.songe.gkd.R
|
||||
import li.songe.gkd.utils.SafeR
|
||||
|
||||
data class BottomNavItem(
|
||||
val label: String,
|
||||
|
@ -13,12 +14,12 @@ data class BottomNavItem(
|
|||
val BottomNavItems = listOf(
|
||||
BottomNavItem(
|
||||
label = "订阅",
|
||||
icon = R.drawable.ic_link,
|
||||
icon = SafeR.ic_link,
|
||||
route = "subscription"
|
||||
),
|
||||
BottomNavItem(
|
||||
label = "设置",
|
||||
icon = R.drawable.ic_cog,
|
||||
icon = SafeR.ic_cog,
|
||||
route = "settings"
|
||||
),
|
||||
)
|
|
@ -1,47 +1,33 @@
|
|||
package li.songe.gkd.ui.home
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.Scaffold
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.zIndex
|
||||
import li.songe.gkd.ui.component.StatusBar
|
||||
import li.songe.gkd.util.ModifierExt.noRippleClickable
|
||||
import li.songe.router.Page
|
||||
import com.ramcosta.composedestinations.annotation.Destination
|
||||
import com.ramcosta.composedestinations.annotation.RootNavGraph
|
||||
import li.songe.gkd.utils.LocalStateCache
|
||||
import li.songe.gkd.utils.StateCache
|
||||
import li.songe.gkd.utils.rememberCache
|
||||
|
||||
val HomePage = Page {
|
||||
var tabInt by remember { mutableStateOf(0) }
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
StatusBar()
|
||||
Scaffold(
|
||||
bottomBar = { BottomNavigationBar(tabInt) { tabInt = it } },
|
||||
content = { padding ->
|
||||
@RootNavGraph(start = true)
|
||||
@Destination
|
||||
@Composable
|
||||
fun HomePage() {
|
||||
var tabIndex by rememberCache { mutableStateOf(0) }
|
||||
val subsStateCache = rememberCache { StateCache() }
|
||||
val settingStateCache = rememberCache { StateCache() }
|
||||
Scaffold(bottomBar = { BottomNavigationBar(tabIndex) { tabIndex = it } }, content = { padding ->
|
||||
Box(modifier = Modifier.padding(padding)) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.alpha(if (tabInt == 0) 1f else 0f)
|
||||
.zIndex(if (tabInt == 0) 1f else 0f)
|
||||
.noRippleClickable { }) {
|
||||
SubscriptionManagePage()
|
||||
}
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.alpha(if (tabInt == 1) 1f else 0f)
|
||||
.zIndex(if (tabInt == 1) 1f else 0f)
|
||||
.noRippleClickable { }) {
|
||||
SettingsPage()
|
||||
when (tabIndex) {
|
||||
0 -> CompositionLocalProvider(LocalStateCache provides subsStateCache) { SubscriptionManagePage() }
|
||||
1 -> CompositionLocalProvider(LocalStateCache provides settingStateCache) { SettingsPage() }
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
|
@ -17,6 +17,7 @@ import androidx.compose.ui.draw.clip
|
|||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import li.songe.gkd.R
|
||||
import li.songe.gkd.utils.SafeR
|
||||
|
||||
@Composable
|
||||
fun NativePage() {
|
||||
|
@ -28,7 +29,7 @@ fun NativePage() {
|
|||
modifier = Modifier.height(40.dp)
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(R.drawable.ic_app_2),
|
||||
painter = painterResource(SafeR.ic_app_2),
|
||||
contentDescription = "",
|
||||
modifier = Modifier
|
||||
.fillMaxHeight()
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package li.songe.gkd.ui.home
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.BottomNavigation
|
||||
import androidx.compose.material.BottomNavigationItem
|
||||
|
@ -43,11 +44,13 @@ import androidx.compose.ui.unit.dp
|
|||
@Composable
|
||||
fun BottomNavigationBar(tabInt: Int, onTabChange: ((Int) -> Unit)? = null) {
|
||||
BottomNavigation(
|
||||
backgroundColor = Color.White,
|
||||
backgroundColor = Color.Transparent,
|
||||
elevation = 0.dp
|
||||
) {
|
||||
BottomNavItems.forEachIndexed { i, navItem ->
|
||||
BottomNavigationItem(
|
||||
selected = i == tabInt,
|
||||
modifier = Modifier.background(Color.Transparent),
|
||||
onClick = {
|
||||
onTabChange?.invoke(i)
|
||||
},
|
||||
|
|
|
@ -1,6 +1,16 @@
|
|||
package li.songe.gkd.ui.home
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.media.projection.MediaProjectionManager
|
||||
import android.net.Uri
|
||||
import android.provider.Settings
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
|
@ -10,21 +20,43 @@ import androidx.compose.runtime.Composable
|
|||
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.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.blankj.utilcode.util.LogUtils
|
||||
import com.blankj.utilcode.util.ToastUtils
|
||||
import com.dylanc.activityresult.launcher.launchForResult
|
||||
import kotlinx.coroutines.delay
|
||||
import li.songe.gkd.MainActivity
|
||||
import li.songe.gkd.R
|
||||
import li.songe.gkd.ui.DebugPage
|
||||
import li.songe.gkd.accessibility.GkdAbService
|
||||
import li.songe.gkd.debug.FloatingService
|
||||
import li.songe.gkd.debug.HttpService
|
||||
import li.songe.gkd.debug.ScreenshotService
|
||||
import li.songe.gkd.ui.component.TextSwitch
|
||||
import li.songe.gkd.util.LocaleString.Companion.localeString
|
||||
import li.songe.gkd.util.Storage
|
||||
import li.songe.router.LocalRouter
|
||||
import li.songe.gkd.utils.Ext
|
||||
import li.songe.gkd.utils.LocalLauncher
|
||||
import li.songe.gkd.utils.LocalNavController
|
||||
import li.songe.gkd.utils.Storage
|
||||
import li.songe.gkd.utils.launchAsFn
|
||||
import li.songe.gkd.utils.usePollState
|
||||
import li.songe.gkd.utils.useTask
|
||||
import rikka.shizuku.Shizuku
|
||||
import com.ramcosta.composedestinations.navigation.navigate
|
||||
import li.songe.gkd.shizuku.shizukuIsSafeOK
|
||||
import li.songe.gkd.ui.destinations.AboutPageDestination
|
||||
import li.songe.gkd.ui.destinations.SnapshotPageDestination
|
||||
|
||||
@Composable
|
||||
fun SettingsPage() {
|
||||
val context = LocalContext.current as MainActivity
|
||||
val launcher = LocalLauncher.current
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
val navController = LocalNavController.current
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.verticalScroll(
|
||||
|
@ -32,37 +64,154 @@ fun SettingsPage() {
|
|||
)
|
||||
.padding(20.dp, 0.dp)
|
||||
) {
|
||||
val gkdAccessRunning by usePollState { GkdAbService.isRunning() }
|
||||
TextSwitch("无障碍授权",
|
||||
"用于获取屏幕信息,点击屏幕上的控件",
|
||||
gkdAccessRunning,
|
||||
onCheckedChange = scope.launchAsFn<Boolean> {
|
||||
if (!it) return@launchAsFn
|
||||
ToastUtils.showShort("请先启动无障碍服务")
|
||||
delay(500)
|
||||
val intent = Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS)
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
context.startActivity(intent)
|
||||
})
|
||||
|
||||
val context = LocalContext.current as MainActivity
|
||||
// val composableScope = rememberCoroutineScope()
|
||||
val router = LocalRouter.current
|
||||
|
||||
val shizukuIsOk by usePollState { shizukuIsSafeOK() }
|
||||
TextSwitch("Shizuku授权",
|
||||
"高级运行模式,能更准确识别界面活动ID",
|
||||
shizukuIsOk,
|
||||
onCheckedChange = scope.launchAsFn<Boolean> {
|
||||
if (!it) return@launchAsFn
|
||||
try {
|
||||
Shizuku.requestPermission(Activity.RESULT_OK)
|
||||
} catch (e: Exception) {
|
||||
ToastUtils.showShort("Shizuku可能没有运行")
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
val canDrawOverlays by usePollState {
|
||||
Settings.canDrawOverlays(context)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(5.dp))
|
||||
TextSwitch("悬浮窗授权",
|
||||
"用于后台提示,主动保存快照等功能",
|
||||
canDrawOverlays,
|
||||
onCheckedChange = scope.launchAsFn<Boolean> {
|
||||
if (!Settings.canDrawOverlays(context)) {
|
||||
val intent = Intent(
|
||||
Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
|
||||
Uri.parse("package:$context.packageName")
|
||||
)
|
||||
launcher.launch(intent) { resultCode, _ ->
|
||||
if (resultCode != ComponentActivity.RESULT_OK) return@launch
|
||||
if (!Settings.canDrawOverlays(context)) return@launch
|
||||
val intent1 = Intent(context, FloatingService::class.java)
|
||||
ContextCompat.startForegroundService(context, intent1)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
Spacer(modifier = Modifier.height(15.dp))
|
||||
|
||||
val httpServerRunning by usePollState { HttpService.isRunning() }
|
||||
TextSwitch("HTTP服务",
|
||||
"开启HTTP服务, 以便在同一局域网下传递数据" + if (httpServerRunning) "\n${
|
||||
Ext.getIpAddressInLocalNetwork()
|
||||
.map { host -> "http://${host}:${Storage.settings.httpServerPort}" }
|
||||
.joinToString(",")
|
||||
}" else "\n暂无地址",
|
||||
httpServerRunning) {
|
||||
if (it) {
|
||||
HttpService.start()
|
||||
} else {
|
||||
HttpService.stop()
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(5.dp))
|
||||
|
||||
val screenshotRunning by usePollState { ScreenshotService.isRunning() }
|
||||
TextSwitch("截屏服务",
|
||||
"生成快照需要截取屏幕,Android>=11无需开启",
|
||||
screenshotRunning,
|
||||
scope.launchAsFn<Boolean> {
|
||||
if (it) {
|
||||
val mediaProjectionManager =
|
||||
context.getSystemService(Context.MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
|
||||
val activityResult =
|
||||
launcher.launchForResult(mediaProjectionManager.createScreenCaptureIntent())
|
||||
if (activityResult.resultCode == Activity.RESULT_OK && activityResult.data != null) {
|
||||
ScreenshotService.start(intent = activityResult.data!!)
|
||||
}
|
||||
} else {
|
||||
ScreenshotService.stop()
|
||||
}
|
||||
})
|
||||
|
||||
Spacer(modifier = Modifier.height(5.dp))
|
||||
|
||||
val floatingRunning by usePollState {
|
||||
FloatingService.isRunning()
|
||||
}
|
||||
TextSwitch("悬浮窗服务", "便于用户主动保存快照", floatingRunning) {
|
||||
if (it) {
|
||||
if (Settings.canDrawOverlays(context)) {
|
||||
val intent = Intent(context, FloatingService::class.java)
|
||||
ContextCompat.startForegroundService(context, intent)
|
||||
} else {
|
||||
val intent = Intent(
|
||||
Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
|
||||
Uri.parse("package:$context.packageName")
|
||||
)
|
||||
launcher.launch(intent) { resultCode, _ ->
|
||||
if (resultCode != ComponentActivity.RESULT_OK) return@launch
|
||||
if (!Settings.canDrawOverlays(context)) return@launch
|
||||
val intent1 = Intent(context, FloatingService::class.java)
|
||||
ContextCompat.startForegroundService(context, intent1)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
FloatingService.stop(context)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Spacer(modifier = Modifier.height(15.dp))
|
||||
|
||||
var enableService by remember { mutableStateOf(Storage.settings.enableService) }
|
||||
TextSwitch(
|
||||
text = "保持服务${(if (enableService) "开启" else "关闭")}",
|
||||
|
||||
Spacer(modifier = Modifier.height(5.dp))
|
||||
TextSwitch(name = "服务开启",
|
||||
desc = "保持服务开启,根据订阅规则匹配屏幕目标节点",
|
||||
checked = enableService,
|
||||
onCheckedChange = {
|
||||
enableService = it
|
||||
Storage.settings.commit {
|
||||
this.enableService = it
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
Spacer(modifier = Modifier.height(5.dp))
|
||||
|
||||
var excludeFromRecents by remember { mutableStateOf(Storage.settings.excludeFromRecents) }
|
||||
TextSwitch(
|
||||
text = "在[最近任务]界面中隐藏本应用",
|
||||
TextSwitch(name = "隐藏后台",
|
||||
desc = "在[最近任务]界面中隐藏本应用",
|
||||
checked = excludeFromRecents,
|
||||
onCheckedChange = {
|
||||
excludeFromRecents = it
|
||||
Storage.settings.commit {
|
||||
this.excludeFromRecents = it
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
Spacer(modifier = Modifier.height(5.dp))
|
||||
|
||||
var enableConsoleLogOut by remember { mutableStateOf(Storage.settings.enableConsoleLogOut) }
|
||||
TextSwitch(
|
||||
text = "保持日志输出到控制台",
|
||||
TextSwitch(name = "日志输出",
|
||||
desc = "保持日志输出到控制台",
|
||||
checked = enableConsoleLogOut,
|
||||
onCheckedChange = {
|
||||
enableConsoleLogOut = it
|
||||
|
@ -70,24 +219,49 @@ fun SettingsPage() {
|
|||
Storage.settings.commit {
|
||||
this.enableConsoleLogOut = it
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
Spacer(modifier = Modifier.height(5.dp))
|
||||
|
||||
var notificationVisible by remember { mutableStateOf(Storage.settings.notificationVisible) }
|
||||
TextSwitch(text = "通知栏显示", checked = notificationVisible,
|
||||
TextSwitch(name = "通知栏显示",
|
||||
desc = "通知栏显示可以降低系统杀后台的概率",
|
||||
checked = notificationVisible,
|
||||
onCheckedChange = {
|
||||
notificationVisible = it
|
||||
Storage.settings.commit {
|
||||
this.notificationVisible = it
|
||||
}
|
||||
})
|
||||
Spacer(modifier = Modifier.height(5.dp))
|
||||
|
||||
Button(onClick = {
|
||||
router.navigate(DebugPage)
|
||||
var enableScreenshot by remember {
|
||||
mutableStateOf(Storage.settings.enableCaptureSystemScreenshot)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(5.dp))
|
||||
TextSwitch(
|
||||
"自动快照", "当用户截屏时,自动保存当前界面的快照,目前仅支持miui", enableScreenshot
|
||||
) {
|
||||
enableScreenshot = it
|
||||
Storage.settings.commit {
|
||||
enableCaptureSystemScreenshot = it
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(5.dp))
|
||||
Button(onClick = scope.useTask().launchAsFn {
|
||||
navController.navigate(SnapshotPageDestination)
|
||||
}) {
|
||||
Text(text = "调试模式")
|
||||
Text(text = "查看快照记录")
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(5.dp))
|
||||
|
||||
Button(onClick = scope.useTask().launchAsFn {
|
||||
navController.navigate(AboutPageDestination)
|
||||
}) {
|
||||
Text(text = "查看关于")
|
||||
}
|
||||
|
||||
Text(text = "多语言自动切换:" + localeString(R.string.app_name))
|
||||
}
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
package li.songe.gkd.ui.home
|
||||
|
||||
import android.webkit.URLUtil
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
|
@ -19,59 +20,127 @@ import androidx.compose.ui.res.painterResource
|
|||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import com.blankj.utilcode.util.ClipboardUtils
|
||||
import com.blankj.utilcode.util.PathUtils
|
||||
import com.blankj.utilcode.util.ToastUtils
|
||||
import com.google.zxing.BarcodeFormat
|
||||
import com.ramcosta.composedestinations.navigation.navigate
|
||||
import io.ktor.client.request.*
|
||||
import io.ktor.client.statement.*
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.withContext
|
||||
import li.songe.gkd.R
|
||||
import li.songe.gkd.data.SubsItem
|
||||
import li.songe.gkd.data.SubscriptionRaw
|
||||
import li.songe.gkd.db.table.SubsConfig
|
||||
import li.songe.gkd.db.table.SubsItem
|
||||
import li.songe.gkd.db.util.Operator.eq
|
||||
import li.songe.gkd.db.util.RoomX
|
||||
import li.songe.gkd.hooks.useNavigateForQrcodeResult
|
||||
import li.songe.gkd.ui.SubsPage
|
||||
import li.songe.gkd.db.DbSet
|
||||
import li.songe.gkd.ui.component.SubsItemCard
|
||||
import li.songe.gkd.util.Ext.launchTry
|
||||
import li.songe.gkd.util.Singleton
|
||||
import li.songe.gkd.util.ThrottleState
|
||||
import li.songe.router.LocalRouter
|
||||
import java.io.File
|
||||
import li.songe.gkd.ui.destinations.SubsPageDestination
|
||||
import li.songe.gkd.utils.LaunchedEffectTry
|
||||
import li.songe.gkd.utils.LocalNavController
|
||||
import li.songe.gkd.utils.SafeR
|
||||
import li.songe.gkd.utils.Singleton
|
||||
import li.songe.gkd.utils.launchAsFn
|
||||
import li.songe.gkd.utils.rememberCache
|
||||
import li.songe.gkd.utils.useNavigateForQrcodeResult
|
||||
import li.songe.gkd.utils.useTask
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun SubscriptionManagePage() {
|
||||
val scope = rememberCoroutineScope()
|
||||
val router = LocalRouter.current
|
||||
val navController = LocalNavController.current
|
||||
|
||||
var subItemList by remember { mutableStateOf(listOf<SubsItem>()) }
|
||||
var shareSubItem: SubsItem? by remember { mutableStateOf(null) }
|
||||
var shareQrcode: ImageBitmap? by remember { mutableStateOf(null) }
|
||||
var deleteSubItem: SubsItem? by remember { mutableStateOf(null) }
|
||||
var subItems by rememberCache { mutableStateOf(listOf<SubsItem>()) }
|
||||
var shareSubItem: SubsItem? by rememberCache { mutableStateOf(null) }
|
||||
var shareQrcode: ImageBitmap? by rememberCache { mutableStateOf(null) }
|
||||
var deleteSubItem: SubsItem? by rememberCache { mutableStateOf(null) }
|
||||
var moveSubItem: SubsItem? by rememberCache { mutableStateOf(null) }
|
||||
|
||||
var showAddDialog by rememberCache { mutableStateOf(false) }
|
||||
|
||||
var showLinkDialog by rememberCache { mutableStateOf(false) }
|
||||
var link by rememberCache { mutableStateOf("") }
|
||||
|
||||
var showAddDialog by remember { mutableStateOf(false) }
|
||||
var showLinkInputDialog by remember { mutableStateOf(false) }
|
||||
val viewSubItemThrottle = ThrottleState.use(scope)
|
||||
val editSubItemThrottle = ThrottleState.use(scope)
|
||||
val refreshSubItemThrottle = ThrottleState.use(scope, 250)
|
||||
val navigateForQrcodeResult = useNavigateForQrcodeResult()
|
||||
|
||||
var linkText by remember {
|
||||
mutableStateOf("")
|
||||
LaunchedEffectTry(Unit) {
|
||||
DbSet.subsItemDao.query().flowOn(IO).collect {
|
||||
subItems = it
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
subItemList = RoomX.select<SubsItem>().sortedBy { it.index }
|
||||
val addSubs = scope.useTask(dialog = true).launchAsFn<List<String>> { urls ->
|
||||
val safeUrls = urls.filter { url ->
|
||||
URLUtil.isNetworkUrl(url) && subItems.all { it.updateUrl != url }
|
||||
}
|
||||
if (safeUrls.isEmpty()) return@launchAsFn
|
||||
onChangeLoading(true)
|
||||
val newItems = safeUrls.mapIndexedNotNull { index, url ->
|
||||
try {
|
||||
val text = Singleton.client.get(url).bodyAsText()
|
||||
val subscriptionRaw = SubscriptionRaw.parse5(text)
|
||||
val newItem = SubsItem(
|
||||
updateUrl = subscriptionRaw.updateUrl ?: url,
|
||||
name = subscriptionRaw.name,
|
||||
version = subscriptionRaw.version,
|
||||
order = index + 1 + subItems.size
|
||||
)
|
||||
withContext(IO) {
|
||||
newItem.subsFile.writeText(text)
|
||||
}
|
||||
newItem
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
if (newItems.isNotEmpty()) {
|
||||
DbSet.subsItemDao.insert(*newItems.toTypedArray())
|
||||
ToastUtils.showShort("成功添加 ${newItems.size} 条订阅")
|
||||
} else {
|
||||
ToastUtils.showShort("添加失败")
|
||||
}
|
||||
}
|
||||
|
||||
val updateSubs = scope.useTask(dialog = true).launchAsFn<List<SubsItem>> { oldItems ->
|
||||
if (oldItems.isEmpty()) return@launchAsFn
|
||||
onChangeLoading(true)
|
||||
val newItems = oldItems.mapNotNull { oldItem ->
|
||||
try {
|
||||
val subscriptionRaw = SubscriptionRaw.parse5(
|
||||
Singleton.client.get(oldItem.updateUrl).bodyAsText()
|
||||
)
|
||||
if (subscriptionRaw.version <= oldItem.version) {
|
||||
return@mapNotNull null
|
||||
}
|
||||
val newItem = oldItem.copy(
|
||||
updateUrl = subscriptionRaw.updateUrl ?: oldItem.updateUrl,
|
||||
name = subscriptionRaw.name,
|
||||
mtime = System.currentTimeMillis(),
|
||||
version = subscriptionRaw.version
|
||||
)
|
||||
withContext(IO) {
|
||||
newItem.subsFile.writeText(
|
||||
SubscriptionRaw.stringify(
|
||||
subscriptionRaw
|
||||
)
|
||||
)
|
||||
}
|
||||
newItem
|
||||
} catch (e: Exception) {
|
||||
ToastUtils.showShort(e.message)
|
||||
null
|
||||
}
|
||||
}
|
||||
if (newItems.isEmpty()) {
|
||||
ToastUtils.showShort("暂无更新")
|
||||
} else {
|
||||
DbSet.subsItemDao.update(*newItems.toTypedArray())
|
||||
ToastUtils.showShort("更新 ${newItems.size} 条订阅")
|
||||
}
|
||||
}
|
||||
|
||||
LazyColumn(
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp), modifier = Modifier.fillMaxHeight()
|
||||
) {
|
||||
item(subItemList) {
|
||||
item(subItems) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
|
@ -79,11 +148,17 @@ fun SubscriptionManagePage() {
|
|||
.fillMaxWidth()
|
||||
.padding(10.dp, 0.dp)
|
||||
) {
|
||||
if (subItems.isEmpty()) {
|
||||
Text(
|
||||
text = "共有${subItemList.size}条订阅,激活:${subItemList.count { it.enable }},禁用:${subItemList.count { !it.enable }}",
|
||||
text = "暂无订阅",
|
||||
)
|
||||
} else {
|
||||
Text(
|
||||
text = "共有${subItems.size}条订阅,激活:${subItems.count { it.enable }},禁用:${subItems.count { !it.enable }}",
|
||||
)
|
||||
}
|
||||
Row {
|
||||
Image(painter = painterResource(R.drawable.ic_add),
|
||||
Image(painter = painterResource(SafeR.ic_add),
|
||||
contentDescription = "",
|
||||
modifier = Modifier
|
||||
.clickable {
|
||||
|
@ -91,98 +166,51 @@ fun SubscriptionManagePage() {
|
|||
}
|
||||
.padding(4.dp)
|
||||
.size(25.dp))
|
||||
Image(painter = painterResource(R.drawable.ic_refresh),
|
||||
Image(
|
||||
painter = painterResource(SafeR.ic_refresh),
|
||||
contentDescription = "",
|
||||
modifier = Modifier
|
||||
.clickable {
|
||||
scope.launchTry {
|
||||
subItemList.mapIndexed { i, oldItem ->
|
||||
val subscriptionRaw = SubscriptionRaw.parse5(
|
||||
Singleton.client
|
||||
.get(oldItem.updateUrl)
|
||||
.bodyAsText()
|
||||
)
|
||||
if (subscriptionRaw.version <= oldItem.version) {
|
||||
ToastUtils.showShort("暂无更新:${oldItem.name}")
|
||||
return@mapIndexed
|
||||
}
|
||||
val newItem = oldItem.copy(
|
||||
updateUrl = subscriptionRaw.updateUrl
|
||||
?: oldItem.updateUrl,
|
||||
name = subscriptionRaw.name,
|
||||
mtime = System.currentTimeMillis(),
|
||||
version = subscriptionRaw.version
|
||||
)
|
||||
RoomX.update(newItem)
|
||||
File(newItem.filePath).writeText(
|
||||
SubscriptionRaw.stringify(
|
||||
subscriptionRaw
|
||||
)
|
||||
)
|
||||
ToastUtils.showShort("更新成功:${newItem.name}")
|
||||
subItemList = subItemList
|
||||
.toMutableList()
|
||||
.also {
|
||||
it[i] = newItem
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.clickable(onClick = {
|
||||
updateSubs(subItems)
|
||||
})
|
||||
.padding(4.dp)
|
||||
.size(25.dp))
|
||||
.size(25.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
items(subItemList.size) { i ->
|
||||
items(subItems.size) { i ->
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.animateItemPlacement()
|
||||
.padding(vertical = 3.dp, horizontal = 8.dp)
|
||||
.clickable(onClick = { router.navigate(SubsPage, subItemList[i]) }),
|
||||
.combinedClickable(
|
||||
onClick = scope
|
||||
.useTask()
|
||||
.launchAsFn {
|
||||
navController.navigate(SubsPageDestination(subItems[i]))
|
||||
}, onLongClick = {
|
||||
if (subItems.size > 1) {
|
||||
moveSubItem = subItems[i]
|
||||
}
|
||||
}),
|
||||
elevation = 0.dp,
|
||||
border = BorderStroke(1.dp, Color(0xfff6f6f6)),
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
) {
|
||||
SubsItemCard(subItemList[i], onShareClick = {
|
||||
shareSubItem = subItemList[i]
|
||||
}, onEditClick = editSubItemThrottle.invoke {
|
||||
SubsItemCard(subItems[i], onShareClick = {
|
||||
shareSubItem = subItems[i]
|
||||
}, onDelClick = {
|
||||
deleteSubItem = subItemList[i]
|
||||
}, onRefreshClick = refreshSubItemThrottle.invoke {
|
||||
val oldItem = subItemList[i]
|
||||
val subscriptionRaw = SubscriptionRaw.parse5(
|
||||
Singleton.client.get(oldItem.updateUrl).bodyAsText()
|
||||
)
|
||||
if (subscriptionRaw.version <= oldItem.version) {
|
||||
ToastUtils.showShort("暂无更新:${oldItem.name}")
|
||||
return@invoke
|
||||
}
|
||||
val newItem = oldItem.copy(
|
||||
updateUrl = subscriptionRaw.updateUrl
|
||||
?: oldItem.updateUrl,
|
||||
name = subscriptionRaw.name,
|
||||
mtime = System.currentTimeMillis(),
|
||||
version = subscriptionRaw.version
|
||||
)
|
||||
RoomX.update(newItem)
|
||||
withContext(IO) {
|
||||
File(newItem.filePath).writeText(SubscriptionRaw.stringify(subscriptionRaw))
|
||||
}
|
||||
subItemList = subItemList.toMutableList().also {
|
||||
it[i] = newItem
|
||||
}
|
||||
ToastUtils.showShort("更新成功:${newItem.name}")
|
||||
}.catch {
|
||||
if (!it.message.isNullOrEmpty()) {
|
||||
ToastUtils.showShort(it.message)
|
||||
}
|
||||
deleteSubItem = subItems[i]
|
||||
}, onRefreshClick = {
|
||||
updateSubs(listOf(subItems[i]))
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
shareSubItem?.let { _shareSubItem ->
|
||||
shareSubItem?.let { shareSubItemVal ->
|
||||
Dialog(onDismissRequest = { shareSubItem = null }) {
|
||||
Box(
|
||||
Modifier
|
||||
|
@ -191,26 +219,22 @@ fun SubscriptionManagePage() {
|
|||
.padding(8.dp)
|
||||
) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Text(text = "二维码",
|
||||
modifier = Modifier
|
||||
Text(text = "二维码", modifier = Modifier
|
||||
.clickable {
|
||||
shareQrcode = Singleton.barcodeEncoder
|
||||
.encodeBitmap(
|
||||
_shareSubItem.updateUrl,
|
||||
BarcodeFormat.QR_CODE,
|
||||
500,
|
||||
500
|
||||
shareSubItemVal.updateUrl, BarcodeFormat.QR_CODE, 500, 500
|
||||
)
|
||||
.asImageBitmap()
|
||||
shareSubItem = null
|
||||
}
|
||||
.fillMaxWidth()
|
||||
.padding(8.dp))
|
||||
Text(text = "导出至剪切板",
|
||||
modifier = Modifier
|
||||
Text(text = "导出至剪切板", modifier = Modifier
|
||||
.clickable {
|
||||
ClipboardUtils.copyText(_shareSubItem.updateUrl)
|
||||
ClipboardUtils.copyText(shareSubItemVal.updateUrl)
|
||||
shareSubItem = null
|
||||
ToastUtils.showShort("复制成功")
|
||||
}
|
||||
.fillMaxWidth()
|
||||
.padding(8.dp))
|
||||
|
@ -219,36 +243,85 @@ fun SubscriptionManagePage() {
|
|||
}
|
||||
}
|
||||
|
||||
shareQrcode?.let { _shareQrcode ->
|
||||
shareQrcode?.let { shareQrcodeVal ->
|
||||
Dialog(onDismissRequest = { shareQrcode = null }) {
|
||||
Image(
|
||||
bitmap = _shareQrcode,
|
||||
bitmap = shareQrcodeVal,
|
||||
contentDescription = "qrcode",
|
||||
modifier = Modifier.size(400.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
moveSubItem?.let { moveSubItemVal ->
|
||||
Dialog(onDismissRequest = { moveSubItem = null }) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier = Modifier
|
||||
.width(200.dp)
|
||||
.wrapContentHeight()
|
||||
.background(Color.White)
|
||||
.padding(8.dp)
|
||||
) {
|
||||
if (subItems.firstOrNull() != moveSubItemVal) {
|
||||
Text(
|
||||
text = "上移",
|
||||
modifier = Modifier
|
||||
.clickable(
|
||||
onClick = scope
|
||||
.useTask()
|
||||
.launchAsFn {
|
||||
val lastItem =
|
||||
subItems[subItems.indexOf(moveSubItemVal) - 1]
|
||||
DbSet.subsItemDao.update(
|
||||
lastItem.copy(
|
||||
order = moveSubItemVal.order
|
||||
),
|
||||
moveSubItemVal.copy(
|
||||
order = lastItem.order
|
||||
)
|
||||
)
|
||||
moveSubItem = null
|
||||
})
|
||||
.fillMaxWidth()
|
||||
.padding(8.dp)
|
||||
)
|
||||
}
|
||||
if (subItems.lastOrNull() != moveSubItemVal) {
|
||||
Text(
|
||||
text = "下移",
|
||||
modifier = Modifier
|
||||
.clickable(
|
||||
onClick = scope
|
||||
.useTask()
|
||||
.launchAsFn {
|
||||
val nextItem =
|
||||
subItems[subItems.indexOf(moveSubItemVal) + 1]
|
||||
DbSet.subsItemDao.update(
|
||||
nextItem.copy(
|
||||
order = moveSubItemVal.order
|
||||
),
|
||||
moveSubItemVal.copy(
|
||||
order = nextItem.order
|
||||
)
|
||||
)
|
||||
moveSubItem = null
|
||||
})
|
||||
.fillMaxWidth()
|
||||
.padding(8.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val delSubItemThrottle = ThrottleState.use(scope)
|
||||
if (deleteSubItem != null) {
|
||||
|
||||
deleteSubItem?.let { deleteSubItemVal ->
|
||||
AlertDialog(onDismissRequest = { deleteSubItem = null },
|
||||
title = { Text(text = "是否删除该项") },
|
||||
confirmButton = {
|
||||
Button(onClick = delSubItemThrottle.invoke {
|
||||
if (deleteSubItem == null) return@invoke
|
||||
deleteSubItem?.let {
|
||||
RoomX.delete(it)
|
||||
RoomX.delete { SubsConfig::subsItemId eq it.id }
|
||||
}
|
||||
withContext(IO) {
|
||||
try {
|
||||
File(deleteSubItem!!.filePath).delete()
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
subItemList = subItemList.toMutableList().also { it.remove(deleteSubItem) }
|
||||
Button(onClick = scope.launchAsFn {
|
||||
deleteSubItemVal.removeAssets()
|
||||
deleteSubItem = null
|
||||
}) {
|
||||
Text("是")
|
||||
|
@ -262,25 +335,40 @@ fun SubscriptionManagePage() {
|
|||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (showAddDialog) {
|
||||
val clickQrcodeThrottle = ThrottleState.use(scope)
|
||||
Dialog(onDismissRequest = { showAddDialog = false }) {
|
||||
Box(
|
||||
Modifier
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.width(250.dp)
|
||||
.background(Color.White)
|
||||
.padding(8.dp)
|
||||
.padding(8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Text(
|
||||
text = "默认订阅", modifier = Modifier
|
||||
.clickable(onClick = {
|
||||
showAddDialog = false
|
||||
addSubs(
|
||||
listOf(
|
||||
"https://cdn.lisonge.com/startup_ad.json",
|
||||
"https://cdn.lisonge.com/internal_ad.json",
|
||||
"https://cdn.lisonge.com/quick_util.json",
|
||||
)
|
||||
)
|
||||
})
|
||||
.fillMaxWidth()
|
||||
.padding(8.dp)
|
||||
)
|
||||
Text(
|
||||
text = "二维码", modifier = Modifier
|
||||
.clickable(onClick = clickQrcodeThrottle.invoke {
|
||||
.clickable(onClick = scope.launchAsFn {
|
||||
showAddDialog = false
|
||||
val qrCode = navigateForQrcodeResult()
|
||||
val contents = qrCode.contents
|
||||
if (contents != null) {
|
||||
showLinkInputDialog = true
|
||||
linkText = contents
|
||||
showLinkDialog = true
|
||||
link = contents
|
||||
}
|
||||
})
|
||||
.fillMaxWidth()
|
||||
|
@ -288,7 +376,7 @@ fun SubscriptionManagePage() {
|
|||
)
|
||||
Text(text = "链接", modifier = Modifier
|
||||
.clickable {
|
||||
showLinkInputDialog = true
|
||||
showLinkDialog = true
|
||||
showAddDialog = false
|
||||
}
|
||||
.fillMaxWidth()
|
||||
|
@ -296,64 +384,30 @@ fun SubscriptionManagePage() {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
LaunchedEffect(showLinkDialog) {
|
||||
if (!showLinkDialog) {
|
||||
link = ""
|
||||
}
|
||||
if (showLinkInputDialog) {
|
||||
Dialog(onDismissRequest = { showLinkInputDialog = false;linkText = "" }) {
|
||||
}
|
||||
if (showLinkDialog) {
|
||||
Dialog(onDismissRequest = { showLinkDialog = false }) {
|
||||
Box(
|
||||
Modifier
|
||||
.width(250.dp)
|
||||
.width(300.dp)
|
||||
.background(Color.White)
|
||||
.padding(8.dp)
|
||||
) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Text(text = "请输入订阅链接")
|
||||
TextField(
|
||||
value = linkText,
|
||||
onValueChange = { linkText = it },
|
||||
singleLine = true
|
||||
value = link, onValueChange = { link = it.trim() }, singleLine = true
|
||||
)
|
||||
Button(onClick = {
|
||||
showLinkInputDialog = false
|
||||
if (subItemList.any { it.updateUrl == linkText }) {
|
||||
ToastUtils.showShort("该链接已经添加过")
|
||||
return@Button
|
||||
}
|
||||
scope.launch {
|
||||
try {
|
||||
val text = Singleton.client.get(linkText).bodyAsText()
|
||||
val subscriptionRaw = SubscriptionRaw.parse5(text)
|
||||
File(
|
||||
PathUtils.getExternalAppFilesPath()
|
||||
.plus("/subscription/")
|
||||
).apply {
|
||||
if (!exists()) {
|
||||
mkdir()
|
||||
}
|
||||
}
|
||||
val file = File(
|
||||
PathUtils.getExternalAppFilesPath()
|
||||
.plus("/subscription/")
|
||||
.plus(System.currentTimeMillis())
|
||||
.plus(".json")
|
||||
)
|
||||
withContext(IO) {
|
||||
file.writeText(text)
|
||||
}
|
||||
val tempItem = SubsItem(
|
||||
updateUrl = subscriptionRaw.updateUrl ?: linkText,
|
||||
filePath = file.absolutePath,
|
||||
name = subscriptionRaw.name,
|
||||
version = subscriptionRaw.version
|
||||
)
|
||||
val newItem = tempItem.copy(
|
||||
id = RoomX.insert(tempItem)[0]
|
||||
)
|
||||
subItemList = subItemList.toMutableList().apply { add(newItem) }
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
ToastUtils.showShort(e.message ?: "")
|
||||
}
|
||||
}
|
||||
addSubs(listOf(link))
|
||||
showLinkDialog = false
|
||||
}) {
|
||||
Text(text = "添加")
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ package li.songe.gkd.ui.theme
|
|||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
val Purple200 = Color(0xFFBB86FC)
|
||||
val Purple500 = Color(0xFF6200EE)
|
||||
val Purple700 = Color(0xFF3700B3)
|
||||
val Purple200 = Color(0xFFf8f9f9)
|
||||
val Purple500 = Color(0xFFf2f3f4)
|
||||
val Purple700 = Color(0xFFe5e7e9)
|
||||
val Teal200 = Color(0xFF03DAC5)
|
|
@ -6,29 +6,12 @@ import androidx.compose.material.darkColors
|
|||
import androidx.compose.material.lightColors
|
||||
import androidx.compose.runtime.Composable
|
||||
|
||||
private val DarkColorPalette = darkColors(
|
||||
primary = Purple200,
|
||||
primaryVariant = Purple700,
|
||||
secondary = Teal200
|
||||
)
|
||||
private val DarkColorPalette = darkColors()
|
||||
|
||||
private val LightColorPalette = lightColors(
|
||||
primary = Purple500,
|
||||
primaryVariant = Purple700,
|
||||
secondary = Teal200
|
||||
|
||||
/* Other default colors to override
|
||||
background = Color.White,
|
||||
surface = Color.White,
|
||||
onPrimary = Color.White,
|
||||
onSecondary = Color.Black,
|
||||
onBackground = Color.Black,
|
||||
onSurface = Color.Black,
|
||||
*/
|
||||
)
|
||||
private val LightColorPalette = lightColors()
|
||||
|
||||
@Composable
|
||||
fun MainTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable() () -> Unit) {
|
||||
fun AppTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable() () -> Unit) {
|
||||
val colors = if (darkTheme) {
|
||||
DarkColorPalette
|
||||
} else {
|
||||
|
|
|
@ -1,42 +0,0 @@
|
|||
package li.songe.gkd.util
|
||||
|
||||
import android.content.res.Configuration
|
||||
import android.os.LocaleList
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import li.songe.gkd.App
|
||||
import java.util.*
|
||||
|
||||
class LocaleString(private vararg val localeList: Locale) : (Int) -> String {
|
||||
private val languageContext by lazy {
|
||||
App.context.createConfigurationContext(Configuration(App.context.resources.configuration).apply {
|
||||
if (this@LocaleString.localeList.isNotEmpty()) {
|
||||
setLocales(LocaleList(*this@LocaleString.localeList))
|
||||
} else {
|
||||
setLocales(App.context.resources.configuration.locales)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun invoke(@StringRes resId: Int) = languageContext.getString(resId)
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
other as LocaleString
|
||||
if (!localeList.contentEquals(other.localeList)) return false
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
|
||||
return localeList.contentHashCode()
|
||||
}
|
||||
|
||||
companion object {
|
||||
var localeString by mutableStateOf(LocaleString())
|
||||
}
|
||||
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
package li.songe.gkd.util
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.composed
|
||||
|
||||
object ModifierExt {
|
||||
inline fun Modifier.noRippleClickable(crossinline onClick: () -> Unit): Modifier = composed {
|
||||
clickable(indication = null,
|
||||
interactionSource = remember { MutableInteractionSource() }) {
|
||||
onClick()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,22 +0,0 @@
|
|||
package li.songe.gkd.util
|
||||
|
||||
sealed class Status<out T> {
|
||||
object Empty : Status<Nothing>()
|
||||
|
||||
/**
|
||||
* @param value 0f to 1f
|
||||
*/
|
||||
class Progress(val value: Float = 0f) : Status<Nothing>()
|
||||
class Success<T>(val value: T) : Status<T>()
|
||||
class Error(val value: Any?) : Status<Nothing>() {
|
||||
// override fun toString(): String {
|
||||
// val nullMsg = "未知错误"
|
||||
// return when (value) {
|
||||
// null -> nullMsg
|
||||
// is String -> value
|
||||
// is Exception -> value.message ?: nullMsg
|
||||
// else -> value.toString()
|
||||
// }
|
||||
// }
|
||||
}
|
||||
}
|
|
@ -1,109 +0,0 @@
|
|||
package li.songe.gkd.util
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class ThrottleState(
|
||||
private val scope: CoroutineScope,
|
||||
private val miniAwaitTime: Long = 200L,
|
||||
val loading: Boolean = false,
|
||||
private val onChangeLoading: (value: Boolean) -> Unit = {},
|
||||
) {
|
||||
companion object {
|
||||
private lateinit var defaultFalseInstance: ThrottleState
|
||||
|
||||
@Composable
|
||||
fun use(scope: CoroutineScope, miniAwaitTime: Long = 0): ThrottleState {
|
||||
var loading by remember { mutableStateOf(false) }
|
||||
if (loading) {
|
||||
if (!::defaultFalseInstance.isInitialized) {
|
||||
defaultFalseInstance = ThrottleState(scope, miniAwaitTime, loading = true)
|
||||
}
|
||||
return defaultFalseInstance
|
||||
}
|
||||
return ThrottleState(scope, miniAwaitTime, loading = false) {
|
||||
loading = it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class CatchInvoke(
|
||||
private val onChangeCatch: (catchFn: ((e: Exception) -> Unit)) -> Unit,
|
||||
private val fn: () -> Unit,
|
||||
) : () -> Unit {
|
||||
override fun invoke() {
|
||||
fn()
|
||||
}
|
||||
|
||||
fun catch(catchFn: ((e: Exception) -> Unit)): CatchInvoke {
|
||||
onChangeCatch(catchFn)
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
||||
class CatchInvoke1<T>(
|
||||
private val onChangeCatch: (catchFn: ((e: Exception) -> Unit)) -> Unit,
|
||||
private val fn: (T) -> Unit,
|
||||
) : (T) -> Unit {
|
||||
override fun invoke(t: T) {
|
||||
fn(t)
|
||||
}
|
||||
|
||||
fun catch(catchFn: ((e: Exception) -> Unit)): CatchInvoke1<T> {
|
||||
onChangeCatch(catchFn)
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
||||
fun invoke(
|
||||
miniAwaitTime: Long = this.miniAwaitTime,
|
||||
fn: suspend () -> Unit,
|
||||
): CatchInvoke {
|
||||
var catchFn = { e: Exception -> e.printStackTrace() }
|
||||
return CatchInvoke({ catchFn = it }) fnWrapper@{
|
||||
if (loading) return@fnWrapper
|
||||
onChangeLoading(true)
|
||||
scope.launch {
|
||||
try {
|
||||
fn()
|
||||
} catch (e: Exception) {
|
||||
catchFn(e)
|
||||
} finally {
|
||||
delay(miniAwaitTime)
|
||||
onChangeLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
fun <T> invoke(
|
||||
miniAwaitTime: Long = this.miniAwaitTime,
|
||||
fn: suspend (T) -> Unit,
|
||||
): CatchInvoke1<T> {
|
||||
var catchFn = { e: Exception -> e.printStackTrace() }
|
||||
return CatchInvoke1({ catchFn = it }) fnWrapper@{ t ->
|
||||
if (loading) return@fnWrapper
|
||||
onChangeLoading(true)
|
||||
scope.launch {
|
||||
try {
|
||||
fn(t)
|
||||
} catch (e: Exception) {
|
||||
catchFn(e)
|
||||
} finally {
|
||||
delay(miniAwaitTime)
|
||||
onChangeLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -1,20 +0,0 @@
|
|||
package li.songe.gkd.util
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import com.blankj.utilcode.util.ScreenUtils
|
||||
|
||||
object UseHook {
|
||||
var screenWidth by mutableStateOf(ScreenUtils.getAppScreenWidth())
|
||||
var screenHeight by mutableStateOf(ScreenUtils.getAppScreenHeight())
|
||||
var screenOrientationIsLandscape by mutableStateOf(ScreenUtils.isLandscape())
|
||||
// var locale by mutableStateOf(LanguageUtils.getSystemLanguage())
|
||||
|
||||
fun update(newConfig: Configuration) {
|
||||
screenHeight = ScreenUtils.getAppScreenHeight()
|
||||
screenWidth = ScreenUtils.getAppScreenWidth()
|
||||
screenOrientationIsLandscape = ScreenUtils.isLandscape()
|
||||
}
|
||||
}
|
|
@ -1,6 +1,8 @@
|
|||
package li.songe.gkd.util
|
||||
package li.songe.gkd.utils
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
/**
|
||||
|
@ -8,24 +10,24 @@ import kotlinx.parcelize.Parcelize
|
|||
*/
|
||||
@Parcelize
|
||||
data class AppSettings(
|
||||
var ctime: Long = System.currentTimeMillis(),
|
||||
var mtime: Long = System.currentTimeMillis(),
|
||||
var enableService: Boolean = true,
|
||||
var excludeFromRecents: Boolean = true,
|
||||
var notificationVisible: Boolean = true,
|
||||
var enableDebugServer: Boolean = false,
|
||||
var httpServerPort: Int = 8888,
|
||||
var enableConsoleLogOut: Boolean = true,
|
||||
var enableCaptureSystemScreenshot: Boolean = true,
|
||||
var httpServerPort: Int = 8888,
|
||||
) : Parcelable {
|
||||
fun commit(block: AppSettings.() -> Unit) {
|
||||
val backup = copy()
|
||||
block.invoke(this)
|
||||
if (this != backup) {
|
||||
mtime = System.currentTimeMillis()
|
||||
Storage.kv.encode(saveKey, this)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val saveKey = "settings-v1"
|
||||
const val saveKey = "settings-v2"
|
||||
}
|
||||
}
|
||||
|
||||
val appSettingsFlow by lazy { MutableStateFlow(AppSettings()) }
|
50
app/src/main/java/li/songe/gkd/utils/ComposeExt.kt
Normal file
50
app/src/main/java/li/songe/gkd/utils/ComposeExt.kt
Normal file
|
@ -0,0 +1,50 @@
|
|||
package li.songe.gkd.utils
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.compositionLocalOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import com.blankj.utilcode.util.ToastUtils
|
||||
import com.dylanc.activityresult.launcher.StartActivityLauncher
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.isActive
|
||||
|
||||
|
||||
val LocalLauncher =
|
||||
compositionLocalOf<StartActivityLauncher> { error("not found StartActivityLauncher") }
|
||||
|
||||
@Composable
|
||||
fun <T> usePollState(interval: Long = 1000L, getter: () -> T): MutableState<T> {
|
||||
val mutableState = remember { mutableStateOf(getter()) }
|
||||
LaunchedEffect(Unit) {
|
||||
while (isActive) {
|
||||
delay(interval)
|
||||
mutableState.value = getter()
|
||||
}
|
||||
}
|
||||
return mutableState
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LaunchedEffectTry(
|
||||
key1: Any? = null,
|
||||
block: suspend CoroutineScope.() -> Unit
|
||||
) {
|
||||
LaunchedEffect(key1) {
|
||||
try {
|
||||
block()
|
||||
} catch (e: CancellationException) {
|
||||
e.printStackTrace()
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
ToastUtils.showShort(e.message ?: "")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
100
app/src/main/java/li/songe/gkd/utils/CoroutineExt.kt
Normal file
100
app/src/main/java/li/songe/gkd/utils/CoroutineExt.kt
Normal file
|
@ -0,0 +1,100 @@
|
|||
package li.songe.gkd.utils
|
||||
|
||||
import com.blankj.utilcode.util.LogUtils
|
||||
import com.blankj.utilcode.util.ToastUtils
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.CoroutineStart
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.coroutines.EmptyCoroutineContext
|
||||
import kotlin.coroutines.coroutineContext
|
||||
|
||||
|
||||
fun CoroutineScope.launchWhile(
|
||||
context: CoroutineContext = EmptyCoroutineContext,
|
||||
start: CoroutineStart = CoroutineStart.DEFAULT,
|
||||
block: suspend CoroutineScope.() -> Unit,
|
||||
) = launch(context, start) {
|
||||
while (isActive) {
|
||||
block()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun CoroutineScope.launchWhileTry(
|
||||
context: CoroutineContext = EmptyCoroutineContext,
|
||||
start: CoroutineStart = CoroutineStart.DEFAULT,
|
||||
interval: Long = 0,
|
||||
block: suspend CoroutineScope.() -> Unit,
|
||||
) = launch(context, start) {
|
||||
while (isActive) {
|
||||
try {
|
||||
block()
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
delay(interval)
|
||||
}
|
||||
}
|
||||
|
||||
fun CoroutineScope.launchTry(
|
||||
context: CoroutineContext = EmptyCoroutineContext,
|
||||
start: CoroutineStart = CoroutineStart.DEFAULT,
|
||||
block: suspend CoroutineScope.() -> Unit,
|
||||
) = launch(context, start) {
|
||||
try {
|
||||
block()
|
||||
} catch (e: CancellationException) {
|
||||
e.printStackTrace()
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
ToastUtils.showShort(e.message)
|
||||
}
|
||||
}
|
||||
|
||||
fun CoroutineScope.launchAsFn(
|
||||
context: CoroutineContext = EmptyCoroutineContext,
|
||||
start: CoroutineStart = CoroutineStart.DEFAULT,
|
||||
block: suspend CoroutineScope.() -> Unit,
|
||||
): () -> Unit {
|
||||
return {
|
||||
launch(context, start) {
|
||||
try {
|
||||
block()
|
||||
} catch (e: CancellationException) {
|
||||
e.printStackTrace()
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
ToastUtils.showShort(e.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun <T> CoroutineScope.launchAsFn(
|
||||
context: CoroutineContext = EmptyCoroutineContext,
|
||||
start: CoroutineStart = CoroutineStart.DEFAULT,
|
||||
block: suspend CoroutineScope.(T) -> Unit,
|
||||
): (T) -> Unit {
|
||||
return {
|
||||
launch(context, start) {
|
||||
try {
|
||||
block(it)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
ToastUtils.showShort(e.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun stopJob(): Nothing {
|
||||
coroutineContext[Job]?.cancel()
|
||||
delay(1)
|
||||
error("stop failed")
|
||||
}
|
||||
|
79
app/src/main/java/li/songe/gkd/utils/DrawableDsl.kt
Normal file
79
app/src/main/java/li/songe/gkd/utils/DrawableDsl.kt
Normal file
|
@ -0,0 +1,79 @@
|
|||
package li.songe.gkd.utils
|
||||
|
||||
import android.graphics.Path
|
||||
import android.graphics.drawable.ShapeDrawable
|
||||
import android.graphics.drawable.shapes.RectShape
|
||||
import androidx.compose.material.icons.materialIcon
|
||||
import androidx.compose.material.icons.materialPath
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.addPathNodes
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
fun createDrawable(block: () -> Unit) {
|
||||
val s = materialIcon(name = "xx") {
|
||||
addPath(addPathNodes(""))
|
||||
}
|
||||
}
|
||||
|
||||
fun createVectorDrawable(block: () -> Unit) {
|
||||
val path = Path().apply {
|
||||
addPathNodes("").forEach {
|
||||
it.isQuad
|
||||
}
|
||||
}
|
||||
val shapeDrawable = ShapeDrawable(RectShape())
|
||||
shapeDrawable.apply {
|
||||
|
||||
addPathNodes("")[0].isCurve
|
||||
}
|
||||
// val r = Resources()
|
||||
// Drawable.createFromXml()
|
||||
}
|
||||
|
||||
|
||||
val x = createDrawable {
|
||||
// val p =
|
||||
vector {
|
||||
width = 24.dp
|
||||
height = 24.dp
|
||||
viewportWidth = 24F
|
||||
viewportHeight = 24F
|
||||
path {
|
||||
width
|
||||
fillColor = Color(0xFF000000)
|
||||
pathData = "M20,11H7.83l5.59,-5.59L12,4l-8,8l8,8l1.41,-1.41L7.83,13H20v-2z"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface VectorType {
|
||||
var width: Dp
|
||||
var height: Dp
|
||||
var viewportWidth: Float
|
||||
var viewportHeight: Float
|
||||
}
|
||||
|
||||
fun vector(block: VectorType.() -> Unit) {}
|
||||
|
||||
interface PathType {
|
||||
var fillColor: Color
|
||||
var pathData: String
|
||||
}
|
||||
|
||||
fun path(block: PathType.() -> Unit) {}
|
||||
|
||||
fun testDrawable() {
|
||||
|
||||
val s2 = vector {
|
||||
width = 24.dp
|
||||
height = 24.dp
|
||||
viewportWidth = 24F
|
||||
viewportHeight = 24F
|
||||
path {
|
||||
width
|
||||
fillColor = Color(0xFF000000)
|
||||
pathData = "M20,11H7.83l5.59,-5.59L12,4l-8,8l8,8l1.41,-1.41L7.83,13H20v-2z"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package li.songe.gkd.util
|
||||
package li.songe.gkd.utils
|
||||
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
|
@ -16,32 +16,18 @@ import android.os.Looper
|
|||
import androidx.compose.runtime.*
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import com.blankj.utilcode.util.ToastUtils
|
||||
import com.dylanc.activityresult.launcher.StartActivityLauncher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.CoroutineStart
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import androidx.core.graphics.drawable.IconCompat
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import li.songe.gkd.App
|
||||
import li.songe.gkd.MainActivity
|
||||
import li.songe.gkd.R
|
||||
import li.songe.gkd.data.RuleManager
|
||||
import li.songe.gkd.data.SubscriptionRaw
|
||||
import li.songe.gkd.db.table.SubsItem
|
||||
import li.songe.gkd.db.util.RoomX
|
||||
import li.songe.gkd.shizuku.Shell
|
||||
import li.songe.gkd.shizuku.ShizukuShell
|
||||
import java.io.File
|
||||
import li.songe.gkd.db.DbSet
|
||||
import li.songe.gkd.icon.AddIcon
|
||||
import java.net.NetworkInterface
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.coroutines.EmptyCoroutineContext
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
|
||||
object Ext {
|
||||
fun PackageManager.getApplicationInfoExt(
|
||||
packageName: String,
|
||||
|
@ -120,7 +106,7 @@ object Ext {
|
|||
)
|
||||
|
||||
val builder = NotificationCompat.Builder(context, channelId)
|
||||
.setSmallIcon(R.drawable.ic_app_2)
|
||||
.setSmallIcon(SafeR.ic_launcher)
|
||||
.setContentTitle("调试模式")
|
||||
.setContentText("正在录制您的屏幕内容")
|
||||
.setContentIntent(pendingIntent)
|
||||
|
@ -160,90 +146,14 @@ object Ext {
|
|||
}
|
||||
|
||||
suspend fun getSubsFileLastModified(): Long {
|
||||
return RoomX.select<SubsItem>().map { File(it.filePath) }
|
||||
return DbSet.subsItemDao.query().first().map { it.subsFile }
|
||||
.filter { it.isFile && it.exists() }
|
||||
.maxOfOrNull { it.lastModified() } ?: -1L
|
||||
}
|
||||
|
||||
suspend fun buildRuleManager(): RuleManager {
|
||||
return RuleManager(*RoomX.select<SubsItem>().sortedBy { it.index }.map { subsItem ->
|
||||
if (!subsItem.enable) return@map null
|
||||
try {
|
||||
val file = File(subsItem.filePath)
|
||||
if (file.isFile && file.exists()) {
|
||||
return@map SubscriptionRaw.parse5(file.readText())
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
return@map null
|
||||
}.filterNotNull().toTypedArray())
|
||||
}
|
||||
|
||||
suspend fun getActivityIdByShizuku(): String? {
|
||||
if (!ShizukuShell.instance.isAvailable) return null
|
||||
val result = withTimeoutOrNull(250) {
|
||||
withContext(Dispatchers.IO) {
|
||||
ShizukuShell.instance.exec(Shell.Command("dumpsys activity activities | grep mResumedActivity"))
|
||||
}
|
||||
} ?: return null
|
||||
val strList = result.out.split("\u0020")
|
||||
if (!result.isSuccessful || strList.size < 4 || !strList[3].contains('/')) {
|
||||
return null
|
||||
}
|
||||
var (appId, activityId) = strList[3].split('/')
|
||||
if (activityId.startsWith('.')) {
|
||||
activityId = appId + activityId
|
||||
}
|
||||
return activityId
|
||||
}
|
||||
|
||||
fun CoroutineScope.launchWhile(
|
||||
context: CoroutineContext = EmptyCoroutineContext,
|
||||
start: CoroutineStart = CoroutineStart.DEFAULT,
|
||||
block: suspend CoroutineScope.() -> Unit,
|
||||
) = launch(context, start) {
|
||||
while (isActive) {
|
||||
block()
|
||||
}
|
||||
}
|
||||
|
||||
fun CoroutineScope.launchTry(
|
||||
context: CoroutineContext = EmptyCoroutineContext,
|
||||
start: CoroutineStart = CoroutineStart.DEFAULT,
|
||||
block: suspend CoroutineScope.() -> Unit,
|
||||
) = launch(context, start) {
|
||||
try {
|
||||
block()
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
ToastUtils.showShort(e.message)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("fallthrough")
|
||||
fun createNotificationChannel(context: Service) {
|
||||
val channelId = "channel_service_ab"
|
||||
val intent = Intent(context, MainActivity::class.java).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
}
|
||||
|
||||
val pendingIntent: PendingIntent = PendingIntent.getActivity(
|
||||
context,
|
||||
0,
|
||||
intent,
|
||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||
)
|
||||
|
||||
val builder = NotificationCompat.Builder(context, channelId)
|
||||
.setSmallIcon(R.drawable.ic_app_2)
|
||||
.setContentTitle("调试模式2")
|
||||
.setContentText("测试后台任务")
|
||||
.setContentIntent(pendingIntent)
|
||||
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
||||
.setOngoing(true)
|
||||
.setAutoCancel(false)
|
||||
|
||||
val channelId = "无障碍后台服务"
|
||||
val name = "无障碍服务"
|
||||
val descriptionText = "无障碍服务保持活跃"
|
||||
val importance = NotificationManager.IMPORTANCE_DEFAULT
|
||||
|
@ -252,31 +162,40 @@ object Ext {
|
|||
}
|
||||
val notificationManager = NotificationManagerCompat.from(context)
|
||||
notificationManager.createNotificationChannel(channel)
|
||||
|
||||
val serviceId = 100
|
||||
val intent = Intent(context, MainActivity::class.java).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
}
|
||||
val pendingIntent: PendingIntent = PendingIntent.getActivity(
|
||||
context,
|
||||
0,
|
||||
intent,
|
||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||
)
|
||||
|
||||
val builder = NotificationCompat.Builder(context, channelId)
|
||||
.setSmallIcon(SafeR.ic_add)
|
||||
.setContentTitle("搞快点")
|
||||
.setContentText("无障碍正在运行")
|
||||
.setContentIntent(pendingIntent)
|
||||
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
||||
.setOngoing(true)
|
||||
.setAutoCancel(false)
|
||||
|
||||
val notification = builder.build()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
context.startForeground(
|
||||
110,
|
||||
serviceId,
|
||||
notification,
|
||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_MANIFEST
|
||||
)
|
||||
} else {
|
||||
context.startForeground(110, notification)
|
||||
context.startForeground(serviceId, notification)
|
||||
}
|
||||
}
|
||||
|
||||
val LocalLauncher =
|
||||
compositionLocalOf<StartActivityLauncher> { error("not found StartActivityLauncher") }
|
||||
|
||||
|
||||
@Composable
|
||||
fun <T> usePollState(interval: Long = 400L, getter: () -> T): MutableState<T> {
|
||||
val mutableState = remember { mutableStateOf(getter()) }
|
||||
LaunchedEffect(Unit) {
|
||||
while (isActive) {
|
||||
delay(interval)
|
||||
mutableState.value = getter()
|
||||
}
|
||||
}
|
||||
return mutableState
|
||||
}
|
||||
|
||||
}
|
18
app/src/main/java/li/songe/gkd/utils/FolderExt.kt
Normal file
18
app/src/main/java/li/songe/gkd/utils/FolderExt.kt
Normal file
|
@ -0,0 +1,18 @@
|
|||
package li.songe.gkd.utils
|
||||
|
||||
import com.blankj.utilcode.util.PathUtils
|
||||
import java.io.File
|
||||
|
||||
object FolderExt {
|
||||
private fun createFolder(name: String): File {
|
||||
return File(PathUtils.getExternalAppFilesPath().plus("/$name")).apply {
|
||||
if (!exists()) {
|
||||
mkdirs()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val dbFolder by lazy { createFolder("db") }
|
||||
val subsFolder by lazy { createFolder("subscription") }
|
||||
val snapshotFolder by lazy { createFolder("snapshot") }
|
||||
}
|
|
@ -1,23 +1,12 @@
|
|||
package li.songe.gkd.hooks
|
||||
package li.songe.gkd.utils
|
||||
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.compose.runtime.Composable
|
||||
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 com.blankj.utilcode.util.LogUtils
|
||||
import com.journeyapps.barcodescanner.ScanContract
|
||||
import com.journeyapps.barcodescanner.ScanIntentResult
|
||||
import com.journeyapps.barcodescanner.ScanOptions
|
||||
import io.ktor.client.call.body
|
||||
import io.ktor.client.request.get
|
||||
import io.ktor.client.statement.bodyAsText
|
||||
import kotlinx.coroutines.delay
|
||||
import li.songe.gkd.data.SubscriptionRaw
|
||||
import li.songe.gkd.data.Value
|
||||
import li.songe.gkd.util.Singleton
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
|
@ -43,14 +32,3 @@ fun useNavigateForQrcodeResult(): suspend () -> ScanIntentResult {
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun useFetchSubs(): suspend (String) -> String {
|
||||
val scope = rememberCoroutineScope()
|
||||
var loading by remember { mutableStateOf(false) }
|
||||
return remember {
|
||||
{ url ->
|
||||
loading
|
||||
Singleton.client.get(url).bodyAsText()
|
||||
}
|
||||
}
|
||||
}
|
14
app/src/main/java/li/songe/gkd/utils/ModifierExt.kt
Normal file
14
app/src/main/java/li/songe/gkd/utils/ModifierExt.kt
Normal file
|
@ -0,0 +1,14 @@
|
|||
package li.songe.gkd.utils
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.composed
|
||||
|
||||
inline fun Modifier.noRippleClickable(crossinline onClick: () -> Unit): Modifier = composed {
|
||||
clickable(indication = null,
|
||||
interactionSource = remember { MutableInteractionSource() }) {
|
||||
onClick()
|
||||
}
|
||||
}
|
8
app/src/main/java/li/songe/gkd/utils/NavExt.kt
Normal file
8
app/src/main/java/li/songe/gkd/utils/NavExt.kt
Normal file
|
@ -0,0 +1,8 @@
|
|||
package li.songe.gkd.utils
|
||||
|
||||
import androidx.compose.runtime.compositionLocalOf
|
||||
import androidx.navigation.NavHostController
|
||||
|
||||
|
||||
val LocalNavController =
|
||||
compositionLocalOf<NavHostController> { error("not found DestinationsNavigator") }
|
28
app/src/main/java/li/songe/gkd/utils/SafeR.kt
Normal file
28
app/src/main/java/li/songe/gkd/utils/SafeR.kt
Normal file
|
@ -0,0 +1,28 @@
|
|||
package li.songe.gkd.utils
|
||||
|
||||
import li.songe.gkd.R
|
||||
|
||||
|
||||
/**
|
||||
* ![image](https://github.com/lisonge/gkd/assets/38517192/545c4fce-77b2-4003-8e22-a21b48ef3d98)
|
||||
*/
|
||||
@Suppress("UNRESOLVED_REFERENCE")
|
||||
object SafeR {
|
||||
val capture: Int = R.drawable.capture
|
||||
val ic_add: Int = R.drawable.ic_add
|
||||
val ic_app_2: Int = R.drawable.ic_app_2
|
||||
val ic_apps: Int = R.drawable.ic_apps
|
||||
val ic_back: Int = R.drawable.ic_back
|
||||
val ic_chart_bar: Int = R.drawable.ic_chart_bar
|
||||
val ic_cog: Int = R.drawable.ic_cog
|
||||
val ic_create_round: Int = R.drawable.ic_create_round
|
||||
val ic_database_set: Int = R.drawable.ic_database_set
|
||||
val ic_del: Int = R.drawable.ic_del
|
||||
val ic_launcher: Int = R.drawable.ic_launcher
|
||||
val ic_launcher_background: Int = R.drawable.ic_launcher_background
|
||||
val ic_launcher_round: Int = R.drawable.ic_launcher_round
|
||||
val ic_link: Int = R.drawable.ic_link
|
||||
val ic_menu: Int = R.drawable.ic_menu
|
||||
val ic_refresh: Int = R.drawable.ic_refresh
|
||||
val ic_share: Int = R.drawable.ic_share
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package li.songe.gkd.util
|
||||
package li.songe.gkd.utils
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
|
@ -16,7 +16,7 @@ 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 li.songe.gkd.utils.Ext.isEmptyBitmap
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.resumeWithException
|
||||
import kotlin.coroutines.suspendCoroutine
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user