From f85254ace2148af8f013b0ca1a3eae6dc8a38de7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E6=B4=AA=E6=9D=A5?= Date: Wed, 4 Mar 2026 14:33:43 +0800 Subject: [PATCH] app: add apps strategy --- .gitignore | 1 + app/build.gradle.kts | 5 +- app/src/main/AndroidManifest.xml | 5 + .../kr328/clash/AccessControlActivity.kt | 46 ++++- .../kr328/clash/AppsStrategyActivity.kt | 154 ++++++++++++++++ .../com/github/kr328/clash/MainActivity.kt | 19 +- .../kr328/clash/design/AppsStrategyDesign.kt | 172 ++++++++++++++++++ .../github/kr328/clash/design/MainDesign.kt | 7 + .../adapter/AppsStrategyConfigAdapter.kt | 44 +++++ .../layout/adapter_apps_strategy_config.xml | 119 ++++++++++++ .../main/res/layout/design_apps_strategy.xml | 60 ++++++ design/src/main/res/layout/design_main.xml | 12 ++ .../dialog_apps_strategy_config_edit.xml | 57 ++++++ design/src/main/res/values-ja-rJP/strings.xml | 3 + design/src/main/res/values-ko-rKR/strings.xml | 3 + design/src/main/res/values-ru/strings.xml | 3 + design/src/main/res/values-vi/strings.xml | 3 + design/src/main/res/values-zh-rHK/strings.xml | 3 + design/src/main/res/values-zh-rTW/strings.xml | 3 + design/src/main/res/values-zh/strings.xml | 3 + design/src/main/res/values/strings.xml | 8 + .../github/kr328/clash/service/TunService.kt | 13 +- .../clash/service/model/AppsStrategyConfig.kt | 26 +++ .../kr328/clash/service/store/ServiceStore.kt | 42 +++++ .../kr328/clash/service/util/Serializers.kt | 2 + 25 files changed, 798 insertions(+), 15 deletions(-) create mode 100644 app/src/main/java/com/github/kr328/clash/AppsStrategyActivity.kt create mode 100644 design/src/main/java/com/github/kr328/clash/design/AppsStrategyDesign.kt create mode 100644 design/src/main/java/com/github/kr328/clash/design/adapter/AppsStrategyConfigAdapter.kt create mode 100644 design/src/main/res/layout/adapter_apps_strategy_config.xml create mode 100644 design/src/main/res/layout/design_apps_strategy.xml create mode 100644 design/src/main/res/layout/dialog_apps_strategy_config_edit.xml create mode 100644 service/src/main/java/com/github/kr328/clash/service/model/AppsStrategyConfig.kt diff --git a/.gitignore b/.gitignore index a169a39695..8a8e9b91e0 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ build/ /app/foss/release /app/premium/release /captures +/.kotlin/ # Ignore Gradle GUI config gradle-app.setting diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 5272b86afb..603769cc31 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -45,8 +45,11 @@ task("downloadGeoFiles") { doLast { geoFilesUrls.forEach { (downloadUrl, outputFileName) -> - val url = URL(downloadUrl) val outputPath = file("$geoFilesDownloadDir/$outputFileName") + if (outputPath.exists()) { + return@forEach + } + val url = URL(downloadUrl) outputPath.parentFile.mkdirs() url.openStream().use { input -> Files.copy(input, outputPath.toPath(), StandardCopyOption.REPLACE_EXISTING) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index f6fc1ab655..11fc50c830 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -167,6 +167,11 @@ android:configChanges="uiMode" android:exported="false" android:label="@string/access_control_packages" /> + () { override suspend fun main() { + val configUuid = intent.uuid val service = ServiceStore(this) - val selected = withContext(Dispatchers.IO) { - service.accessControlPackages.toMutableSet() + val (config, selected) = withContext(Dispatchers.IO) { + if (configUuid != null) { + val cfg = service.appsStrategyConfigs.find { it.uuid == configUuid } + if (cfg != null) { + cfg to cfg.packages.toMutableSet() + } else { + null to service.accessControlPackages.toMutableSet() + } + } else { + null to service.accessControlPackages.toMutableSet() + } } defer { withContext(Dispatchers.IO) { - val changed = selected != service.accessControlPackages - service.accessControlPackages = selected - if (clashRunning && changed) { - stopClashService() - while (clashRunning) { - delay(200) + if (config != null) { + val changed = selected != config.packages.toSet() + if (changed) { + val updatedConfig = AppsStrategyConfig( + uuid = config.uuid, + name = config.name, + mode = config.mode, + packages = selected.toList() + ) + val configs = service.appsStrategyConfigs.map { + if (it.uuid == config.uuid) updatedConfig else it + } + service.appsStrategyConfigs = configs + } + } else { + val changed = selected != service.accessControlPackages + service.accessControlPackages = selected + if (clashRunning && changed) { + stopClashService() + while (clashRunning) { + delay(200) + } + startClashService() } - startClashService() } } } diff --git a/app/src/main/java/com/github/kr328/clash/AppsStrategyActivity.kt b/app/src/main/java/com/github/kr328/clash/AppsStrategyActivity.kt new file mode 100644 index 0000000000..4ff2c679d0 --- /dev/null +++ b/app/src/main/java/com/github/kr328/clash/AppsStrategyActivity.kt @@ -0,0 +1,154 @@ +package com.github.kr328.clash + +import com.github.kr328.clash.common.util.intent +import com.github.kr328.clash.common.util.setUUID +import com.github.kr328.clash.design.AppsStrategyDesign +import com.github.kr328.clash.design.dialog.requestModelTextInput +import com.github.kr328.clash.design.requestAppListsConfigEdit +import com.github.kr328.clash.service.model.AccessControlMode +import com.github.kr328.clash.service.model.AppsStrategyConfig +import com.github.kr328.clash.service.store.ServiceStore +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.isActive +import kotlinx.coroutines.selects.select +import kotlinx.coroutines.withContext +import java.util.* + +class AppsStrategyActivity : BaseActivity() { + override suspend fun main() { + val design = AppsStrategyDesign(this) + + setContentDesign(design) + + design.fetch() + + while (isActive) { + select { + events.onReceive { + when (it) { + Event.ActivityStart -> { + design.fetch() + } + + else -> Unit + } + } + design.requests.onReceive { + when (it) { + is AppsStrategyDesign.Request.Choose -> { + startActivity(AccessControlActivity::class.intent.setUUID(it.config.uuid)) + } + + is AppsStrategyDesign.Request.Edit -> { + val result = requestAppListsConfigEdit( + initialName = it.config.name, + initialMode = it.config.mode, + title = getString(com.github.kr328.clash.design.R.string.edit_config_name), + hint = getString(com.github.kr328.clash.design.R.string.config_name), + showDelete = true + ) + if (result != null) { + if (result.deleted) { + deleteConfig(it.config.uuid) + design.fetch() + } else if (result.name != it.config.name || result.mode != it.config.mode) { + updateConfig(it.config.uuid, result.name, result.mode) + design.fetch() + } + } + } + + is AppsStrategyDesign.Request.Active -> { + setActiveConfig(it.config.uuid) + } + + AppsStrategyDesign.Request.Create -> { + val name = requestModelTextInput( + initial = "", + title = getString(com.github.kr328.clash.design.R.string.new_config), + hint = getString(com.github.kr328.clash.design.R.string.config_name) + ) + if (name.isNotBlank()) { + createConfig(name) + } + } + } + } + } + } + } + + private suspend fun AppsStrategyDesign.fetch() { + val configs = queryConfigs() + val activeUuid = queryActiveUuid() + patchConfigs(configs, activeUuid) + } + + private suspend fun queryConfigs(): List { + return withContext(Dispatchers.IO) { + val service = ServiceStore(this@AppsStrategyActivity) + service.appsStrategyConfigs + } + } + + private suspend fun queryActiveUuid(): UUID? { + return withContext(Dispatchers.IO) { + val service = ServiceStore(this@AppsStrategyActivity) + service.activeAppsStrategyConfigUuid + } + } + + private suspend fun setActiveConfig(uuid: UUID) { + withContext(Dispatchers.IO) { + val service = ServiceStore(this@AppsStrategyActivity) + service.activeAppsStrategyConfigUuid = uuid + design?.fetch() + } + } + + private suspend fun deleteConfig(uuid: UUID) { + withContext(Dispatchers.IO) { + val service = ServiceStore(this@AppsStrategyActivity) + val configs = service.appsStrategyConfigs.filter { it.uuid != uuid } + service.appsStrategyConfigs = configs + if (service.activeAppsStrategyConfigUuid == uuid) { + service.activeAppsStrategyConfigUuid = null + } + } + } + + private suspend fun createConfig(name: String) { + withContext(Dispatchers.IO) { + val service = ServiceStore(this@AppsStrategyActivity) + val configs = service.appsStrategyConfigs.toMutableList() + val newConfig = AppsStrategyConfig( + uuid = UUID.randomUUID(), + name = name, + mode = com.github.kr328.clash.service.model.AccessControlMode.AcceptAll, + packages = emptyList() + ) + configs.add(newConfig) + service.appsStrategyConfigs = configs + design?.fetch() + } + } + + private suspend fun updateConfig(uuid: UUID, newName: String, newMode: AccessControlMode) { + withContext(Dispatchers.IO) { + val service = ServiceStore(this@AppsStrategyActivity) + val configs = service.appsStrategyConfigs.map { config -> + if (config.uuid == uuid) { + AppsStrategyConfig( + uuid = config.uuid, + name = newName, + mode = newMode, + packages = config.packages + ) + } else { + config + } + } + service.appsStrategyConfigs = configs + } + } +} diff --git a/app/src/main/java/com/github/kr328/clash/MainActivity.kt b/app/src/main/java/com/github/kr328/clash/MainActivity.kt index c4237906b2..4b0c686072 100644 --- a/app/src/main/java/com/github/kr328/clash/MainActivity.kt +++ b/app/src/main/java/com/github/kr328/clash/MainActivity.kt @@ -3,15 +3,14 @@ package com.github.kr328.clash import android.content.pm.PackageManager import android.os.Build import android.os.Bundle -import android.os.PersistableBundle import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts.RequestPermission -import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat import com.github.kr328.clash.common.util.intent import com.github.kr328.clash.common.util.ticker import com.github.kr328.clash.design.MainDesign import com.github.kr328.clash.design.ui.ToastDuration +import com.github.kr328.clash.service.store.ServiceStore import com.github.kr328.clash.util.startClashService import com.github.kr328.clash.util.stopClashService import com.github.kr328.clash.util.withClash @@ -59,6 +58,8 @@ class MainActivity : BaseActivity() { startActivity(ProfilesActivity::class.intent) MainDesign.Request.OpenProviders -> startActivity(ProvidersActivity::class.intent) + MainDesign.Request.OpenAppsStrategy -> + startActivity(AppsStrategyActivity::class.intent) MainDesign.Request.OpenLogs -> { if (LogcatService.running) { startActivity(LogcatActivity::class.intent) @@ -99,6 +100,8 @@ class MainActivity : BaseActivity() { withProfile { setProfileName(queryActive()?.name) } + + setAppsStrategyName(queryActiveAppsStrategyName()) } private suspend fun MainDesign.fetchTraffic() { @@ -143,6 +146,18 @@ class MainActivity : BaseActivity() { } } + private suspend fun queryActiveAppsStrategyName(): String? { + return withContext(Dispatchers.IO) { + val service = ServiceStore(this@MainActivity) + val activeUuid = service.activeAppsStrategyConfigUuid + if (activeUuid != null) { + service.appsStrategyConfigs.find { it.uuid == activeUuid }?.name + } else { + null + } + } + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { diff --git a/design/src/main/java/com/github/kr328/clash/design/AppsStrategyDesign.kt b/design/src/main/java/com/github/kr328/clash/design/AppsStrategyDesign.kt new file mode 100644 index 0000000000..e609640b01 --- /dev/null +++ b/design/src/main/java/com/github/kr328/clash/design/AppsStrategyDesign.kt @@ -0,0 +1,172 @@ +package com.github.kr328.clash.design + +import android.content.Context +import android.view.View +import androidx.appcompat.app.AlertDialog +import androidx.core.widget.doOnTextChanged +import com.github.kr328.clash.design.adapter.AppsStrategyConfigAdapter +import com.github.kr328.clash.design.databinding.DesignAppsStrategyBinding +import com.github.kr328.clash.design.databinding.DialogAppsStrategyConfigEditBinding +import com.github.kr328.clash.design.util.Validator +import com.github.kr328.clash.design.util.ValidatorAcceptAll +import com.github.kr328.clash.design.util.applyFrom +import com.github.kr328.clash.design.util.applyLinearAdapter +import com.github.kr328.clash.design.util.bindAppBarElevation +import com.github.kr328.clash.design.util.layoutInflater +import com.github.kr328.clash.design.util.patchDataSet +import com.github.kr328.clash.design.util.requestTextInput +import com.github.kr328.clash.design.util.root +import com.github.kr328.clash.service.model.AccessControlMode +import com.github.kr328.clash.service.model.AppsStrategyConfig +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.withContext +import java.util.* +import kotlin.coroutines.resume + +class AppsStrategyDesign(context: Context) : Design(context) { + + sealed class Request { + object Create : Request() + data class Edit(val config: AppsStrategyConfig) : Request() + data class Active(val config: AppsStrategyConfig) : Request() + data class Choose(val config: AppsStrategyConfig) : Request() + } + + private val binding = DesignAppsStrategyBinding + .inflate(context.layoutInflater, context.root, false) + + private val adapter = AppsStrategyConfigAdapter( + context, + onClicked = { config -> + requests.trySend(Request.Active(config)) + }, + onEditClicked = { config -> + requests.trySend(Request.Edit(config)) + }, + onChooseClicked = { config -> + requests.trySend(Request.Choose(config)) + } + ) + + override val root: View + get() = binding.root + + init { + binding.self = this + + binding.activityBarLayout.applyFrom(context) + + binding.mainList.recyclerList.also { + it.bindAppBarElevation(binding.activityBarLayout) + it.applyLinearAdapter(context, adapter) + } + } + + suspend fun patchConfigs(configs: List, activeUuid: UUID?) { + withContext(Dispatchers.Main) { + adapter.apply { + patchDataSet(this::configs, configs, id = { it.uuid }) + this.activeUuid = activeUuid + notifyItemRangeChanged(0, configs.size) + } + } + } + + fun requestCreate() { + requests.trySend(Request.Create) + } +} + +data class AppListsConfigEditResult( + val name: String, + val mode: AccessControlMode, + val deleted: Boolean = false +) + +suspend fun Context.requestAppListsConfigEdit( + initialName: String, + initialMode: AccessControlMode, + title: CharSequence, + hint: CharSequence? = null, + error: CharSequence? = null, + showDelete: Boolean = false, + validator: Validator = ValidatorAcceptAll, +): AppListsConfigEditResult? { + return suspendCancellableCoroutine { + val binding = DialogAppsStrategyConfigEditBinding + .inflate(layoutInflater, this.root, false) + + val builder = MaterialAlertDialogBuilder(this) + .setTitle(title) + .setView(binding.root) + .setCancelable(true) + .setPositiveButton(R.string.ok) { _, _ -> + val text = binding.textField.text?.toString() ?: "" + val mode = when (binding.modeGroup.checkedRadioButtonId) { + R.id.mode_accept_selected -> AccessControlMode.AcceptSelected + R.id.mode_deny_selected -> AccessControlMode.DenySelected + else -> AccessControlMode.AcceptAll + } + + if (validator(text)) + it.resume(AppListsConfigEditResult(text, mode, false)) + else + it.resume(null) + } + .setNegativeButton(R.string.cancel) { _, _ -> } + .setOnDismissListener { _ -> + if (!it.isCompleted) + it.resume(null) + } + + if (showDelete) { + builder.setNeutralButton(R.string.delete) { _, _ -> + it.resume(AppListsConfigEditResult("", AccessControlMode.AcceptAll, true)) + } + } + + val dialog = builder.create() + + it.invokeOnCancellation { + dialog.dismiss() + } + + dialog.setOnShowListener { + if (hint != null) + binding.textLayout.hint = hint + + binding.textField.apply { + binding.textLayout.isErrorEnabled = error != null + + doOnTextChanged { text, _, _, _ -> + if (!validator(text?.toString() ?: "")) { + if (error != null) + binding.textLayout.error = error + + dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = false + } else { + if (error != null) + binding.textLayout.error = null + + dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = true + } + } + + setText(initialName) + setSelection(0, initialName.length) + requestTextInput() + } + + // Set initial mode + when (initialMode) { + AccessControlMode.AcceptAll -> binding.modeAcceptAll.isChecked = true + AccessControlMode.AcceptSelected -> binding.modeAcceptSelected.isChecked = true + AccessControlMode.DenySelected -> binding.modeDenySelected.isChecked = true + } + } + + dialog.show() + } +} diff --git a/design/src/main/java/com/github/kr328/clash/design/MainDesign.kt b/design/src/main/java/com/github/kr328/clash/design/MainDesign.kt index f418e488d9..d699cc6545 100644 --- a/design/src/main/java/com/github/kr328/clash/design/MainDesign.kt +++ b/design/src/main/java/com/github/kr328/clash/design/MainDesign.kt @@ -19,6 +19,7 @@ class MainDesign(context: Context) : Design(context) { OpenProxy, OpenProfiles, OpenProviders, + OpenAppsStrategy, OpenLogs, OpenSettings, OpenHelp, @@ -37,6 +38,12 @@ class MainDesign(context: Context) : Design(context) { } } + suspend fun setAppsStrategyName(name: String?) { + withContext(Dispatchers.Main) { + binding.appsStrategyName = name + } + } + suspend fun setClashRunning(running: Boolean) { withContext(Dispatchers.Main) { binding.clashRunning = running diff --git a/design/src/main/java/com/github/kr328/clash/design/adapter/AppsStrategyConfigAdapter.kt b/design/src/main/java/com/github/kr328/clash/design/adapter/AppsStrategyConfigAdapter.kt new file mode 100644 index 0000000000..e08d424917 --- /dev/null +++ b/design/src/main/java/com/github/kr328/clash/design/adapter/AppsStrategyConfigAdapter.kt @@ -0,0 +1,44 @@ +package com.github.kr328.clash.design.adapter + +import android.content.Context +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.github.kr328.clash.design.databinding.AdapterAppsStrategyConfigBinding +import com.github.kr328.clash.design.util.layoutInflater +import com.github.kr328.clash.service.model.AppsStrategyConfig +import java.util.* + +class AppsStrategyConfigAdapter( + private val context: Context, + private val onClicked: (AppsStrategyConfig) -> Unit, + private val onEditClicked: (AppsStrategyConfig) -> Unit, + private val onChooseClicked: (AppsStrategyConfig) -> Unit, +) : RecyclerView.Adapter() { + + class Holder(val binding: AdapterAppsStrategyConfigBinding) : + RecyclerView.ViewHolder(binding.root) + + var configs: List = emptyList() + var activeUuid: UUID? = null + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder { + return Holder( + AdapterAppsStrategyConfigBinding + .inflate(context.layoutInflater, parent, false) + ) + } + + override fun onBindViewHolder(holder: Holder, position: Int) { + val config = configs[position] + val binding = holder.binding + + binding.config = config + binding.isActive = config.uuid == activeUuid + binding.clicked = View.OnClickListener { onClicked(config) } + binding.editClicked = View.OnClickListener { onEditClicked(config) } + binding.chooseClicked = View.OnClickListener { onChooseClicked(config) } + } + + override fun getItemCount(): Int = configs.size +} diff --git a/design/src/main/res/layout/adapter_apps_strategy_config.xml b/design/src/main/res/layout/adapter_apps_strategy_config.xml new file mode 100644 index 0000000000..c9b280f25c --- /dev/null +++ b/design/src/main/res/layout/adapter_apps_strategy_config.xml @@ -0,0 +1,119 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/design/src/main/res/layout/design_apps_strategy.xml b/design/src/main/res/layout/design_apps_strategy.xml new file mode 100644 index 0000000000..f9ca50a024 --- /dev/null +++ b/design/src/main/res/layout/design_apps_strategy.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/design/src/main/res/layout/design_main.xml b/design/src/main/res/layout/design_main.xml index 1d02d7b403..38bb08c82a 100644 --- a/design/src/main/res/layout/design_main.xml +++ b/design/src/main/res/layout/design_main.xml @@ -17,6 +17,9 @@ + @@ -107,6 +110,15 @@ app:subtext="@{profileName != null ? @string/format_profile_activated(profileName) : @string/not_selected}" app:text="@string/profile" /> + + + + + + + + + + + + + + + + + + + + + + diff --git a/design/src/main/res/values-ja-rJP/strings.xml b/design/src/main/res/values-ja-rJP/strings.xml index c346e3e760..d874b20e03 100644 --- a/design/src/main/res/values-ja-rJP/strings.xml +++ b/design/src/main/res/values-ja-rJP/strings.xml @@ -253,4 +253,7 @@ Override Destination カメラのアクセスが制限されています。設定から有効にしてください。 システムで予期しない例外が発生しました。 + 新規構成 + 構成名 + 構成名の編集 \ No newline at end of file diff --git a/design/src/main/res/values-ko-rKR/strings.xml b/design/src/main/res/values-ko-rKR/strings.xml index ab0bbf0a5e..65c52b07cb 100644 --- a/design/src/main/res/values-ko-rKR/strings.xml +++ b/design/src/main/res/values-ko-rKR/strings.xml @@ -253,4 +253,7 @@ Override Destination 카메라 접근이 제한되었습니다. 설정에서 허용해 주세요. 처리되지 않은 시스템 예외가 발생했습니다. + 새 구성 + 구성 이름 + 구성 이름 편집 \ No newline at end of file diff --git a/design/src/main/res/values-ru/strings.xml b/design/src/main/res/values-ru/strings.xml index 342b639d28..d82f658663 100644 --- a/design/src/main/res/values-ru/strings.xml +++ b/design/src/main/res/values-ru/strings.xml @@ -317,4 +317,7 @@ Override Destination Доступ к камере ограничен. Разрешите его в настройках. Произошла не обрабатываемая системная ошибка. + Новая конфигурация + Название конфигурации + Изменить название конфигурации diff --git a/design/src/main/res/values-vi/strings.xml b/design/src/main/res/values-vi/strings.xml index 2bf8aacad4..0d28ff86d7 100644 --- a/design/src/main/res/values-vi/strings.xml +++ b/design/src/main/res/values-vi/strings.xml @@ -239,4 +239,7 @@ Nhập từ Mã QR Quyền truy cập camera bị hạn chế. Vui lòng bật trong Cài đặt. Đã xảy ra ngoại lệ hệ thống không xử lý được. + Cấu hình mới + Tên cấu hình + Sửa tên cấu hình diff --git a/design/src/main/res/values-zh-rHK/strings.xml b/design/src/main/res/values-zh-rHK/strings.xml index 537b33f3ee..0f1c33aa3b 100644 --- a/design/src/main/res/values-zh-rHK/strings.xml +++ b/design/src/main/res/values-zh-rHK/strings.xml @@ -250,4 +250,7 @@ Override Destination 相機權限受限,請前往設定開啟。 發生系統未知異常,操作失敗。 + 新配置 + 配置名稱 + 編輯配置名稱 \ No newline at end of file diff --git a/design/src/main/res/values-zh-rTW/strings.xml b/design/src/main/res/values-zh-rTW/strings.xml index cf70cc2d41..8d1f234d22 100644 --- a/design/src/main/res/values-zh-rTW/strings.xml +++ b/design/src/main/res/values-zh-rTW/strings.xml @@ -250,4 +250,7 @@ Override Destination 相機權限受限,請前往設定開啟。 發生系統未知異常,操作失敗。 + 新配置 + 配置名稱 + 編輯配置名稱 diff --git a/design/src/main/res/values-zh/strings.xml b/design/src/main/res/values-zh/strings.xml index 48daff432b..43c56571af 100644 --- a/design/src/main/res/values-zh/strings.xml +++ b/design/src/main/res/values-zh/strings.xml @@ -262,4 +262,7 @@ Clash.Meta 服务已停止 相机权限受限,请前往设置开启。 发生系统未知异常,操作失败。 + 新配置 + 配置名称 + 编辑配置名称 diff --git a/design/src/main/res/values/strings.xml b/design/src/main/res/values/strings.xml index 518af90d0d..9615f9bda4 100644 --- a/design/src/main/res/values/strings.xml +++ b/design/src/main/res/values/strings.xml @@ -1,4 +1,9 @@ + + %d app selected + %d apps selected + + Clash Meta Clash Meta Alpha Clash Meta for Android @@ -350,4 +355,7 @@ Hide app from the Recent apps screen Camera access is restricted. Please enable it in Settings. An unhandled system exception occurred. + New Config + Config Name + Edit Config Name diff --git a/service/src/main/java/com/github/kr328/clash/service/TunService.kt b/service/src/main/java/com/github/kr328/clash/service/TunService.kt index 8550f8ab81..643bbcd364 100644 --- a/service/src/main/java/com/github/kr328/clash/service/TunService.kt +++ b/service/src/main/java/com/github/kr328/clash/service/TunService.kt @@ -154,15 +154,22 @@ class TunService : VpnService(), CoroutineScope by CoroutineScope(Dispatchers.De } // Access Control - when (store.accessControlMode) { + val activeConfig = store.activeAppsStrategyConfigUuid?.let { uuid -> + store.appsStrategyConfigs.find { it.uuid == uuid } + } + + val mode = activeConfig?.mode ?: store.accessControlMode + val packages = activeConfig?.packages ?: store.accessControlPackages.toList() + + when (mode) { AccessControlMode.AcceptAll -> Unit AccessControlMode.AcceptSelected -> { - (store.accessControlPackages + packageName).forEach { + (packages + packageName).forEach { runCatching { addAllowedApplication(it) } } } AccessControlMode.DenySelected -> { - (store.accessControlPackages - packageName).forEach { + (packages - packageName).forEach { runCatching { addDisallowedApplication(it) } } } diff --git a/service/src/main/java/com/github/kr328/clash/service/model/AppsStrategyConfig.kt b/service/src/main/java/com/github/kr328/clash/service/model/AppsStrategyConfig.kt new file mode 100644 index 0000000000..547db16de7 --- /dev/null +++ b/service/src/main/java/com/github/kr328/clash/service/model/AppsStrategyConfig.kt @@ -0,0 +1,26 @@ +package com.github.kr328.clash.service.model + +import com.github.kr328.clash.service.util.UUIDSerializer +import kotlinx.serialization.InternalSerializationApi +import kotlinx.serialization.Serializable +import java.util.* + +@OptIn(InternalSerializationApi::class) +@Serializable +class AppsStrategyConfig( + @Serializable(with = UUIDSerializer::class) + val uuid: UUID, + val name: String, + val mode: AccessControlMode, + val packages: List +) { + companion object { + fun create( + name: String, + mode: AccessControlMode, + packages: List + ): AppsStrategyConfig { + return AppsStrategyConfig(UUID.randomUUID(), name, mode, packages) + } + } +} diff --git a/service/src/main/java/com/github/kr328/clash/service/store/ServiceStore.kt b/service/src/main/java/com/github/kr328/clash/service/store/ServiceStore.kt index d361848ff5..c5d5173bbb 100644 --- a/service/src/main/java/com/github/kr328/clash/service/store/ServiceStore.kt +++ b/service/src/main/java/com/github/kr328/clash/service/store/ServiceStore.kt @@ -5,6 +5,9 @@ import com.github.kr328.clash.common.store.Store import com.github.kr328.clash.common.store.asStoreProvider import com.github.kr328.clash.service.PreferenceProvider import com.github.kr328.clash.service.model.AccessControlMode +import com.github.kr328.clash.service.model.AppsStrategyConfig +import kotlinx.serialization.json.Json +import java.io.File import java.util.* class ServiceStore(context: Context) { @@ -14,6 +17,15 @@ class ServiceStore(context: Context) { .asStoreProvider() ) + private val filesDir = context.filesDir + private val settingsDir = File(filesDir, "settings").apply { mkdirs() } + private val appListsConfigsFile = File(settingsDir, "app_lists_configs.json") + + private val json = Json { + ignoreUnknownKeys = true + prettyPrint = true + } + var activeProfile: UUID? by store.typedString( key = "active_profile", from = { if (it.isBlank()) null else UUID.fromString(it) }, @@ -65,4 +77,34 @@ class ServiceStore(context: Context) { key = "dynamic_notification", defaultValue = true ) + + var appsStrategyConfigs: List + get() { + return if (appListsConfigsFile.exists()) { + try { + json.decodeFromString( + kotlinx.serialization.builtins.ListSerializer(AppsStrategyConfig.serializer()), + appListsConfigsFile.readText() + ) + } catch (e: Exception) { + emptyList() + } + } else { + emptyList() + } + } + set(value) { + appListsConfigsFile.writeText( + json.encodeToString( + kotlinx.serialization.builtins.ListSerializer(AppsStrategyConfig.serializer()), + value + ) + ) + } + + var activeAppsStrategyConfigUuid: UUID? by store.typedString( + key = "active_apps_strategy_config_uuid", + from = { if (it.isBlank()) null else UUID.fromString(it) }, + to = { it?.toString() ?: "" } + ) } \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/util/Serializers.kt b/service/src/main/java/com/github/kr328/clash/service/util/Serializers.kt index 5cbe51c017..7e8bfba987 100644 --- a/service/src/main/java/com/github/kr328/clash/service/util/Serializers.kt +++ b/service/src/main/java/com/github/kr328/clash/service/util/Serializers.kt @@ -1,5 +1,6 @@ package com.github.kr328.clash.service.util +import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.KSerializer import kotlinx.serialization.descriptors.PrimitiveKind import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor @@ -8,6 +9,7 @@ import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder import java.util.* +@OptIn(ExperimentalSerializationApi::class) class UUIDSerializer : KSerializer { override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("UUID", PrimitiveKind.STRING)