Compare commits

..

12 Commits
1.9.4 ... 1.9.5

38 changed files with 370 additions and 140 deletions

View File

@@ -11,8 +11,8 @@ android {
applicationId = "com.v2ray.ang"
minSdk = 21
targetSdk = 34
versionCode = 597
versionName = "1.9.4"
versionCode = 598
versionName = "1.9.5"
multiDexEnabled = true
splits {
abi {

View File

@@ -150,6 +150,7 @@ object AppConfig {
const val WIREGUARD = "wireguard://"
const val TUIC = "tuic://"
const val HYSTERIA2 = "hysteria2://"
const val HY2 = "hy2://"
/** Give a good name to this, IDK*/
const val VPN = "VPN"

View File

@@ -0,0 +1,9 @@
package com.v2ray.ang.dto
data class ConfigResult (
var status: Boolean,
var guid: String? = null,
var content: String = "",
var domainPort: String? = null,
)

View File

@@ -4,9 +4,20 @@ data class Hysteria2Bean(
val server: String?,
val auth: String?,
val lazy: Boolean? = true,
val socks5: Socks5Bean?,
val tls: TlsBean?
val obfs: ObfsBean? = null,
val socks5: Socks5Bean? = null,
val http: Socks5Bean? = null,
val tls: TlsBean? = null,
) {
data class ObfsBean(
val type: String?,
val salamander: SalamanderBean?
) {
data class SalamanderBean(
val password: String?,
)
}
data class Socks5Bean(
val listen: String?,
)
@@ -15,4 +26,4 @@ data class Hysteria2Bean(
val sni: String?,
val insecure: Boolean?,
)
}
}

View File

@@ -104,7 +104,8 @@ data class V2rayConfig(
var secretKey: String? = null,
val peers: List<WireGuardBean>? = null,
var reserved: List<Int>? = null,
var mtu: Int? = null
var mtu: Int? = null,
var obfsPassword: String? = null,
) {
data class VnextBean(

View File

@@ -5,10 +5,12 @@ import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.Build
import android.os.Bundle
import android.widget.Toast
import com.v2ray.ang.AngApplication
import me.drakeet.support.toast.ToastCompat
import org.json.JSONObject
import java.io.Serializable
import java.net.URI
import java.net.URLConnection
@@ -81,4 +83,14 @@ fun Context.listenForPackageChanges(onetime: Boolean = true, callback: () -> Uni
addDataScheme("package")
})
}
}
}
inline fun <reified T : Serializable> Bundle.serializable(key: String): T? = when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU -> getSerializable(key, T::class.java)
else -> @Suppress("DEPRECATION") getSerializable(key) as? T
}
inline fun <reified T : Serializable> Intent.serializable(key: String): T? = when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU -> getSerializableExtra(key, T::class.java)
else -> @Suppress("DEPRECATION") getSerializableExtra(key) as? T
}

View File

@@ -15,6 +15,7 @@ import com.v2ray.ang.AppConfig.SUBSCRIPTION_UPDATE_CHANNEL
import com.v2ray.ang.AppConfig.SUBSCRIPTION_UPDATE_CHANNEL_NAME
import com.v2ray.ang.R
import com.v2ray.ang.util.AngConfigManager
import com.v2ray.ang.util.AngConfigManager.updateConfigViaSub
import com.v2ray.ang.util.MmkvManager
import com.v2ray.ang.util.Utils
@@ -40,8 +41,8 @@ object SubscriptionUpdater {
val subs = MmkvManager.decodeSubscriptions().filter { it.second.autoUpdate }
for (i in subs) {
val subscription = i.second
for (sub in subs) {
val subItem = sub.second
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
notification.setChannelId(SUBSCRIPTION_UPDATE_CHANNEL)
@@ -56,11 +57,10 @@ object SubscriptionUpdater {
notificationManager.notify(3, notification.build())
Log.d(
AppConfig.ANG_PACKAGE,
"subscription automatic update: ---${subscription.remarks}"
"subscription automatic update: ---${subItem.remarks}"
)
val configs = Utils.getUrlContentWithCustomUserAgent(subscription.url)
AngConfigManager.importBatchConfig(configs, i.first, false)
notification.setContentText("Updating ${subscription.remarks}")
updateConfigViaSub(Pair(sub.first, subItem))
notification.setContentText("Updating ${subItem.remarks}")
}
notificationManager.cancel(3)
return Result.success()

View File

@@ -164,7 +164,7 @@ object V2RayServiceManager {
MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_START_SUCCESS, "")
showNotification()
PluginUtil.runPlugin(service, config)
PluginUtil.runPlugin(service, config, result.domainPort)
} else {
MessageUtil.sendMsg2UI(service, AppConfig.MSG_STATE_START_FAILURE, "")
cancelNotification()

View File

@@ -3,12 +3,18 @@ package com.v2ray.ang.service
import android.app.Service
import android.content.Intent
import android.os.IBinder
import android.util.Log
import com.v2ray.ang.AppConfig.MSG_MEASURE_CONFIG
import com.v2ray.ang.AppConfig.MSG_MEASURE_CONFIG_CANCEL
import com.v2ray.ang.AppConfig.MSG_MEASURE_CONFIG_SUCCESS
import com.v2ray.ang.dto.EConfigType
import com.v2ray.ang.extension.serializable
import com.v2ray.ang.util.MessageUtil
import com.v2ray.ang.util.MmkvManager
import com.v2ray.ang.util.PluginUtil
import com.v2ray.ang.util.SpeedtestUtil
import com.v2ray.ang.util.Utils
import com.v2ray.ang.util.V2rayConfigUtil
import go.Seq
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
@@ -30,10 +36,10 @@ class V2RayTestService : Service() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
when (intent?.getIntExtra("key", 0)) {
MSG_MEASURE_CONFIG -> {
val contentPair = intent.getSerializableExtra("content") as Pair<String, String>
val guid = intent.serializable<String>("content") ?: ""
realTestScope.launch {
val result = SpeedtestUtil.realPing(contentPair.second)
MessageUtil.sendMsg2UI(this@V2RayTestService, MSG_MEASURE_CONFIG_SUCCESS, Pair(contentPair.first, result))
val result = startRealPing(guid)
MessageUtil.sendMsg2UI(this@V2RayTestService, MSG_MEASURE_CONFIG_SUCCESS, Pair(guid, result))
}
}
@@ -47,4 +53,29 @@ class V2RayTestService : Service() {
override fun onBind(intent: Intent?): IBinder? {
return null
}
private fun startRealPing(guid: String): Long {
val retFailure = -1L
val server = MmkvManager.decodeServerConfig(guid) ?: return retFailure
if (server.getProxyOutbound()?.protocol?.equals(EConfigType.HYSTERIA2.name, true) == true) {
val socksPort = Utils.findFreePort(listOf(0))
PluginUtil.runPlugin(this, server, "0:${socksPort}")
Thread.sleep(1000L)
var delay = SpeedtestUtil.testConnection(this, socksPort)
if (delay.first < 0) {
Thread.sleep(10L)
delay = SpeedtestUtil.testConnection(this, socksPort)
}
PluginUtil.stopPlugin()
return delay.first
} else {
val config = V2rayConfigUtil.getV2rayConfig(this, guid)
if (!config.status) {
return retFailure
}
return SpeedtestUtil.realPing(config.content)
}
}
}

View File

@@ -203,7 +203,7 @@ class V2RayVpnService : VpnService(), ServiceControl {
}
private fun runTun2socks() {
val socksPort = Utils.parseInt(settingsStorage?.decodeString(AppConfig.PREF_SOCKS_PORT), AppConfig.PORT_SOCKS.toInt())
val socksPort = SettingsManager.getSocksPort()
val cmd = arrayListOf(
File(applicationContext.applicationInfo.nativeLibraryDir, TUN2SOCKS).absolutePath,
"--netif-ipaddr", PRIVATE_VLAN4_ROUTER,

View File

@@ -10,12 +10,14 @@ import androidx.appcompat.app.AlertDialog
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import com.v2ray.ang.AppConfig
import com.v2ray.ang.R
import com.v2ray.ang.databinding.ActivityRoutingSettingBinding
import com.v2ray.ang.dto.RulesetItem
import com.v2ray.ang.extension.toast
import com.v2ray.ang.helper.SimpleItemTouchHelperCallback
import com.v2ray.ang.util.JsonUtil
import com.v2ray.ang.util.MmkvManager
import com.v2ray.ang.util.MmkvManager.settingsStorage
import com.v2ray.ang.util.SettingsManager
@@ -108,6 +110,44 @@ class RoutingSettingActivity : BaseActivity() {
true
}
R.id.import_rulesets_from_clipboard -> {
AlertDialog.Builder(this).setMessage(R.string.routing_settings_import_rulesets_tip)
.setPositiveButton(android.R.string.ok) { _, _ ->
try {
val clipboard = Utils.getClipboard(this)
lifecycleScope.launch(Dispatchers.IO) {
val ret = SettingsManager.resetRoutingRulesetsFromClipboard(clipboard)
launch(Dispatchers.Main) {
if (ret) {
refreshData()
toast(R.string.toast_success)
} else {
toast(R.string.toast_failure)
}
}
}
} catch (e: Exception) {
e.printStackTrace()
}
}
.setNegativeButton(android.R.string.no) { _, _ ->
//do noting
}
.show()
true
}
R.id.export_rulesets_to_clipboard -> {
val rulesetList = MmkvManager.decodeRoutingRulesets()
if (rulesetList.isNullOrEmpty()) {
toast(R.string.toast_failure)
} else {
Utils.setClipboard(this, JsonUtil.toJson(rulesetList))
toast(R.string.toast_success)
}
true
}
else -> super.onOptionsItemSelected(item)
}

View File

@@ -36,7 +36,8 @@ class RoutingSettingRecyclerAdapter(val activity: RoutingSettingActivity) : Recy
)
}
holder.itemRoutingSettingBinding.chkEnable.setOnCheckedChangeListener { _, isChecked ->
holder.itemRoutingSettingBinding.chkEnable.setOnCheckedChangeListener { it, isChecked ->
if( !it.isPressed) return@setOnCheckedChangeListener
ruleset.enabled = isChecked
SettingsManager.saveRoutingRuleset(position, ruleset)
}

View File

@@ -118,6 +118,7 @@ class ServerActivity : BaseActivity() {
private val et_reserved3: EditText? by lazy { findViewById(R.id.et_reserved3) }
private val et_local_address: EditText? by lazy { findViewById(R.id.et_local_address) }
private val et_local_mtu: EditText? by lazy { findViewById(R.id.et_local_mtu) }
private val et_obfs_password: EditText? by lazy { findViewById(R.id.et_obfs_password) }
override fun onCreate(savedInstanceState: Bundle?) {
@@ -292,7 +293,10 @@ class ServerActivity : BaseActivity() {
} else {
et_local_mtu?.text = Utils.getEditable(outbound.settings?.mtu.toString())
}
} else if (config.configType == EConfigType.HYSTERIA2) {
et_obfs_password?.text = Utils.getEditable(outbound.settings?.obfsPassword)
}
val securityEncryptions =
if (config.configType == EConfigType.SHADOWSOCKS) shadowsocksSecuritys else securitys
val security =
@@ -455,6 +459,9 @@ class ServerActivity : BaseActivity() {
if (config.subscriptionId.isEmpty() && !subscriptionId.isNullOrEmpty()) {
config.subscriptionId = subscriptionId.orEmpty()
}
if (config.configType == EConfigType.HYSTERIA2) {
config.outboundBean?.settings?.obfsPassword = et_obfs_password?.text?.toString()
}
MmkvManager.encodeServerConfig(editGuid, config)
toast(R.string.toast_success)

View File

@@ -8,13 +8,14 @@ import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import com.blacksquircle.ui.editorkit.utils.EditorTheme
import com.blacksquircle.ui.language.json.JsonLanguage
import com.google.gson.Gson
import com.v2ray.ang.R
import com.v2ray.ang.databinding.ActivityServerCustomConfigBinding
import com.v2ray.ang.dto.EConfigType
import com.v2ray.ang.dto.ServerConfig
import com.v2ray.ang.dto.V2rayConfig
import com.v2ray.ang.extension.toast
import com.v2ray.ang.util.JsonUtil
import com.v2ray.ang.util.MmkvManager
import com.v2ray.ang.util.Utils
import me.drakeet.support.toast.ToastCompat
@@ -78,7 +79,7 @@ class ServerCustomConfigActivity : BaseActivity() {
}
val v2rayConfig = try {
Gson().fromJson(binding.editor.text.toString(), V2rayConfig::class.java)
JsonUtil.fromJson(binding.editor.text.toString(), V2rayConfig::class.java)
} catch (e: Exception) {
e.printStackTrace()
ToastCompat.makeText(this, "${getString(R.string.toast_malformed_josn)} ${e.cause?.message}", Toast.LENGTH_LONG).show()

View File

@@ -44,7 +44,8 @@ class SubSettingRecyclerAdapter(val activity: SubSettingActivity) : RecyclerView
)
}
holder.itemSubSettingBinding.chkEnable.setOnCheckedChangeListener { _, isChecked ->
holder.itemSubSettingBinding.chkEnable.setOnCheckedChangeListener { it, isChecked ->
if( !it.isPressed) return@setOnCheckedChangeListener
subItem.enabled = isChecked
MmkvManager.encodeSubscription(subId, subItem)

View File

@@ -31,6 +31,7 @@ import com.v2ray.ang.extension.toTrafficString
import com.v2ray.ang.extension.toast
import com.v2ray.ang.util.MmkvManager
import com.v2ray.ang.util.MmkvManager.settingsStorage
import com.v2ray.ang.util.SettingsManager
import com.v2ray.ang.util.Utils
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -176,7 +177,7 @@ class UserAssetActivity : BaseActivity() {
.show()
toast(R.string.msg_downloading_content)
val httpPort = Utils.parseInt(settingsStorage?.decodeString(AppConfig.PREF_HTTP_PORT), AppConfig.PORT_HTTP.toInt())
val httpPort = SettingsManager.getHttpPort()
var assets = MmkvManager.decodeAssetUrls()
assets = addBuiltInGeoItems(assets)

View File

@@ -4,16 +4,16 @@ import android.content.Context
import android.graphics.Bitmap
import android.text.TextUtils
import android.util.Log
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.google.gson.JsonPrimitive
import com.google.gson.JsonSerializationContext
import com.google.gson.JsonSerializer
import com.google.gson.reflect.TypeToken
import com.v2ray.ang.AppConfig
import com.v2ray.ang.AppConfig.HY2
import com.v2ray.ang.R
import com.v2ray.ang.dto.*
import com.v2ray.ang.util.MmkvManager.settingsStorage
import com.v2ray.ang.util.fmt.Hysteria2Fmt
import com.v2ray.ang.util.fmt.ShadowsocksFmt
import com.v2ray.ang.util.fmt.SocksFmt
@@ -52,7 +52,7 @@ object AngConfigManager {
VlessFmt.parse(str)
} else if (str.startsWith(EConfigType.WIREGUARD.protocolScheme)) {
WireguardFmt.parse(str)
} else if (str.startsWith(EConfigType.HYSTERIA2.protocolScheme)) {
} else if (str.startsWith(EConfigType.HYSTERIA2.protocolScheme) || str.startsWith(HY2)) {
Hysteria2Fmt.parse(str)
} else {
null
@@ -278,34 +278,21 @@ object AngConfigManager {
&& server.contains("routing")
) {
try {
//val gson = GsonBuilder().setPrettyPrinting().create()
val gson = GsonBuilder()
.setPrettyPrinting()
.disableHtmlEscaping()
.registerTypeAdapter( // custom serialiser is needed here since JSON by default parse number as Double, core will fail to start
object : TypeToken<Double>() {}.type,
JsonSerializer { src: Double?, _: Type?, _: JsonSerializationContext? ->
JsonPrimitive(
src?.toInt()
)
}
)
.create()
val serverList: Array<Any> =
Gson().fromJson(server, Array<Any>::class.java)
JsonUtil.fromJson(server, Array<Any>::class.java)
if (serverList.isNotEmpty()) {
var count = 0
for (srv in serverList.reversed()) {
val config = ServerConfig.create(EConfigType.CUSTOM)
config.fullConfig =
Gson().fromJson(Gson().toJson(srv), V2rayConfig::class.java)
JsonUtil.fromJson(JsonUtil.toJson(srv), V2rayConfig::class.java)
config.remarks = config.fullConfig?.remarks
?: ("%04d-".format(count + 1) + System.currentTimeMillis()
.toString())
config.subscriptionId = subid
val key = MmkvManager.encodeServerConfig("", config)
MmkvManager.encodeServerRaw(key, gson.toJson(srv))
MmkvManager.encodeServerRaw(key, JsonUtil.toJsonPretty(srv))
count += 1
}
return count
@@ -318,7 +305,7 @@ object AngConfigManager {
// For compatibility
val config = ServerConfig.create(EConfigType.CUSTOM)
config.subscriptionId = subid
config.fullConfig = Gson().fromJson(server, V2rayConfig::class.java)
config.fullConfig = JsonUtil.fromJson(server, V2rayConfig::class.java)
config.remarks = config.fullConfig?.remarks ?: System.currentTimeMillis().toString()
val key = MmkvManager.encodeServerConfig("", config)
MmkvManager.encodeServerRaw(key, server)
@@ -373,19 +360,17 @@ object AngConfigManager {
return 0
}
Log.d(AppConfig.ANG_PACKAGE, url)
var configText = try {
Utils.getUrlContentWithCustomUserAgent(url)
val httpPort = SettingsManager.getHttpPort()
Utils.getUrlContentWithCustomUserAgent(url, 30000, httpPort)
} catch (e: Exception) {
e.printStackTrace()
""
}
if (configText.isEmpty()) {
configText = try {
val httpPort = Utils.parseInt(
settingsStorage?.decodeString(AppConfig.PREF_HTTP_PORT),
AppConfig.PORT_HTTP.toInt()
)
Utils.getUrlContentWithCustomUserAgent(url, 30000, httpPort)
Utils.getUrlContentWithCustomUserAgent(url)
} catch (e: Exception) {
e.printStackTrace()
""

View File

@@ -0,0 +1,37 @@
package com.v2ray.ang.util
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.google.gson.JsonPrimitive
import com.google.gson.JsonSerializationContext
import com.google.gson.JsonSerializer
import com.google.gson.reflect.TypeToken
import java.lang.reflect.Type
object JsonUtil {
private var gson = Gson()
fun toJson(src: Any?): String {
return gson.toJson(src)
}
fun <T> fromJson(json: String, cls: Class<T>): T {
return gson.fromJson(json, cls)
}
fun toJsonPretty(src: Any?): String {
val gsonPre = GsonBuilder()
.setPrettyPrinting()
.disableHtmlEscaping()
.registerTypeAdapter( // custom serialiser is needed here since JSON by default parse number as Double, core will fail to start
object : TypeToken<Double>() {}.type,
JsonSerializer { src: Double?, _: Type?, _: JsonSerializationContext? ->
JsonPrimitive(
src?.toInt()
)
}
)
.create()
return gsonPre.toJson(src)
}
}

View File

@@ -1,6 +1,6 @@
package com.v2ray.ang.util
import com.google.gson.Gson
import com.tencent.mmkv.MMKV
import com.v2ray.ang.AppConfig.PREF_IS_BOOTED
import com.v2ray.ang.AppConfig.PREF_ROUTING_RULESET
@@ -49,7 +49,7 @@ object MmkvManager {
}
fun encodeServerList(serverList: MutableList<String>) {
mainStorage.encode(KEY_ANG_CONFIGS, Gson().toJson(serverList))
mainStorage.encode(KEY_ANG_CONFIGS, JsonUtil.toJson(serverList))
}
fun decodeServerList(): MutableList<String> {
@@ -57,7 +57,7 @@ object MmkvManager {
return if (json.isNullOrBlank()) {
mutableListOf()
} else {
Gson().fromJson(json, Array<String>::class.java).toMutableList()
JsonUtil.fromJson(json, Array<String>::class.java).toMutableList()
}
}
@@ -69,7 +69,7 @@ object MmkvManager {
if (json.isNullOrBlank()) {
return null
}
return Gson().fromJson(json, ServerConfig::class.java)
return JsonUtil.fromJson(json, ServerConfig::class.java)
}
fun decodeProfileConfig(guid: String): ProfileItem? {
@@ -80,12 +80,12 @@ object MmkvManager {
if (json.isNullOrBlank()) {
return null
}
return Gson().fromJson(json, ProfileItem::class.java)
return JsonUtil.fromJson(json, ProfileItem::class.java)
}
fun encodeServerConfig(guid: String, config: ServerConfig): String {
val key = guid.ifBlank { Utils.getUuid() }
serverStorage.encode(key, Gson().toJson(config))
serverStorage.encode(key, JsonUtil.toJson(config))
val serverList = decodeServerList()
if (!serverList.contains(key)) {
serverList.add(0, key)
@@ -101,7 +101,7 @@ object MmkvManager {
server = config.getProxyOutbound()?.getServerAddress(),
serverPort = config.getProxyOutbound()?.getServerPort(),
)
profileStorage.encode(key, Gson().toJson(profile))
profileStorage.encode(key, JsonUtil.toJson(profile))
return key
}
@@ -141,7 +141,7 @@ object MmkvManager {
if (json.isNullOrBlank()) {
return null
}
return Gson().fromJson(json, ServerAffiliationInfo::class.java)
return JsonUtil.fromJson(json, ServerAffiliationInfo::class.java)
}
fun encodeServerTestDelayMillis(guid: String, testResult: Long) {
@@ -150,14 +150,14 @@ object MmkvManager {
}
val aff = decodeServerAffiliationInfo(guid) ?: ServerAffiliationInfo()
aff.testDelayMillis = testResult
serverAffStorage.encode(guid, Gson().toJson(aff))
serverAffStorage.encode(guid, JsonUtil.toJson(aff))
}
fun clearAllTestDelayResults(keys: List<String>?) {
keys?.forEach { key ->
decodeServerAffiliationInfo(key)?.let { aff ->
aff.testDelayMillis = 0
serverAffStorage.encode(key, Gson().toJson(aff))
serverAffStorage.encode(key, JsonUtil.toJson(aff))
}
}
}
@@ -217,7 +217,7 @@ object MmkvManager {
decodeSubsList().forEach { key ->
val json = subStorage.decodeString(key)
if (!json.isNullOrBlank()) {
subscriptions.add(Pair(key, Gson().fromJson(json, SubscriptionItem::class.java)))
subscriptions.add(Pair(key, JsonUtil.fromJson(json, SubscriptionItem::class.java)))
}
}
return subscriptions
@@ -234,7 +234,7 @@ object MmkvManager {
fun encodeSubscription(guid: String, subItem: SubscriptionItem) {
val key = guid.ifBlank { Utils.getUuid() }
subStorage.encode(key, Gson().toJson(subItem))
subStorage.encode(key, JsonUtil.toJson(subItem))
val subsList = decodeSubsList()
if (!subsList.contains(key)) {
@@ -245,11 +245,11 @@ object MmkvManager {
fun decodeSubscription(subscriptionId: String): SubscriptionItem? {
val json = subStorage.decodeString(subscriptionId) ?: return null
return Gson().fromJson(json, SubscriptionItem::class.java)
return JsonUtil.fromJson(json, SubscriptionItem::class.java)
}
fun encodeSubsList(subsList: MutableList<String>) {
mainStorage.encode(KEY_SUB_IDS, Gson().toJson(subsList))
mainStorage.encode(KEY_SUB_IDS, JsonUtil.toJson(subsList))
}
fun decodeSubsList(): MutableList<String> {
@@ -257,7 +257,7 @@ object MmkvManager {
return if (json.isNullOrBlank()) {
mutableListOf()
} else {
Gson().fromJson(json, Array<String>::class.java).toMutableList()
JsonUtil.fromJson(json, Array<String>::class.java).toMutableList()
}
}
@@ -270,7 +270,7 @@ object MmkvManager {
assetStorage.allKeys()?.forEach { key ->
val json = assetStorage.decodeString(key)
if (!json.isNullOrBlank()) {
assetUrlItems.add(Pair(key, Gson().fromJson(json, AssetUrlItem::class.java)))
assetUrlItems.add(Pair(key, JsonUtil.fromJson(json, AssetUrlItem::class.java)))
}
}
return assetUrlItems.sortedBy { (_, value) -> value.addedTime }
@@ -282,12 +282,12 @@ object MmkvManager {
fun encodeAsset(assetid: String, assetItem: AssetUrlItem) {
val key = assetid.ifBlank { Utils.getUuid() }
assetStorage.encode(key, Gson().toJson(assetItem))
assetStorage.encode(key, JsonUtil.toJson(assetItem))
}
fun decodeAsset(assetid: String): AssetUrlItem? {
val json = assetStorage.decodeString(assetid) ?: return null
return Gson().fromJson(json, AssetUrlItem::class.java)
return JsonUtil.fromJson(json, AssetUrlItem::class.java)
}
//endregion
@@ -297,14 +297,14 @@ object MmkvManager {
fun decodeRoutingRulesets(): MutableList<RulesetItem>? {
val ruleset = settingsStorage.decodeString(PREF_ROUTING_RULESET)
if (ruleset.isNullOrEmpty()) return null
return Gson().fromJson(ruleset, Array<RulesetItem>::class.java).toMutableList()
return JsonUtil.fromJson(ruleset, Array<RulesetItem>::class.java).toMutableList()
}
fun encodeRoutingRulesets(rulesetList: MutableList<RulesetItem>?) {
if (rulesetList.isNullOrEmpty())
settingsStorage.encode(PREF_ROUTING_RULESET, "")
else
settingsStorage.encode(PREF_ROUTING_RULESET, Gson().toJson(rulesetList))
settingsStorage.encode(PREF_ROUTING_RULESET, JsonUtil.toJson(rulesetList))
}
//endregion

View File

@@ -3,12 +3,10 @@ package com.v2ray.ang.util
import android.content.Context
import android.os.SystemClock
import android.util.Log
import com.google.gson.Gson
import com.v2ray.ang.AppConfig
import com.v2ray.ang.AppConfig.ANG_PACKAGE
import com.v2ray.ang.dto.EConfigType
import com.v2ray.ang.dto.ServerConfig
import com.v2ray.ang.util.MmkvManager.settingsStorage
import com.v2ray.ang.util.fmt.Hysteria2Fmt
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -25,22 +23,23 @@ object PluginUtil {
// return PluginManager.init(name)!!
// }
fun runPlugin(context: Context, config: ServerConfig?) {
fun runPlugin(context: Context, config: ServerConfig?, domainPort: String?) {
Log.d(packageName, "runPlugin")
val outbound = config?.getProxyOutbound() ?: return
if (outbound.protocol.equals(EConfigType.HYSTERIA2.name, true)) {
Log.d(packageName, "runPlugin $HYSTERIA2")
val socksPort = 100 + Utils.parseInt(settingsStorage?.decodeString(AppConfig.PREF_SOCKS_PORT), AppConfig.PORT_SOCKS.toInt())
val socksPort = domainPort?.split(":")?.last()
.let { if (it.isNullOrEmpty()) return else it.toInt() }
val hy2Config = Hysteria2Fmt.toNativeConfig(config, socksPort) ?: return
val configFile = File(context.noBackupFilesDir, "hy2_${SystemClock.elapsedRealtime()}.json")
Log.d(packageName, "runPlugin ${configFile.absolutePath}")
configFile.parentFile?.mkdirs()
configFile.writeText(Gson().toJson(hy2Config))
Log.d(packageName, Gson().toJson(hy2Config))
configFile.writeText(JsonUtil.toJson(hy2Config))
Log.d(packageName, JsonUtil.toJson(hy2Config))
runHy2(context, configFile)
}

View File

@@ -2,12 +2,15 @@ package com.v2ray.ang.util
import android.content.Context
import android.text.TextUtils
import com.google.gson.Gson
import com.v2ray.ang.AppConfig
import com.v2ray.ang.dto.RulesetItem
import com.v2ray.ang.dto.ServerConfig
import com.v2ray.ang.util.MmkvManager.decodeProfileConfig
import com.v2ray.ang.util.MmkvManager.decodeServerConfig
import com.v2ray.ang.util.MmkvManager.decodeServerList
import com.v2ray.ang.util.MmkvManager.settingsStorage
import com.v2ray.ang.util.Utils.parseInt
import java.util.Collections
object SettingsManager {
@@ -32,10 +35,34 @@ object SettingsManager {
return null
}
return Gson().fromJson(assets, Array<RulesetItem>::class.java).toMutableList()
return JsonUtil.fromJson(assets, Array<RulesetItem>::class.java).toMutableList()
}
fun resetRoutingRulesets(context: Context, index: Int) {
val rulesetList = getPresetRoutingRulesets(context, index) ?: return
resetRoutingRulesetsCommon(rulesetList)
}
fun resetRoutingRulesetsFromClipboard(content: String?): Boolean {
if (content.isNullOrEmpty()) {
return false
}
try {
val rulesetList = JsonUtil.fromJson(content, Array<RulesetItem>::class.java).toMutableList()
if (rulesetList.isNullOrEmpty()) {
return false
}
resetRoutingRulesetsCommon(rulesetList)
return true
} catch (e: Exception) {
e.printStackTrace()
return false
}
}
private fun resetRoutingRulesetsCommon(rulesetList: MutableList<RulesetItem>) {
val rulesetNew: MutableList<RulesetItem> = mutableListOf()
MmkvManager.decodeRoutingRulesets()?.forEach { key ->
if (key.looked == true) {
@@ -43,7 +70,6 @@ object SettingsManager {
}
}
val rulesetList = getPresetRoutingRulesets(context, index) ?: return
rulesetNew.addAll(rulesetList)
MmkvManager.encodeRoutingRulesets(rulesetNew)
}
@@ -117,4 +143,12 @@ object SettingsManager {
return null
}
fun getSocksPort(): Int {
return parseInt(settingsStorage?.decodeString(AppConfig.PREF_SOCKS_PORT), AppConfig.PORT_SOCKS.toInt())
}
fun getHttpPort(): Int {
return parseInt(settingsStorage?.decodeString(AppConfig.PREF_HTTP_PORT), AppConfig.PORT_HTTP.toInt())
}
}

View File

@@ -98,9 +98,9 @@ object SpeedtestUtil {
}
}
fun testConnection(context: Context, port: Int): String {
// return V2RayVpnService.measureV2rayDelay()
fun testConnection(context: Context, port: Int): Pair<Long, String> {
var result: String
var elapsed = -1L
var conn: HttpURLConnection? = null
try {
@@ -120,7 +120,7 @@ object SpeedtestUtil {
val start = SystemClock.elapsedRealtime()
val code = conn.responseCode
val elapsed = SystemClock.elapsedRealtime() - start
elapsed = SystemClock.elapsedRealtime() - start
if (code == 204 || code == 200 && conn.responseLength == 0L) {
result = context.getString(R.string.connection_test_available, elapsed)
@@ -134,10 +134,7 @@ object SpeedtestUtil {
}
} catch (e: IOException) {
// network exception
Log.d(
AppConfig.ANG_PACKAGE,
"testConnection IOException: " + Log.getStackTraceString(e)
)
Log.d(AppConfig.ANG_PACKAGE, "testConnection IOException: " + Log.getStackTraceString(e))
result = context.getString(R.string.connection_test_error, e.message)
} catch (e: Exception) {
// library exception, eg sumsung
@@ -147,7 +144,7 @@ object SpeedtestUtil {
conn?.disconnect()
}
return result
return Pair(elapsed, result)
}
fun getLibVersion(): String {

View File

@@ -453,6 +453,17 @@ object Utils {
}
}
fun findFreePort(ports: List<Int>): Int {
for (port in ports) {
try {
return ServerSocket(port).use { it.localPort }
} catch (ex: IOException) {
continue // try next port
}
}
// if the program gets here, no port in the range was found
throw IOException("no free port found")
}
}

View File

@@ -3,7 +3,7 @@ package com.v2ray.ang.util
import android.content.Context
import android.text.TextUtils
import android.util.Log
import com.google.gson.Gson
import com.v2ray.ang.AppConfig
import com.v2ray.ang.AppConfig.ANG_PACKAGE
import com.v2ray.ang.AppConfig.LOOPBACK
@@ -14,6 +14,7 @@ import com.v2ray.ang.AppConfig.TAG_FRAGMENT
import com.v2ray.ang.AppConfig.TAG_PROXY
import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_ADDRESS_V4
import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_ADDRESS_V6
import com.v2ray.ang.dto.ConfigResult
import com.v2ray.ang.dto.EConfigType
import com.v2ray.ang.dto.RulesetItem
import com.v2ray.ang.dto.ServerConfig
@@ -25,34 +26,32 @@ import com.v2ray.ang.util.MmkvManager.settingsStorage
object V2rayConfigUtil {
data class Result(var status: Boolean, var content: String = "", var domainPort: String? = null)
fun getV2rayConfig(context: Context, guid: String): Result {
fun getV2rayConfig(context: Context, guid: String): ConfigResult {
try {
val config = MmkvManager.decodeServerConfig(guid) ?: return Result(false)
val config = MmkvManager.decodeServerConfig(guid) ?: return ConfigResult(false)
if (config.configType == EConfigType.CUSTOM) {
val raw = MmkvManager.decodeServerRaw(guid)
val customConfig = if (raw.isNullOrBlank()) {
config.fullConfig?.toPrettyPrinting() ?: return Result(false)
config.fullConfig?.toPrettyPrinting() ?: return ConfigResult(false)
} else {
raw
}
val domainPort = config.getProxyOutbound()?.getServerAddressAndPort()
return Result(true, customConfig, domainPort)
return ConfigResult(true, guid, customConfig, domainPort)
}
val result = getV2rayNonCustomConfig(context, config)
//Log.d(ANG_PACKAGE, result.content)
Log.d(ANG_PACKAGE, result.domainPort?:"")
result.guid = guid
return result
} catch (e: Exception) {
e.printStackTrace()
return Result(false)
return ConfigResult(false)
}
}
private fun getV2rayNonCustomConfig(context: Context, config: ServerConfig): Result {
val result = Result(false)
private fun getV2rayNonCustomConfig(context: Context, config: ServerConfig): ConfigResult {
val result = ConfigResult(false)
val outbound = config.getProxyOutbound() ?: return result
val address = outbound.getServerAddress() ?: return result
@@ -68,7 +67,7 @@ object V2rayConfigUtil {
if (TextUtils.isEmpty(assets)) {
return result
}
val v2rayConfig = Gson().fromJson(assets, V2rayConfig::class.java) ?: return result
val v2rayConfig = JsonUtil.fromJson(assets, V2rayConfig::class.java) ?: return result
v2rayConfig.log.loglevel = settingsStorage?.decodeString(AppConfig.PREF_LOGLEVEL) ?: "warning"
v2rayConfig.remarks = config.remarks
@@ -101,14 +100,8 @@ object V2rayConfigUtil {
private fun inbounds(v2rayConfig: V2rayConfig): Boolean {
try {
val socksPort = Utils.parseInt(
settingsStorage?.decodeString(AppConfig.PREF_SOCKS_PORT),
AppConfig.PORT_SOCKS.toInt()
)
val httpPort = Utils.parseInt(
settingsStorage?.decodeString(AppConfig.PREF_HTTP_PORT),
AppConfig.PORT_HTTP.toInt()
)
val socksPort = SettingsManager.getSocksPort()
val httpPort = SettingsManager.getHttpPort()
v2rayConfig.inbounds.forEach { curInbound ->
if (settingsStorage?.decodeBool(AppConfig.PREF_PROXY_SHARING) != true) {
@@ -149,7 +142,7 @@ object V2rayConfigUtil {
private fun outbounds(v2rayConfig: V2rayConfig, outbound: V2rayConfig.OutboundBean, isPlugin: Boolean): Pair<Boolean, String> {
if (isPlugin) {
val socksPort = 100 + Utils.parseInt(settingsStorage?.decodeString(AppConfig.PREF_SOCKS_PORT), AppConfig.PORT_SOCKS.toInt())
val socksPort = Utils.findFreePort(listOf(100 + SettingsManager.getSocksPort(), 0))
val outboundNew = V2rayConfig.OutboundBean(
mux = null,
protocol = EConfigType.SOCKS.name.lowercase(),
@@ -213,7 +206,7 @@ object V2rayConfigUtil {
return
}
val rule = Gson().fromJson(Gson().toJson(item), RulesBean::class.java) ?: return
val rule = JsonUtil.fromJson(JsonUtil.toJson(item), RulesBean::class.java) ?: return
v2rayConfig.routing.rules.add(rule)
@@ -447,7 +440,7 @@ object V2rayConfigUtil {
val requestString: String by lazy {
"""{"version":"1.1","method":"GET","headers":{"User-Agent":["Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.143 Safari/537.36","Mozilla/5.0 (iPhone; CPU iPhone OS 10_0_2 like Mac OS X) AppleWebKit/601.1 (KHTML, like Gecko) CriOS/53.0.2785.109 Mobile/14A456 Safari/601.1.46"],"Accept-Encoding":["gzip, deflate"],"Connection":["keep-alive"],"Pragma":"no-cache"}}"""
}
outbound.streamSettings?.tcpSettings?.header?.request = Gson().fromJson(
outbound.streamSettings?.tcpSettings?.header?.request = JsonUtil.fromJson(
requestString,
V2rayConfig.OutboundBean.StreamSettingsBean.TcpSettingsBean.HeaderBean.RequestBean::class.java
)

View File

@@ -26,7 +26,7 @@ object Hysteria2Fmt {
config.outboundBean?.streamSettings?.populateTlsSettings(
V2rayConfig.TLS,
if ((queryParam["allowInsecure"].orEmpty()) == "1") true else allowInsecure,
if ((queryParam["insecure"].orEmpty()) == "1") true else allowInsecure,
queryParam["sni"] ?: uri.idnHost,
null,
queryParam["alpn"],
@@ -40,6 +40,10 @@ object Hysteria2Fmt {
server.port = uri.port
server.password = uri.userInfo
}
if (!queryParam["obfs-password"].isNullOrEmpty()) {
config.outboundBean?.settings?.obfsPassword = queryParam["obfs-password"]
}
return config
}
@@ -59,6 +63,10 @@ object Hysteria2Fmt {
dicQuery["alpn"] = Utils.removeWhiteSpace(tlsSetting.alpn.joinToString(",")).orEmpty()
}
}
if (!outbound.settings?.obfsPassword.isNullOrEmpty()) {
dicQuery["obfs"] = "salamander"
dicQuery["obfs-password"] = outbound.settings?.obfsPassword ?: ""
}
val query = "?" + dicQuery.toList().joinToString(
separator = "&",
@@ -76,12 +84,24 @@ object Hysteria2Fmt {
fun toNativeConfig(config: ServerConfig, socksPort: Int): Hysteria2Bean? {
val outbound = config.getProxyOutbound() ?: return null
val tls = outbound.streamSettings?.tlsSettings
val obfs = if (outbound.settings?.obfsPassword.isNullOrEmpty()) null else
Hysteria2Bean.ObfsBean(
type = "salamander",
salamander = Hysteria2Bean.ObfsBean.SalamanderBean(
password = outbound.settings?.obfsPassword
)
)
val bean = Hysteria2Bean(
server = outbound.getServerAddressAndPort(),
auth = outbound.getPassword(),
obfs = obfs,
socks5 = Hysteria2Bean.Socks5Bean(
listen = "$LOOPBACK:${socksPort}",
),
http = Hysteria2Bean.Socks5Bean(
listen = "$LOOPBACK:${socksPort}",
),
tls = Hysteria2Bean.TlsBean(
sni = tls?.serverName ?: outbound.getServerAddress(),
insecure = tls?.allowInsecure

View File

@@ -2,13 +2,14 @@ package com.v2ray.ang.util.fmt
import android.text.TextUtils
import android.util.Log
import com.google.gson.Gson
import com.v2ray.ang.AppConfig
import com.v2ray.ang.dto.EConfigType
import com.v2ray.ang.dto.ServerConfig
import com.v2ray.ang.dto.V2rayConfig
import com.v2ray.ang.dto.VmessQRCode
import com.v2ray.ang.extension.idnHost
import com.v2ray.ang.util.JsonUtil
import com.v2ray.ang.util.MmkvManager.settingsStorage
import com.v2ray.ang.util.Utils
import java.net.URI
@@ -29,7 +30,7 @@ object VmessFmt {
Log.d(AppConfig.ANG_PACKAGE, "R.string.toast_decoding_failed")
return null
}
val vmessQRCode = Gson().fromJson(result, VmessQRCode::class.java)
val vmessQRCode = JsonUtil.fromJson(result, VmessQRCode::class.java)
// Although VmessQRCode fields are non null, looks like Gson may still create null fields
if (TextUtils.isEmpty(vmessQRCode.add)
|| TextUtils.isEmpty(vmessQRCode.port)
@@ -100,7 +101,7 @@ object VmessFmt {
vmessQRCode.host = transportDetails[1]
vmessQRCode.path = transportDetails[2]
}
val json = Gson().toJson(vmessQRCode)
val json = JsonUtil.toJson(vmessQRCode)
return Utils.encode(json)
}

View File

@@ -11,7 +11,6 @@ import android.util.Log
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import com.google.gson.Gson
import com.v2ray.ang.AngApplication
import com.v2ray.ang.AppConfig
import com.v2ray.ang.AppConfig.ANG_PACKAGE
@@ -21,14 +20,15 @@ import com.v2ray.ang.dto.ProfileItem
import com.v2ray.ang.dto.ServerConfig
import com.v2ray.ang.dto.ServersCache
import com.v2ray.ang.dto.V2rayConfig
import com.v2ray.ang.extension.serializable
import com.v2ray.ang.extension.toast
import com.v2ray.ang.util.AngConfigManager
import com.v2ray.ang.util.AngConfigManager.updateConfigViaSub
import com.v2ray.ang.util.JsonUtil
import com.v2ray.ang.util.MessageUtil
import com.v2ray.ang.util.MmkvManager
import com.v2ray.ang.util.SpeedtestUtil
import com.v2ray.ang.util.Utils
import com.v2ray.ang.util.V2rayConfigUtil
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
@@ -98,7 +98,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
try {
val config = ServerConfig.create(EConfigType.CUSTOM)
config.subscriptionId = subscriptionId
config.fullConfig = Gson().fromJson(server, V2rayConfig::class.java)
config.fullConfig = JsonUtil.fromJson(server, V2rayConfig::class.java)
config.remarks = config.fullConfig?.remarks ?: System.currentTimeMillis().toString()
val key = MmkvManager.encodeServerConfig("", config)
MmkvManager.encodeServerRaw(key, server)
@@ -211,14 +211,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
getApplication<AngApplication>().toast(R.string.connection_test_testing)
viewModelScope.launch(Dispatchers.Default) { // without Dispatchers.Default viewModelScope will launch in main thread
for (item in serversCopy) {
val config = V2rayConfigUtil.getV2rayConfig(getApplication(), item.guid)
if (config.status) {
MessageUtil.sendMsg2TestService(
getApplication(),
AppConfig.MSG_MEASURE_CONFIG,
Pair(item.guid, config.content)
)
}
MessageUtil.sendMsg2TestService(getApplication(), AppConfig.MSG_MEASURE_CONFIG, item.guid)
}
}
}
@@ -394,11 +387,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
}
AppConfig.MSG_MEASURE_CONFIG_SUCCESS -> {
val resultPair: Pair<String, Long> = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
intent.getSerializableExtra("content", Pair::class.java) as Pair<String, Long>
} else {
intent.getSerializableExtra("content") as Pair<String, Long>
}
val resultPair = intent.serializable<Pair<String, Long>>("content") ?: return
MmkvManager.encodeServerTestDelayMillis(resultPair.first, resultPair.second)
updateListAction.value = getPosition(resultPair.first)
}

View File

@@ -89,7 +89,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_marginBottom="24dp">
android:layout_marginBottom="12dp">
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab"

View File

@@ -13,6 +13,25 @@
<include layout="@layout/layout_address_port" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/layout_margin_top_height"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/server_obfs_password" />
<EditText
android:id="@+id/et_obfs_password"
android:layout_width="match_parent"
android:layout_height="@dimen/edit_height"
android:inputType="text" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
@@ -32,7 +51,6 @@
</LinearLayout>
<include layout="@layout/layout_tls_hysteria2" />
<LinearLayout

View File

@@ -14,5 +14,13 @@
android:id="@+id/import_rulesets"
android:title="@string/routing_settings_import_rulesets"
app:showAsAction="never" />
<item
android:id="@+id/import_rulesets_from_clipboard"
android:title="@string/routing_settings_import_rulesets_from_clipboard"
app:showAsAction="never" />
<item
android:id="@+id/export_rulesets_to_clipboard"
android:title="@string/routing_settings_export_rulesets_to_clipboard"
app:showAsAction="never" />
</menu>

View File

@@ -112,6 +112,7 @@
<string name="msg_file_not_found">الملف غير موجود</string>
<string name="msg_remark_is_duplicate">الملاحظات موجودة بالفعل</string>
<string name="toast_action_not_allowed">الإجراء غير مسموح به</string>
<string name="server_obfs_password">Obfs password</string>
<!-- PerAppProxyActivity -->
<string name="msg_dialog_progress">جار التحميل</string>
@@ -260,6 +261,8 @@
<string name="routing_settings_add_rule">Add rule</string>
<string name="routing_settings_import_rulesets">Import ruleset</string>
<string name="routing_settings_import_rulesets_tip">Existing rulesets will be deleted, are you sure to continue?</string>
<string name="routing_settings_import_rulesets_from_clipboard">Import ruleset from clipboard</string>
<string name="routing_settings_export_rulesets_to_clipboard">Export ruleset to clipboard</string>
<string name="routing_settings_locked">Locked, keep this rule when import presets</string>
<string name="connection_test_pending">التحقق من الاتصال</string>

View File

@@ -111,6 +111,8 @@
<string name="msg_file_not_found">ফাইল খুঁজে পাওয়া যায়নি</string>
<string name="msg_remark_is_duplicate">মন্তব্য ইতিমধ্যে বিদ্যমান</string>
<string name="toast_action_not_allowed">অ্যাকশন অনুমোদিত নয়</string>
<string name="server_obfs_password">Obfs password</string>
<!-- PerAppProxyActivity -->
<string name="msg_dialog_progress">লোড হচ্ছে</string>
<string name="menu_item_search">অনুসন্ধান করুন</string>
@@ -257,6 +259,8 @@
<string name="routing_settings_add_rule">Add rule</string>
<string name="routing_settings_import_rulesets">Import ruleset</string>
<string name="routing_settings_import_rulesets_tip">Existing rulesets will be deleted, are you sure to continue?</string>
<string name="routing_settings_import_rulesets_from_clipboard">Import ruleset from clipboard</string>
<string name="routing_settings_export_rulesets_to_clipboard">Export ruleset to clipboard</string>
<string name="routing_settings_locked">Locked, keep this rule when import presets</string>
<string name="connection_test_pending">সংযোগ পরীক্ষা করুন</string>

View File

@@ -105,6 +105,7 @@
<string name="title_url">URL</string>
<string name="menu_item_download_file">دانلود فایل‌ها</string>
<string name="toast_action_not_allowed">این عمل ممنوع است</string>
<string name="server_obfs_password">Obfs password</string>
<!-- PerAppProxyActivity -->
<string name="title_user_asset_add_url">URL را اضافه کنید</string>
@@ -256,6 +257,8 @@
<string name="routing_settings_add_rule">Add rule</string>
<string name="routing_settings_import_rulesets">Import ruleset</string>
<string name="routing_settings_import_rulesets_tip">Existing rulesets will be deleted, are you sure to continue?</string>
<string name="routing_settings_import_rulesets_from_clipboard">Import ruleset from clipboard</string>
<string name="routing_settings_export_rulesets_to_clipboard">Export ruleset to clipboard</string>
<string name="routing_settings_locked">Locked, keep this rule when import presets</string>
<string name="connection_test_pending">اتصال را بررسی کنید</string>

View File

@@ -110,7 +110,7 @@
<string name="msg_file_not_found">Файл не найден</string>
<string name="msg_remark_is_duplicate">Название уже существует</string>
<string name="toast_action_not_allowed">Это действие запрещено</string>
<string name="server_obfs_password">Obfs password</string>
<!-- PerAppProxyActivity -->
<string name="msg_dialog_progress">Загрузка…</string>
@@ -130,8 +130,8 @@
<string name="title_vpn_settings">Настройки VPN</string>
<string name="title_pref_per_app_proxy">Прокси для выбранных приложений</string>
<string name="summary_pref_per_app_proxy">Основной: выделенное приложение соединяется через прокси, не выделенное — напрямую;\nРежим обхода: выделенное приложение соединяется напрямую, не выделенное — через прокси.\nЕсть возможность автоматического выбора проксируемых приложений в меню.</string>
<string name="title_pref_is_booted">Auto connect at startup</string>
<string name="summary_pref_is_booted">Automatically connects to the selected server at startup, which may be unsuccessful</string>
<string name="title_pref_is_booted">Автоподключение при запуске</string>
<string name="summary_pref_is_booted">Автоматически подключаться к выбранному серверу при запуске приложения (может оказаться неудачным)</string>
<string name="title_mux_settings">Настройки мультиплексирования</string>
<string name="title_pref_mux_enabled">Использовать мультиплексирование</string>
@@ -235,10 +235,10 @@
<string name="sub_setting_url">URL (необязательно)</string>
<string name="sub_setting_filter">Название фильтра</string>
<string name="sub_setting_enable">Использовать обновление</string>
<string name="sub_auto_update">Использовать автоматическое обновление</string>
<string name="sub_auto_update">Использовать автообновление</string>
<string name="sub_setting_pre_profile">Название предыдущего прокси</string>
<string name="sub_setting_next_profile">Название следующего прокси</string>
<string name="sub_setting_pre_profile_tip">Убедитесь, что название существует и является уникальным</string>
<string name="sub_setting_pre_profile_tip">Название должно существовать и быть уникальным</string>
<string name="title_sub_update">Обновить подписку группы</string>
<string name="title_ping_all_server">Проверка профилей группы</string>
<string name="title_real_ping_all_server">Время отклика профилей группы</string>
@@ -260,7 +260,9 @@
<string name="routing_settings_add_rule">Добавить правило</string>
<string name="routing_settings_import_rulesets">Импорт правил</string>
<string name="routing_settings_import_rulesets_tip">Существующие правила будут удалены. Продолжить?</string>
<string name="routing_settings_locked">Locked, keep this rule when import presets</string>
<string name="routing_settings_import_rulesets_from_clipboard">Import ruleset from clipboard</string>
<string name="routing_settings_export_rulesets_to_clipboard">Export ruleset to clipboard</string>
<string name="routing_settings_locked">Постоянное (сохранится при импорте правил)</string>
<string name="routing_settings_domain">Домен</string>
<string name="routing_settings_ip">IP</string>
<string name="routing_settings_port">Порт</string>

View File

@@ -105,6 +105,7 @@
<string name="title_url">URL</string>
<string name="menu_item_download_file">Tải xuống tệp tin</string>
<string name="toast_action_not_allowed">Hành động này bị cấm!</string>
<string name="server_obfs_password">Obfs password</string>
<!-- PerAppProxyActivity -->
<string name="title_user_asset_add_url">Thêm URL nội dung</string>
@@ -259,6 +260,8 @@
<string name="routing_settings_add_rule">Add rule</string>
<string name="routing_settings_import_rulesets">Import ruleset</string>
<string name="routing_settings_import_rulesets_tip">Existing rulesets will be deleted, are you sure to continue?</string>
<string name="routing_settings_import_rulesets_from_clipboard">Import ruleset from clipboard</string>
<string name="routing_settings_export_rulesets_to_clipboard">Export ruleset to clipboard</string>
<string name="routing_settings_locked">Locked, keep this rule when import presets</string>
<string name="connection_test_pending">Kiểm tra kết nối</string>

View File

@@ -105,7 +105,7 @@
<string name="title_url">URL</string>
<string name="menu_item_download_file">下载文件</string>
<string name="toast_action_not_allowed">禁止此项操作</string>
<string name="server_obfs_password">混淆密码</string>
<!-- PerAppProxyActivity -->
<string name="title_user_asset_add_url">添加资产网址</string>
@@ -257,6 +257,8 @@
<string name="routing_settings_add_rule">添加规则</string>
<string name="routing_settings_import_rulesets">导入预设规则集</string>
<string name="routing_settings_import_rulesets_tip">将删除现有的规则集,是否确定继续?</string>
<string name="routing_settings_import_rulesets_from_clipboard">从剪贴板导入规则集</string>
<string name="routing_settings_export_rulesets_to_clipboard">导出规则集至剪贴板</string>
<string name="routing_settings_locked">锁定中,导入预设时不删除此规则</string>
<string name="connection_test_pending">"检查网络连接"</string>

View File

@@ -105,6 +105,7 @@
<string name="title_url">URL</string>
<string name="menu_item_download_file">下載檔案</string>
<string name="toast_action_not_allowed">禁止此項操作</string>
<string name="server_obfs_password">混淆密碼</string>
<!-- PerAppProxyActivity -->
<string name="title_user_asset_add_url">新增資產網址</string>
@@ -258,6 +259,8 @@
<string name="routing_settings_add_rule">新增規則</string>
<string name="routing_settings_import_rulesets">匯入預設規則集</string>
<string name="routing_settings_import_rulesets_tip">將刪除現有的規則集,是否確定繼續? </string>
<string name="routing_settings_import_rulesets_from_clipboard">從剪貼簿匯入規則集</string>
<string name="routing_settings_export_rulesets_to_clipboard">匯出規則集至剪貼簿</string>
<string name="routing_settings_locked">鎖定中,匯入預設時不刪除此規則</string>
<string name="connection_test_pending">"測試連線能力"</string>

View File

@@ -111,7 +111,7 @@
<string name="msg_file_not_found">File not found</string>
<string name="msg_remark_is_duplicate">The remarks already exists</string>
<string name="toast_action_not_allowed">Action not allowed</string>
<string name="server_obfs_password">Obfs password</string>
<!-- PerAppProxyActivity -->
<string name="msg_dialog_progress">Loading</string>
@@ -263,6 +263,8 @@
<string name="routing_settings_add_rule">Add rule</string>
<string name="routing_settings_import_rulesets">Import ruleset</string>
<string name="routing_settings_import_rulesets_tip">Existing rulesets will be deleted, are you sure to continue?</string>
<string name="routing_settings_import_rulesets_from_clipboard">Import ruleset from clipboard</string>
<string name="routing_settings_export_rulesets_to_clipboard">Export ruleset to clipboard</string>
<string name="routing_settings_locked">Locked, keep this rule when import presets</string>
<string name="routing_settings_domain" translatable="false">domain</string>
<string name="routing_settings_ip" translatable="false">ip</string>