Refactor per-app proxy logic to ViewModel

This commit is contained in:
2dust committed 2026-01-13 10:57:42 +08:00
1 parent 68ed08d36e
commit 4a3686fb32
3 files changed
+101 -69

No files matched your search

@@ -7,6 +7,7 @@ import android.util.Log
import android.view.Menu
import android.view.MenuItem
import android.widget.Toast
import androidx.activity.viewModels
import androidx.appcompat.widget.SearchView
import androidx.lifecycle.lifecycleScope
import com.v2ray.ang.AppConfig
@@ -23,6 +24,7 @@ import com.v2ray.ang.handler.SettingsManager
import com.v2ray.ang.util.AppManagerUtil
import com.v2ray.ang.util.HttpUtil
import com.v2ray.ang.util.Utils
import com.v2ray.ang.viewmodel.PerAppProxyViewModel
import es.dmoral.toasty.Toasty
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -34,6 +36,7 @@ class PerAppProxyActivity : BaseActivity() {
private var adapter: PerAppProxyAdapter? = null
private var appsAll: List<AppInfo>? = null
private val viewModel: PerAppProxyViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -64,14 +67,13 @@ class PerAppProxyActivity : BaseActivity() {
lifecycleScope.launch {
try {
val blacklist =
MmkvManager.decodeSettingsStringSet(AppConfig.PREF_PER_APP_PROXY_SET)
val apps = withContext(Dispatchers.IO) {
val appsList = AppManagerUtil.loadNetworkAppList(this@PerAppProxyActivity)
if (blacklist != null) {
val blacklistSet = viewModel.getAll()
if (blacklistSet.isNotEmpty()) {
appsList.forEach { app ->
app.isSelected = if (blacklist.contains(app.packageName)) 1 else 0
app.isSelected = if (blacklistSet.contains(app.packageName)) 1 else 0
}
appsList.sortedWith { p1, p2 ->
when {
@@ -93,8 +95,9 @@ class PerAppProxyActivity : BaseActivity() {
}
appsAll = apps
adapter = PerAppProxyAdapter(this@PerAppProxyActivity, apps, blacklist)
adapter = PerAppProxyAdapter(this@PerAppProxyActivity, apps, viewModel)
binding.recyclerView.adapter = adapter
} catch (e: Exception) {
Log.e(ANG_PACKAGE, "Error loading apps", e)
} finally {
@@ -103,13 +106,6 @@ class PerAppProxyActivity : BaseActivity() {
}
}
override fun onPause() {
super.onPause()
adapter?.let {
MmkvManager.encodeSettings(AppConfig.PREF_PER_APP_PROXY_SET, it.blacklist)
}
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.menu_bypass_list, menu)
@@ -164,33 +160,23 @@ class PerAppProxyActivity : BaseActivity() {
}
private fun selectAllApp() {
adapter?.let { it ->
val pkgNames = it.apps.map { it.packageName }
if (it.blacklist.containsAll(pkgNames)) {
it.apps.forEach {
val packageName = it.packageName
adapter?.blacklist?.remove(packageName)
}
adapter?.let { adapter ->
val pkgNames = adapter.apps.map { it.packageName }
val allSelected = pkgNames.all { viewModel.contains(it) }
if (allSelected) {
viewModel.removeAll(pkgNames)
} else {
it.apps.forEach {
val packageName = it.packageName
adapter?.blacklist?.add(packageName)
}
viewModel.addAll(pkgNames)
}
it.notifyDataSetChanged()
true
refreshData()
}
}
private fun invertSelection() {
adapter?.let { adapter ->
adapter.apps.forEach { app ->
val packageName = app.packageName
if (adapter.blacklist.contains(packageName)) {
adapter.blacklist.remove(packageName)
} else {
adapter.blacklist.add(packageName)
}
viewModel.toggle(app.packageName)
}
refreshData()
}
@@ -226,8 +212,8 @@ class PerAppProxyActivity : BaseActivity() {
private fun exportProxyApp() {
var lst = binding.switchBypassApps.isChecked.toString()
adapter?.blacklist?.forEach block@{
lst = lst + System.lineSeparator() + it
viewModel.getAll().forEach { pkg ->
lst = lst + System.lineSeparator() + pkg
}
Utils.setClipboard(applicationContext, lst)
toastSuccess(R.string.toast_success)
@@ -248,29 +234,27 @@ class PerAppProxyActivity : BaseActivity() {
}
if (TextUtils.isEmpty(proxyApps)) return false
adapter?.blacklist?.clear()
viewModel.clear()
if (binding.switchBypassApps.isChecked) {
adapter?.let { it ->
it.apps.forEach block@{
val packageName = it.packageName
adapter?.let { adapter ->
adapter.apps.forEach { app ->
val packageName = app.packageName
if (!inProxyApps(proxyApps, packageName, force)) {
adapter?.blacklist?.add(packageName)
return@block
viewModel.add(packageName)
}
}
it.notifyDataSetChanged()
refreshData()
}
} else {
adapter?.let { it ->
it.apps.forEach block@{
val packageName = it.packageName
adapter?.let { adapter ->
adapter.apps.forEach { app ->
val packageName = app.packageName
if (inProxyApps(proxyApps, packageName, force)) {
adapter?.blacklist?.add(packageName)
return@block
viewModel.add(packageName)
}
}
it.notifyDataSetChanged()
refreshData()
}
}
} catch (e: Exception) {
@@ -308,7 +292,7 @@ class PerAppProxyActivity : BaseActivity() {
}
}
adapter = PerAppProxyAdapter(this, apps, adapter?.blacklist)
adapter = PerAppProxyAdapter(this, apps, adapter?.viewModel ?: viewModel)
binding.recyclerView.adapter = adapter
refreshData()
return true
@@ -6,9 +6,9 @@ import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.v2ray.ang.databinding.ItemRecyclerBypassListBinding
import com.v2ray.ang.dto.AppInfo
import com.v2ray.ang.handler.SettingsChangeManager
import com.v2ray.ang.viewmodel.PerAppProxyViewModel
class PerAppProxyAdapter(val activity: BaseActivity, val apps: List<AppInfo>, blacklist: MutableSet<String>?) :
class PerAppProxyAdapter(val activity: BaseActivity, val apps: List<AppInfo>, val viewModel: PerAppProxyViewModel) :
RecyclerView.Adapter<PerAppProxyAdapter.BaseViewHolder>() {
companion object {
@@ -16,8 +16,6 @@ class PerAppProxyAdapter(val activity: BaseActivity, val apps: List<AppInfo>, bl
private const val VIEW_TYPE_ITEM = 1
}
val blacklist = if (blacklist == null) HashSet() else HashSet(blacklist)
override fun onBindViewHolder(holder: BaseViewHolder, position: Int) {
if (holder is AppViewHolder) {
val appInfo = apps[position - 1]
@@ -39,11 +37,7 @@ class PerAppProxyAdapter(val activity: BaseActivity, val apps: List<AppInfo>, bl
)
BaseViewHolder(view)
}
// VIEW_TYPE_ITEM -> AppViewHolder(ctx.layoutInflater
// .inflate(R.layout.item_recycler_bypass_list, parent, false))
else -> AppViewHolder(ItemRecyclerBypassListBinding.inflate(LayoutInflater.from(ctx), parent, false))
}
}
@@ -53,13 +47,11 @@ class PerAppProxyAdapter(val activity: BaseActivity, val apps: List<AppInfo>, bl
inner class AppViewHolder(private val itemBypassBinding: ItemRecyclerBypassListBinding) : BaseViewHolder(itemBypassBinding.root),
View.OnClickListener {
private val inBlacklist: Boolean get() = blacklist.contains(appInfo.packageName)
private lateinit var appInfo: AppInfo
fun bind(appInfo: AppInfo) {
this.appInfo = appInfo
// Set app icon and name
itemBypassBinding.icon.setImageDrawable(appInfo.appIcon)
itemBypassBinding.name.text = if (appInfo.isSystemApp) {
String.format("** %s", appInfo.appName)
@@ -67,24 +59,16 @@ class PerAppProxyAdapter(val activity: BaseActivity, val apps: List<AppInfo>, bl
appInfo.appName
}
// Set package name and checkbox state
itemBypassBinding.packageName.text = appInfo.packageName
itemBypassBinding.checkBox.isChecked = inBlacklist
itemBypassBinding.checkBox.isChecked = viewModel.contains(appInfo.packageName)
// Handle item click to toggle blacklist status
itemView.setOnClickListener(this)
}
override fun onClick(v: View?) {
if (inBlacklist) {
blacklist.remove(appInfo.packageName)
itemBypassBinding.checkBox.isChecked = false
} else {
blacklist.add(appInfo.packageName)
itemBypassBinding.checkBox.isChecked = true
}
SettingsChangeManager.makeRestartService()
val packageName = appInfo.packageName
viewModel.toggle(packageName)
itemBypassBinding.checkBox.isChecked = viewModel.contains(packageName)
}
}
}
@@ -0,0 +1,64 @@
package com.v2ray.ang.viewmodel
import androidx.lifecycle.ViewModel
import com.v2ray.ang.AppConfig
import com.v2ray.ang.handler.MmkvManager
import com.v2ray.ang.handler.SettingsChangeManager
class PerAppProxyViewModel : ViewModel() {
private val blacklist: MutableSet<String> = MmkvManager.decodeSettingsStringSet(AppConfig.PREF_PER_APP_PROXY_SET)?.let {
HashSet(it)
} ?: HashSet()
fun contains(packageName: String): Boolean = blacklist.contains(packageName)
fun getAll(): Set<String> = blacklist.toSet()
fun add(packageName: String): Boolean {
val changed = blacklist.add(packageName)
if (changed) {
save()
}
return changed
}
fun remove(packageName: String): Boolean {
val changed = blacklist.remove(packageName)
if (changed) {
save()
}
return changed
}
fun toggle(packageName: String) {
if (blacklist.contains(packageName)) {
remove(packageName)
} else {
add(packageName)
}
}
fun addAll(packages: Collection<String>) {
if (blacklist.addAll(packages)) {
save()
}
}
fun removeAll(packages: Collection<String>) {
if (blacklist.removeAll(packages.toSet())) {
save()
}
}
fun clear() {
if (blacklist.isNotEmpty()) {
blacklist.clear()
save()
}
}
private fun save() {
MmkvManager.encodeSettings(AppConfig.PREF_PER_APP_PROXY_SET, blacklist)
SettingsChangeManager.makeRestartService()
}
}