Support custom outbound tags in routing
Allow routing rules to reference outbounds by server remarks and inject missing outbounds into the generated V2ray config. Adds BUILTIN_OUTBOUND_TAGS, a pre-pass (injectCustomOutbounds) that converts referenced non-builtin tags to outbounds, and a fallback to the proxy tag when an outbound couldn't be injected. Updates getBalance to use AppConfig.TAG_PROXY constant. Replaces the outbound-tag Spinner with an AutoCompleteTextView + dropdown button that suggests builtin tags and existing profile remarks, adds a drop-down drawable and a hint string resource.
This commit is contained in:
1 parent
f371da490a
commit
04478ab490
6 files changed
+132
-15
No files matched your search
@@ -279,4 +279,9 @@ object AppConfig {
|
||||
"Chocolate4U/Iran-v2ray-rules"
|
||||
)
|
||||
|
||||
val BUILTIN_OUTBOUND_TAGS = setOf(
|
||||
TAG_PROXY,
|
||||
TAG_DIRECT,
|
||||
TAG_BLOCKED,
|
||||
)
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import android.content.Context
|
||||
import android.text.TextUtils
|
||||
import com.google.gson.JsonArray
|
||||
import com.v2ray.ang.AppConfig
|
||||
import com.v2ray.ang.AppConfig.BUILTIN_OUTBOUND_TAGS
|
||||
import com.v2ray.ang.dto.ConfigResult
|
||||
import com.v2ray.ang.dto.ProfileItem
|
||||
import com.v2ray.ang.dto.RulesetItem
|
||||
@@ -445,6 +446,45 @@ object V2rayConfigManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pre-pass: scans all routing rulesets for non-builtin outbound tags,
|
||||
* looks up the matching profile by remarks, converts it to an OutboundBean,
|
||||
* and appends it to v2rayConfig.outbounds. This must run before getRouting
|
||||
* so that every custom tag is already present when routing rules are applied.
|
||||
*
|
||||
* @param v2rayConfig The V2ray configuration object to be modified
|
||||
*/
|
||||
private fun injectCustomOutbounds(v2rayConfig: V2rayConfig) {
|
||||
val existingTags = v2rayConfig.outbounds.mapTo(mutableSetOf()) { it.tag }
|
||||
val rulesetItems = MmkvManager.decodeRoutingRulesets() ?: return
|
||||
|
||||
rulesetItems
|
||||
.filter { it.enabled }
|
||||
.mapNotNull { it.outboundTag.takeIf { tag -> tag.isNotBlank() } }
|
||||
.filter { it !in BUILTIN_OUTBOUND_TAGS }
|
||||
.distinct()
|
||||
.forEach { tag ->
|
||||
if (tag in existingTags) return@forEach
|
||||
try {
|
||||
val profile = SettingsManager.getServerViaRemarks(tag) ?: run {
|
||||
LogUtil.w(AppConfig.TAG, "Custom outbound tag '$tag' not found by remarks, skipping")
|
||||
return@forEach
|
||||
}
|
||||
val outbound = convertProfile2Outbound(profile) ?: run {
|
||||
LogUtil.w(AppConfig.TAG, "Could not convert profile '$tag' to outbound, skipping")
|
||||
return@forEach
|
||||
}
|
||||
updateOutboundWithGlobalSettings(outbound)
|
||||
outbound.tag = tag
|
||||
v2rayConfig.outbounds.add(outbound)
|
||||
existingTags.add(tag)
|
||||
LogUtil.d(AppConfig.TAG, "Injected custom outbound: tag='$tag'")
|
||||
} catch (e: Exception) {
|
||||
LogUtil.e(AppConfig.TAG, "Failed to inject custom outbound for tag '$tag', skipping", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures routing settings for V2ray.
|
||||
*
|
||||
@@ -460,6 +500,9 @@ object V2rayConfigManager {
|
||||
MmkvManager.decodeSettingsString(AppConfig.PREF_ROUTING_DOMAIN_STRATEGY)
|
||||
?: "AsIs"
|
||||
|
||||
// Pre-pass: inject outbounds referenced by remarks in routing rules
|
||||
injectCustomOutbounds(v2rayConfig)
|
||||
|
||||
val rulesetItems = MmkvManager.decodeRoutingRulesets()
|
||||
rulesetItems?.forEach { key ->
|
||||
getRoutingUserRule(context, key, v2rayConfig)
|
||||
@@ -510,6 +553,16 @@ object V2rayConfigManager {
|
||||
rule.process = null
|
||||
}
|
||||
|
||||
// If the outbound tag is a custom one that failed to inject, fall back to proxy
|
||||
val outboundTag = rule.outboundTag
|
||||
if (!outboundTag.isNullOrBlank()
|
||||
&& outboundTag !in BUILTIN_OUTBOUND_TAGS
|
||||
&& v2rayConfig.outbounds.none { it.tag == outboundTag }
|
||||
) {
|
||||
LogUtil.w(AppConfig.TAG, "Outbound tag '$outboundTag' not found, falling back to '${AppConfig.TAG_PROXY}'")
|
||||
rule.outboundTag = AppConfig.TAG_PROXY
|
||||
}
|
||||
|
||||
v2rayConfig.routing.rules.add(rule)
|
||||
|
||||
} catch (e: Exception) {
|
||||
@@ -888,7 +941,7 @@ object V2rayConfigManager {
|
||||
private fun getBalance(v2rayConfig: V2rayConfig, config: ProfileItem) {
|
||||
try {
|
||||
v2rayConfig.routing.rules.forEach { rule ->
|
||||
if (rule.outboundTag == "proxy") {
|
||||
if (rule.outboundTag == AppConfig.TAG_PROXY) {
|
||||
rule.outboundTag = null
|
||||
rule.balancerTag = AppConfig.TAG_BALANCER
|
||||
}
|
||||
|
||||
@@ -3,14 +3,18 @@ package com.v2ray.ang.ui
|
||||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.widget.ArrayAdapter
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.v2ray.ang.AppConfig.BUILTIN_OUTBOUND_TAGS
|
||||
import com.v2ray.ang.AppConfig.TAG_PROXY
|
||||
import com.v2ray.ang.R
|
||||
import com.v2ray.ang.databinding.ActivityRoutingEditBinding
|
||||
import com.v2ray.ang.dto.RulesetItem
|
||||
import com.v2ray.ang.extension.nullIfBlank
|
||||
import com.v2ray.ang.extension.toast
|
||||
import com.v2ray.ang.extension.toastSuccess
|
||||
import com.v2ray.ang.handler.MmkvManager
|
||||
import com.v2ray.ang.handler.SettingsManager
|
||||
import com.v2ray.ang.util.Utils
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -20,15 +24,12 @@ class RoutingEditActivity : BaseActivity() {
|
||||
private val binding by lazy { ActivityRoutingEditBinding.inflate(layoutInflater) }
|
||||
private val position by lazy { intent.getIntExtra("position", -1) }
|
||||
|
||||
private val outbound_tag: Array<out String> by lazy {
|
||||
resources.getStringArray(R.array.outbound_tag)
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
//setContentView(binding.root)
|
||||
setContentViewWithToolbar(binding.root, showHomeAsUp = true, title = getString(R.string.routing_settings_rule_title))
|
||||
|
||||
setupOutboundTagInput()
|
||||
|
||||
val rulesetItem = SettingsManager.getRoutingRuleset(position)
|
||||
if (rulesetItem != null) {
|
||||
bindingServer(rulesetItem)
|
||||
@@ -41,6 +42,33 @@ class RoutingEditActivity : BaseActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the AutoCompleteTextView for outbound tag:
|
||||
* suggestions = built-in tags (proxy/direct/block) + all existing profile remarks.
|
||||
* The dropdown button triggers showing the full list without typing.
|
||||
*/
|
||||
private fun setupOutboundTagInput() {
|
||||
val profileRemarks = MmkvManager.decodeAllServerList()
|
||||
.mapNotNull { id -> MmkvManager.decodeServerConfig(id)?.remarks }
|
||||
.filter { it.isNotBlank() }
|
||||
|
||||
val suggestions = (BUILTIN_OUTBOUND_TAGS.toList() + profileRemarks).distinct()
|
||||
val adapter = ArrayAdapter(this, android.R.layout.simple_dropdown_item_1line, suggestions)
|
||||
binding.spOutboundTag.setAdapter(adapter)
|
||||
// threshold=0 means show all suggestions even before typing; still need focus+request
|
||||
binding.spOutboundTag.threshold = 0
|
||||
|
||||
// Dropdown arrow button shows the full suggestion list
|
||||
binding.btnOutboundTagDropdown.setOnClickListener {
|
||||
binding.spOutboundTag.requestFocus()
|
||||
binding.spOutboundTag.showDropDown()
|
||||
}
|
||||
// Also show on field click when it already has focus
|
||||
binding.spOutboundTag.setOnClickListener {
|
||||
binding.spOutboundTag.showDropDown()
|
||||
}
|
||||
}
|
||||
|
||||
private fun bindingServer(rulesetItem: RulesetItem): Boolean {
|
||||
binding.etRemarks.text = Utils.getEditable(rulesetItem.remarks)
|
||||
binding.chkLocked.isChecked = rulesetItem.locked == true
|
||||
@@ -50,15 +78,14 @@ class RoutingEditActivity : BaseActivity() {
|
||||
binding.etPort.text = Utils.getEditable(rulesetItem.port)
|
||||
binding.etProtocol.text = Utils.getEditable(rulesetItem.protocol?.joinToString(","))
|
||||
binding.etNetwork.text = Utils.getEditable(rulesetItem.network)
|
||||
val outbound = Utils.arrayFind(outbound_tag, rulesetItem.outboundTag)
|
||||
binding.spOutboundTag.setSelection(outbound)
|
||||
|
||||
// Set text directly; filter won't fire because we're not using setText(filter=true)
|
||||
binding.spOutboundTag.setText(rulesetItem.outboundTag, false)
|
||||
return true
|
||||
}
|
||||
|
||||
private fun clearServer(): Boolean {
|
||||
binding.etRemarks.text = null
|
||||
binding.spOutboundTag.setSelection(0)
|
||||
binding.spOutboundTag.setText(BUILTIN_OUTBOUND_TAGS.first(), false)
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -74,7 +101,7 @@ class RoutingEditActivity : BaseActivity() {
|
||||
protocol = binding.etProtocol.text.toString().nullIfBlank()?.split(",")?.map { it.trim() }?.filter { it.isNotEmpty() }
|
||||
port = binding.etPort.text.toString().nullIfBlank()
|
||||
network = binding.etNetwork.text.toString().nullIfBlank()
|
||||
outboundTag = outbound_tag[binding.spOutboundTag.selectedItemPosition]
|
||||
outboundTag = binding.spOutboundTag.text.toString().trim().ifEmpty { TAG_PROXY }
|
||||
}
|
||||
|
||||
if (rulesetItem.remarks.isNullOrEmpty()) {
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<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="?attr/colorControlNormal"
|
||||
android:pathData="M7,10l5,5 5,-5z" />
|
||||
</vector>
|
||||
|
||||
@@ -190,13 +190,33 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/routing_settings_outbound_tag" />
|
||||
|
||||
<Spinner
|
||||
android:id="@+id/sp_outbound_tag"
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/padding_spacing_dp8"
|
||||
android:layout_marginBottom="@dimen/padding_spacing_dp8"
|
||||
android:entries="@array/outbound_tag" />
|
||||
android:layout_marginBottom="@dimen/padding_spacing_dp8">
|
||||
|
||||
<AutoCompleteTextView
|
||||
android:id="@+id/sp_outbound_tag"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:completionThreshold="0"
|
||||
android:hint="@string/routing_settings_outbound_tag_hint"
|
||||
android:inputType="text"
|
||||
android:imeOptions="actionDone"
|
||||
android:paddingEnd="40dp" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btn_outbound_tag_dropdown"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/routing_settings_outbound_tag"
|
||||
android:src="@drawable/ic_arrow_drop_down" />
|
||||
|
||||
</RelativeLayout>
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
@@ -342,6 +342,7 @@
|
||||
<string name="routing_settings_protocol_tip">[http,tls,bittorrent]</string>
|
||||
<string name="routing_settings_network">network</string>
|
||||
<string name="routing_settings_network_tip">[udp|tcp]</string>
|
||||
<string name="routing_settings_outbound_tag_hint" translatable="false">proxy / direct / block / <remarks></string>
|
||||
<string name="routing_settings_outbound_tag">outboundTag</string>
|
||||
|
||||
<string name="connection_test_pending">Check Connectivity</string>
|
||||
|
||||
Reference in new issue
Block a user