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)