Compare commits

...

40 Commits

Author SHA1 Message Date
2dust
413f4efd69 up 1.8.35 2024-07-29 19:12:58 +08:00
Tamim Hossain
de69605eff Introduced version catalog for streamlined dependency management (#3377)
This pull request integrates a version catalog (libs.versions.toml) to centralize dependency management within the project. By utilizing this approach, we enhance dependency consistency, reduce maintenance overhead, and facilitate version updates across multiple modules. This change provides a more organized and efficient way to manage project dependencies.
2024-07-28 09:27:58 +08:00
mayampi01
9338ba3525 Set UseIP in Direct to break the Android Private DNS dead loop (#3362)
* Set UseIP in Direct to break the Android Private DNS dead loop

* fakedns: No need to set UseIP again
2024-07-25 19:50:23 +08:00
2dust
322b6ec615 Bug fix 2024-07-25 19:46:39 +08:00
2dust
304232d029 up 1.8.34 2024-07-22 11:34:44 +08:00
2dust
f80c3bfe07 up 1.8.33 2024-07-20 15:05:45 +08:00
2dust
bb0a62fc8b up 1.8.32 2024-07-18 10:45:05 +08:00
2dust
5bfdca6cd9 Bug fix 2024-07-18 10:15:58 +08:00
2dust
447e712a9d up 1.8.31 2024-07-16 14:05:34 +08:00
2dust
8bb03f189d up 1.8.30 2024-07-13 10:43:46 +08:00
2dust
3b0554cd9b up 1.8.29 2024-07-12 13:30:38 +08:00
2dust
858101b0d9 Bug fix
https://github.com/2dust/v2rayNG/issues/3234
https://github.com/2dust/v2rayNG/issues/3291
2024-07-11 16:41:38 +08:00
2dust
a7cf8bee28 Code clean
Add allowInsecure to Share
2024-07-11 16:40:25 +08:00
2dust
af1ec7bea9 Bug fix 2024-07-09 12:29:04 +08:00
2dust
002bf7ef22 up 1.8.28 2024-07-07 18:14:36 +08:00
2dust
6919e2336d Bug fix
https://github.com/2dust/v2rayNG/issues/3278
2024-07-01 18:09:57 +08:00
2dust
5a5bd22073 Bug fix
https://github.com/2dust/v2rayNG/issues/3232
2024-07-01 10:34:45 +08:00
2dust
a726f00f35 Bug fix 2024-06-29 16:47:06 +08:00
2dust
79297f8a42 up 1.8.27
App bundle support
2024-06-29 15:32:01 +08:00
2dust
e3f70ac253 Add LEANBACK_LAUNCHER 2024-06-29 15:29:20 +08:00
GFW-knocker
838b346fcc fix wireguard issue (#3260)
fix crash when invalid wireguard config imported
2024-06-28 09:41:55 +08:00
solokot
eba9545ccf Update Russian translation to 1.8.26 (#3238) 2024-06-24 09:09:35 +08:00
2dust
890ade9495 up 1.8.26 2024-06-21 20:33:34 +08:00
2dust
518ef1e0ec Add splithttp transport for xray 2024-06-21 20:18:04 +08:00
2dust
bdcecfca72 Hardcode gstatic.com taking detour [proxy] 2024-06-21 16:48:30 +08:00
2dust
1363846ac4 Fix view 2024-06-08 20:24:23 +08:00
2dust
30347546a2 Optimized code 2024-06-08 10:17:54 +08:00
2dust
ac7eb28e91 Auto update subscriptions when adding a url 2024-06-07 21:06:27 +08:00
2dust
748405473b Fix
https://github.com/2dust/v2rayNG/issues/3175
2024-06-07 10:51:13 +08:00
2dust
1080390bed Adding a second test when a delayed test fails
https://www.google.com/generate_204
2024-06-07 10:25:23 +08:00
2dust
c48725c7dd Bug fix
https://github.com/2dust/v2rayNG/issues/3193
2024-06-06 19:48:50 +08:00
2dust
a5287dbadc Optimized code 2024-06-02 20:50:27 +08:00
solokot
ee5a3b0dd9 Update Russian translation (#3172) 2024-05-30 20:06:16 +08:00
2dust
3d001541e5 Refactor the parser 2024-05-30 20:04:43 +08:00
2dust
b376b229b9 Add progress bar when subscription is updated 2024-05-30 16:07:11 +08:00
2dust
33b6203978 Bug fix 2024-05-29 20:30:10 +08:00
2dust
2d803e009c Bug fix
https://github.com/2dust/v2rayNG/issues/3168
2024-05-29 17:41:58 +08:00
2dust
2ddbe38781 Merge branch 'master' of https://github.com/2dust/v2rayNG 2024-05-25 20:32:37 +08:00
user09283
96181a2b8d Update strings.xml (#3152)
Update Vietnamese language!
2024-05-24 20:42:30 +08:00
2dust
8308b8eaf2 Bug fix
android.app.RemoteServiceException$ForegroundServiceDidNotStartInTimeException: Context.startForegroundService() did not then call Service.startForeground(): ServiceRecord{8f445a6 u0 com.v2ray.ang/.service.V2RayVpnService}
2024-05-24 10:35:46 +08:00
53 changed files with 1865 additions and 1311 deletions

View File

@@ -11,8 +11,8 @@ android {
applicationId = "com.v2ray.ang"
minSdk = 21
targetSdk = 34
versionCode = 562
versionName = "1.8.25"
versionCode = 579
versionName = "1.8.35"
multiDexEnabled = true
splits.abi {
reset()
@@ -53,14 +53,14 @@ android {
splits {
abi {
isEnable = true
isUniversalApk = false
isUniversalApk = true
}
}
applicationVariants.all {
val variant = this
val versionCodes =
mapOf("armeabi-v7a" to 1, "arm64-v8a" to 2, "x86" to 3, "x86_64" to 4)
mapOf("armeabi-v7a" to 4, "arm64-v8a" to 4, "x86" to 4, "x86_64" to 4, "universal" to 4)
variant.outputs
.map { it as com.android.build.gradle.internal.api.ApkVariantOutputImpl }
@@ -68,7 +68,7 @@ android {
val abi = if (output.getFilter("ABI") != null)
output.getFilter("ABI")
else
"all"
"universal"
output.outputFileName = "v2rayNG_${variant.versionName}_${abi}.apk"
if(versionCodes.containsKey(abi))
@@ -86,47 +86,54 @@ android {
viewBinding = true
buildConfig = true
}
packagingOptions {
jniLibs {
useLegacyPackaging = true
}
}
}
dependencies {
implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.aar","*.jar"))))
testImplementation("junit:junit:4.13.2")
testImplementation(libs.junit)
implementation(libs.flexbox)
// Androidx
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
implementation("androidx.legacy:legacy-support-v4:1.0.0")
implementation("androidx.appcompat:appcompat:1.6.1")
implementation("com.google.android.material:material:1.12.0")
implementation("androidx.cardview:cardview:1.0.0")
implementation("androidx.preference:preference-ktx:1.2.1")
implementation("androidx.recyclerview:recyclerview:1.3.2")
implementation("androidx.fragment:fragment-ktx:1.7.1")
implementation("androidx.multidex:multidex:2.0.1")
implementation("androidx.viewpager2:viewpager2:1.1.0")
implementation(libs.constraintlayout)
implementation(libs.legacy.support.v4)
implementation(libs.appcompat)
implementation(libs.material)
implementation(libs.cardview)
implementation(libs.preference.ktx)
implementation(libs.recyclerview)
implementation(libs.fragment.ktx)
implementation(libs.multidex)
implementation(libs.viewpager2)
// Androidx ktx
implementation("androidx.activity:activity-ktx:1.9.0")
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.0")
implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.8.0")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.0")
implementation(libs.activity.ktx)
implementation(libs.lifecycle.viewmodel.ktx)
implementation(libs.lifecycle.livedata.ktx)
implementation(libs.lifecycle.runtime.ktx)
//kotlin
implementation("org.jetbrains.kotlin:kotlin-reflect:1.9.23")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.0")
implementation(libs.kotlin.reflect)
implementation(libs.kotlinx.coroutines.core)
implementation(libs.kotlinx.coroutines.android)
implementation("com.tencent:mmkv-static:1.3.4")
implementation("com.google.code.gson:gson:2.10.1")
implementation("io.reactivex:rxjava:1.3.8")
implementation("io.reactivex:rxandroid:1.2.1")
implementation("com.tbruyelle.rxpermissions:rxpermissions:0.9.4@aar")
implementation("me.drakeet.support:toastcompat:1.1.0")
implementation("com.blacksquircle.ui:editorkit:2.9.0")
implementation("com.blacksquircle.ui:language-base:2.9.0")
implementation("com.blacksquircle.ui:language-json:2.9.0")
implementation("io.github.g00fy2.quickie:quickie-bundled:1.9.0")
implementation("com.google.zxing:core:3.5.3")
implementation(libs.mmkv.static)
implementation(libs.gson)
implementation(libs.rxjava)
implementation(libs.rxandroid)
implementation(libs.rxpermissions)
implementation(libs.toastcompat)
implementation(libs.editorkit)
implementation(libs.language.base)
implementation(libs.language.json)
implementation(libs.quickie.bundled)
implementation(libs.core)
implementation("androidx.work:work-runtime-ktx:2.8.1")
implementation("androidx.work:work-multiprocess:2.8.1")
implementation(libs.work.runtime.ktx)
implementation(libs.work.multiprocess)
}

View File

@@ -54,7 +54,7 @@
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<!-- <category android:name="android.intent.category.LEANBACK_LAUNCHER"/>-->
<category android:name="android.intent.category.LEANBACK_LAUNCHER"/>
</intent-filter>
<intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES" />

View File

@@ -81,7 +81,9 @@
},
{
"protocol": "freedom",
"settings": {},
"settings": {
"domainStrategy": "UseIP"
},
"tag": "direct"
},
{

View File

@@ -100,6 +100,7 @@ object AppConfig {
const val GeoUrl = "https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/"
const val TgChannelUrl = "https://t.me/github_2dust"
const val DelayTestUrl = "https://www.gstatic.com/generate_204"
const val DelayTestUrl2 = "https://www.google.com/generate_204"
const val DNS_PROXY = "1.1.1.1"
const val DNS_DIRECT = "223.5.5.5"

View File

@@ -1,32 +0,0 @@
package com.v2ray.ang.dto
data class AngConfig(
var index: Int,
var vmess: ArrayList<VmessBean>,
var subItem: ArrayList<SubItemBean>
) {
data class VmessBean(var guid: String = "123456",
var address: String = "v2ray.cool",
var port: Int = 10086,
var id: String = "a3482e88-686a-4a58-8126-99c9df64b7bf",
var alterId: Int = 64,
var security: String = "aes-128-cfb",
var network: String = "tcp",
var remarks: String = "def",
var headerType: String = "",
var requestHost: String = "",
var path: String = "",
var streamSecurity: String = "",
var allowInsecure: String = "",
var configType: Int = 1,
var configVersion: Int = 1,
var testResult: String = "",
var subid: String = "",
var flow: String = "",
var sni: String = "")
data class SubItemBean(var id: String = "",
var remarks: String = "",
var url: String = "",
var enabled: Boolean = true)
}

View File

@@ -139,6 +139,7 @@ data class V2rayConfig(
var kcpSettings: KcpSettingsBean? = null,
var wsSettings: WsSettingsBean? = null,
var httpupgradeSettings: HttpupgradeSettingsBean? = null,
var splithttpSettings: SplithttpSettingsBean? = null,
var httpSettings: HttpSettingsBean? = null,
var tlsSettings: TlsSettingsBean? = null,
var quicSettings: QuicSettingBean? = null,
@@ -157,7 +158,7 @@ data class V2rayConfig(
var headers: HeadersBean = HeadersBean(),
val version: String? = null,
val method: String? = null) {
data class HeadersBean(var Host: List<String> = ArrayList(),
data class HeadersBean(var Host: List<String>? = ArrayList(),
@SerializedName("User-Agent")
val userAgent: List<String>? = null,
@SerializedName("Accept-Encoding")
@@ -192,6 +193,10 @@ data class V2rayConfig(
var host: String = "",
val acceptProxyProtocol: Boolean? = null)
data class SplithttpSettingsBean(var path: String = "",
var host: String = "",
val maxUploadSize: Int? = null,
val maxConcurrentUploads: Int? = null)
data class HttpSettingsBean(var host: List<String> = ArrayList(),
var path: String = "")
@@ -247,7 +252,7 @@ data class V2rayConfig(
requestObj.headers.Host = (host ?: "").split(",").map { it.trim() }.filter { it.isNotEmpty() }
requestObj.path = (path ?: "").split(",").map { it.trim() }.filter { it.isNotEmpty() }
tcpSetting.header.request = requestObj
sni = requestObj.headers.Host.getOrNull(0) ?: sni
sni = requestObj.headers.Host?.getOrNull(0) ?: sni
}
} else {
tcpSetting.header.type = "none"
@@ -279,6 +284,13 @@ data class V2rayConfig(
httpupgradeSetting.path = path ?: "/"
httpupgradeSettings = httpupgradeSetting
}
"splithttp" -> {
val splithttpSetting = SplithttpSettingsBean()
splithttpSetting.host = host ?: ""
sni = splithttpSetting.host
splithttpSetting.path = path ?: "/"
splithttpSettings = splithttpSetting
}
"h2", "http" -> {
network = "h2"
val h2Setting = HttpSettingsBean()
@@ -418,6 +430,12 @@ data class V2rayConfig(
httpupgradeSetting.host,
httpupgradeSetting.path)
}
"splithttp" -> {
val splithttpSetting = streamSettings?.splithttpSettings ?: return null
listOf("",
splithttpSetting.host,
splithttpSetting.path)
}
"h2" -> {
val h2Setting = streamSettings?.httpSettings ?: return null
listOf("",
@@ -495,7 +513,7 @@ data class V2rayConfig(
var poolSize: Int = 10000) // roughly 10 times smaller than total ip pool
fun getProxyOutbound(): OutboundBean? {
outbounds.forEach { outbound ->
outbounds?.forEach { outbound ->
EConfigType.entries.forEach {
if (outbound.protocol.equals(it.name, true)) {
return outbound

View File

@@ -58,23 +58,11 @@ object SubscriptionUpdater {
"subscription automatic update: ---${subscription.remarks}"
)
val configs = Utils.getUrlContentWithCustomUserAgent(subscription.url)
importBatchConfig(configs, i.first)
AngConfigManager.importBatchConfig(configs, i.first, false)
notification.setContentText("Updating ${subscription.remarks}")
}
notificationManager.cancel(3)
return Result.success()
}
}
fun importBatchConfig(server: String?, subid: String = "") {
val append = subid.isEmpty()
val count = AngConfigManager.importBatchConfig(server, subid, append)
if (count <= 0) {
AngConfigManager.importBatchConfig(Utils.decode(server!!), subid, append)
}
if (count <= 0) {
AngConfigManager.appendCustomConfigServer(server, subid)
}
}
}

View File

@@ -63,12 +63,17 @@ object V2RayServiceManager {
private var mNotificationManager: NotificationManager? = null
fun startV2Ray(context: Context) {
if (v2rayPoint.isRunning) return
val guid = mainStorage?.decodeString(MmkvManager.KEY_SELECTED_SERVER) ?: return
val result = V2rayConfigUtil.getV2rayConfig(context, guid)
if (!result.status) return
if (settingsStorage?.decodeBool(AppConfig.PREF_PROXY_SHARING) == true) {
context.toast(R.string.toast_warning_pref_proxysharing_short)
} else {
context.toast(R.string.toast_services_start)
}
val intent = if (settingsStorage?.decodeString(AppConfig.PREF_MODE) ?: "VPN" == "VPN") {
val intent = if ((settingsStorage?.decodeString(AppConfig.PREF_MODE) ?: "VPN") == "VPN") {
Intent(context.applicationContext, V2RayVpnService::class.java)
} else {
Intent(context.applicationContext, V2RayProxyOnlyService::class.java)
@@ -126,7 +131,9 @@ object V2RayServiceManager {
val service = serviceControl?.get()?.getService() ?: return
val guid = mainStorage?.decodeString(MmkvManager.KEY_SELECTED_SERVER) ?: return
val config = MmkvManager.decodeServerConfig(guid) ?: return
if (!v2rayPoint.isRunning) {
if (v2rayPoint.isRunning) {
return
}
val result = V2rayConfigUtil.getV2rayConfig(service, guid)
if (!result.status)
return
@@ -163,7 +170,6 @@ object V2RayServiceManager {
cancelNotification()
}
}
}
fun stopV2rayPoint() {
val service = serviceControl?.get()?.getService() ?: return
@@ -242,6 +248,14 @@ object V2RayServiceManager {
Log.d(ANG_PACKAGE, "measureV2rayDelay: $e")
errstr = e.message?.substringAfter("\":") ?: "empty message"
}
if (time == -1L) {
try {
time = v2rayPoint.measureDelay(Utils.getDelayTestUrl(true))
} catch (e: Exception) {
Log.d(ANG_PACKAGE, "measureV2rayDelay: $e")
errstr = e.message?.substringAfter("\":") ?: "empty message"
}
}
}
val result = if (time == -1L) {
service.getString(R.string.connection_test_error, errstr)

View File

@@ -9,7 +9,6 @@ import android.net.VpnService
import android.os.Build
import android.os.Bundle
import android.text.TextUtils
import android.util.Log
import android.view.KeyEvent
import android.view.Menu
import android.view.MenuItem
@@ -28,9 +27,9 @@ import com.google.android.material.navigation.NavigationView
import com.tbruyelle.rxpermissions.RxPermissions
import com.tencent.mmkv.MMKV
import com.v2ray.ang.AppConfig
import com.v2ray.ang.AppConfig.ANG_PACKAGE
import com.v2ray.ang.R
import com.v2ray.ang.databinding.ActivityMainBinding
import com.v2ray.ang.databinding.LayoutProgressBinding
import com.v2ray.ang.dto.EConfigType
import com.v2ray.ang.extension.toast
import com.v2ray.ang.helper.SimpleItemTouchHelperCallback
@@ -40,12 +39,11 @@ import com.v2ray.ang.util.MmkvManager
import com.v2ray.ang.util.Utils
import com.v2ray.ang.viewmodel.MainViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import me.drakeet.support.toast.ToastCompat
import rx.Observable
import rx.android.schedulers.AndroidSchedulers
import java.io.File
import java.io.FileOutputStream
import java.util.concurrent.TimeUnit
class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedListener {
@@ -110,8 +108,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
setupViewModel()
copyAssets()
//migrateLegacy()
mainViewModel.copyAssets(assets)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
RxPermissions(this)
@@ -160,45 +157,6 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
mainViewModel.startListenBroadcast()
}
private fun copyAssets() {
val extFolder = Utils.userAssetPath(this)
lifecycleScope.launch(Dispatchers.IO) {
try {
val geo = arrayOf("geosite.dat", "geoip.dat")
assets.list("")
?.filter { geo.contains(it) }
?.filter { !File(extFolder, it).exists() }
?.forEach {
val target = File(extFolder, it)
assets.open(it).use { input ->
FileOutputStream(target).use { output ->
input.copyTo(output)
}
}
Log.i(ANG_PACKAGE, "Copied from apk assets folder to ${target.absolutePath}")
}
} catch (e: Exception) {
Log.e(ANG_PACKAGE, "asset copy failed", e)
}
}
}
// private fun migrateLegacy() {
// lifecycleScope.launch(Dispatchers.IO) {
// val result = AngConfigManager.migrateLegacyConfig(this@MainActivity)
// if (result != null) {
// launch(Dispatchers.Main) {
// if (result) {
// toast(getString(R.string.migration_success))
// mainViewModel.reloadServerList()
// } else {
// toast(getString(R.string.migration_fail))
// }
// }
// }
// }
// }
fun startV2Ray() {
if (mainStorage?.decodeString(MmkvManager.KEY_SELECTED_SERVER).isNullOrEmpty()) {
return
@@ -281,11 +239,6 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
true
}
// R.id.sub_setting -> {
// startActivity<SubSettingActivity>()
// true
// }
R.id.sub_update -> {
importConfigViaSub()
true
@@ -331,6 +284,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
AlertDialog.Builder(this).setMessage(R.string.del_config_comfirm)
.setPositiveButton(android.R.string.ok) { _, _ ->
mainViewModel.removeDuplicateServer()
mainViewModel.reloadServerList()
}
.setNegativeButton(android.R.string.no) {_, _ ->
//do noting
@@ -375,7 +329,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
/**
* import config from qrcode
*/
fun importQRcode(forConfig: Boolean): Boolean {
private fun importQRcode(forConfig: Boolean): Boolean {
// try {
// startActivityForResult(Intent("com.google.zxing.client.android.SCAN")
// .addCategory(Intent.CATEGORY_DEFAULT)
@@ -411,7 +365,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
/**
* import config from clipboard
*/
fun importClipboard()
private fun importClipboard()
: Boolean {
try {
val clipboard = Utils.getClipboard(this)
@@ -423,30 +377,28 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
return true
}
fun importBatchConfig(server: String?, subid: String = "") {
val subid2 = if(subid.isNullOrEmpty()){
mainViewModel.subscriptionId
}else{
subid
}
val append = subid.isNullOrEmpty()
private fun importBatchConfig(server: String?) {
val dialog = AlertDialog.Builder(this)
.setView(LayoutProgressBinding.inflate(layoutInflater).root)
.setCancelable(false)
.show()
var count = AngConfigManager.importBatchConfig(server, subid2, append)
if (count <= 0) {
count = AngConfigManager.importBatchConfig(Utils.decode(server!!), subid2, append)
}
if (count <= 0) {
count = AngConfigManager.appendCustomConfigServer(server, subid2)
}
lifecycleScope.launch(Dispatchers.IO) {
val count = AngConfigManager.importBatchConfig(server, mainViewModel.subscriptionId, true)
delay(500L)
launch(Dispatchers.Main) {
if (count > 0) {
toast(R.string.toast_success)
mainViewModel.reloadServerList()
} else {
toast(R.string.toast_failure)
}
dialog.dismiss()
}
}
}
fun importConfigCustomClipboard()
private fun importConfigCustomClipboard()
: Boolean {
try {
val configText = Utils.getClipboard(this)
@@ -465,7 +417,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
/**
* import config from local config file
*/
fun importConfigCustomLocal(): Boolean {
private fun importConfigCustomLocal(): Boolean {
try {
showFileChooser()
} catch (e: Exception) {
@@ -475,7 +427,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
return true
}
fun importConfigCustomUrlClipboard()
private fun importConfigCustomUrlClipboard()
: Boolean {
try {
val url = Utils.getClipboard(this)
@@ -493,7 +445,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
/**
* import config from url
*/
fun importConfigCustomUrl(url: String?): Boolean {
private fun importConfigCustomUrl(url: String?): Boolean {
try {
if (!Utils.isValidUrl(url)) {
toast(R.string.toast_invalid_url)
@@ -520,55 +472,24 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
/**
* import config from sub
*/
fun importConfigViaSub()
: Boolean {
try {
toast(R.string.title_sub_update)
MmkvManager.decodeSubscriptions().forEach {
if (TextUtils.isEmpty(it.first)
|| TextUtils.isEmpty(it.second.remarks)
|| TextUtils.isEmpty(it.second.url)
) {
return@forEach
}
if (!it.second.enabled) {
return@forEach
}
val url = Utils.idnToASCII(it.second.url)
if (!Utils.isValidUrl(url)) {
return@forEach
}
Log.d(ANG_PACKAGE, url)
private fun importConfigViaSub() : Boolean {
val dialog = AlertDialog.Builder(this)
.setView(LayoutProgressBinding.inflate(layoutInflater).root)
.setCancelable(false)
.show()
lifecycleScope.launch(Dispatchers.IO) {
var configText = try {
Utils.getUrlContentWithCustomUserAgent(url)
} 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, httpPort)
} catch (e: Exception) {
e.printStackTrace()
""
}
}
if(configText.isEmpty()) {
val count = AngConfigManager.updateConfigViaSubAll()
delay(500L)
launch(Dispatchers.Main) {
toast("\"" + it.second.remarks + "\" " + getString(R.string.toast_failure))
if (count > 0) {
toast(R.string.toast_success)
mainViewModel.reloadServerList()
} else {
toast(R.string.toast_failure)
}
return@launch
dialog.dismiss()
}
launch(Dispatchers.Main) {
importBatchConfig(configText, it.first)
}
}
}
} catch (e: Exception) {
e.printStackTrace()
return false
}
return true
}
@@ -623,15 +544,18 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
/**
* import customize config
*/
fun importCustomizeConfig(server: String?) {
private fun importCustomizeConfig(server: String?) {
try {
if (server == null || TextUtils.isEmpty(server)) {
toast(R.string.toast_none_data)
return
}
mainViewModel.appendCustomConfigServer(server)
if (mainViewModel.appendCustomConfigServer(server)) {
mainViewModel.reloadServerList()
toast(R.string.toast_success)
} else {
toast(R.string.toast_failure)
}
//adapter.notifyItemInserted(mainViewModel.serverList.lastIndex)
} catch (e: Exception) {
ToastCompat.makeText(this, "${getString(R.string.toast_malformed_josn)} ${e.cause?.message}", Toast.LENGTH_LONG).show()
@@ -640,7 +564,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
}
}
fun setTestState(content: String?) {
private fun setTestState(content: String?) {
binding.tvTestState.text = content
}

View File

@@ -97,7 +97,13 @@ class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<Mai
holder.itemMainBinding.tvType.text = config.configType.name.lowercase()
}
}
val strState = "${outbound?.getServerAddress()?.dropLast(3)}*** : ${outbound?.getServerPort()}"
val strState = try{
"${outbound?.getServerAddress()?.dropLast(3)}*** : ${outbound?.getServerPort()}"
}catch(e: Exception){
""
}
holder.itemMainBinding.tvStatistics.text = strState
holder.itemMainBinding.layoutShare.setOnClickListener {
@@ -162,7 +168,7 @@ class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<Mai
if (guid != selected) {
mainStorage?.encode(MmkvManager.KEY_SELECTED_SERVER, guid)
if (!TextUtils.isEmpty(selected)) {
notifyItemChanged(mActivity.mainViewModel.getPosition(selected!!))
notifyItemChanged(mActivity.mainViewModel.getPosition(selected?:""))
}
notifyItemChanged(mActivity.mainViewModel.getPosition(guid))
if (isRunning) {

View File

@@ -193,7 +193,7 @@ class PerAppProxyActivity : BaseActivity() {
}
override fun onQueryTextChange(newText: String?): Boolean {
filterProxyApp(newText!!)
filterProxyApp(newText?:"")
return false
}
})
@@ -209,12 +209,12 @@ class PerAppProxyActivity : BaseActivity() {
if (it.blacklist.containsAll(pkgNames)) {
it.apps.forEach {
val packageName = it.packageName
adapter?.blacklist!!.remove(packageName)
adapter?.blacklist?.remove(packageName)
}
} else {
it.apps.forEach {
val packageName = it.packageName
adapter?.blacklist!!.add(packageName)
adapter?.blacklist?.add(packageName)
}
}
it.notifyDataSetChanged()
@@ -278,7 +278,7 @@ class PerAppProxyActivity : BaseActivity() {
return false
}
adapter?.blacklist!!.clear()
adapter?.blacklist?.clear()
if (binding.switchBypassApps.isChecked) {
adapter?.let {
@@ -286,7 +286,7 @@ class PerAppProxyActivity : BaseActivity() {
val packageName = it.packageName
Log.d(ANG_PACKAGE, packageName)
if (!inProxyApps(proxyApps, packageName, force)) {
adapter?.blacklist!!.add(packageName)
adapter?.blacklist?.add(packageName)
println(packageName)
return@block
}
@@ -299,7 +299,7 @@ class PerAppProxyActivity : BaseActivity() {
val packageName = it.packageName
Log.d(ANG_PACKAGE, packageName)
if (inProxyApps(proxyApps, packageName, force)) {
adapter?.blacklist!!.add(packageName)
adapter?.blacklist?.add(packageName)
println(packageName)
return@block
}

View File

@@ -49,7 +49,7 @@ class RoutingSettingsFragment : Fragment() {
super.onViewCreated(view, savedInstanceState)
val content = settingsStorage?.getString(requireArguments().getString(routing_arg), "")
binding.etRoutingContent.text = Utils.getEditable(content!!)
binding.etRoutingContent.text = Utils.getEditable(content)
setHasOptionsMenu(true)
}
@@ -113,7 +113,7 @@ class RoutingSettingsFragment : Fragment() {
private val scanQRCodeForReplace = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode == RESULT_OK) {
val content = it.data?.getStringExtra("SCAN_RESULT")
binding.etRoutingContent.text = Utils.getEditable(content!!)
binding.etRoutingContent.text = Utils.getEditable(content)
}
}
@@ -145,7 +145,7 @@ class RoutingSettingsFragment : Fragment() {
val content = Utils.getUrlContext(url, 5000)
launch(Dispatchers.Main) {
val routingList = if (TextUtils.isEmpty(content)) {
Utils.readTextFromAssets(activity?.v2RayApplication!!, "custom_routing_$tag")
Utils.readTextFromAssets(activity?.v2RayApplication, "custom_routing_$tag")
} else {
content
}

View File

@@ -45,7 +45,7 @@ class ScannerActivity : BaseActivity(){
private fun handleResult(result: QRResult) {
if (result is QRResult.QRSuccess ) {
finished(result.content.rawValue!!)
finished(result.content.rawValue?:"")
} else {
finish()
}
@@ -110,7 +110,7 @@ class ScannerActivity : BaseActivity(){
try {
val bitmap = BitmapFactory.decodeStream(contentResolver.openInputStream(uri))
val text = QRCodeDecoder.syncDecodeQRCode(bitmap)
finished(text!!)
finished(text?:"")
} catch (e: Exception) {
e.printStackTrace()
toast(e.message.toString())

View File

@@ -5,10 +5,14 @@ import android.text.TextUtils
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.widget.*
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.EditText
import android.widget.LinearLayout
import android.widget.Spinner
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import com.tencent.mmkv.MMKV
import com.v2ray.ang.AngApplication
import com.v2ray.ang.AppConfig
import com.v2ray.ang.AppConfig.PREF_ALLOW_INSECURE
import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_ADDRESS_V4
@@ -106,7 +110,9 @@ class ServerActivity : BaseActivity() {
private val sp_network: Spinner? by lazy { findViewById(R.id.sp_network) }
private val sp_header_type: Spinner? by lazy { findViewById(R.id.sp_header_type) }
private val sp_header_type_title: TextView? by lazy { findViewById(R.id.sp_header_type_title) }
private val tv_request_host: TextView? by lazy { findViewById(R.id.tv_request_host) }
private val et_request_host: EditText? by lazy { findViewById(R.id.et_request_host) }
private val tv_path: TextView? by lazy { findViewById(R.id.tv_path) }
private val et_path: EditText? by lazy { findViewById(R.id.et_path) }
private val sp_stream_alpn: Spinner? by lazy { findViewById(R.id.sp_stream_alpn) } //uTLS
private val container_alpn: LinearLayout? by lazy { findViewById(R.id.l4) }
@@ -158,6 +164,36 @@ class ServerActivity : BaseActivity() {
et_request_host?.text = Utils.getEditable(transportDetails[1])
et_path?.text = Utils.getEditable(transportDetails[2])
}
tv_request_host?.text = Utils.getEditable(
getString(
when (networks[position]) {
"tcp" -> R.string.server_lab_request_host_http
"ws" -> R.string.server_lab_request_host_ws
"httpupgrade" -> R.string.server_lab_request_host_httpupgrade
"splithttp" -> R.string.server_lab_request_host_splithttp
"h2" -> R.string.server_lab_request_host_h2
"quic" -> R.string.server_lab_request_host_quic
"grpc" -> R.string.server_lab_request_host_grpc
else -> R.string.server_lab_request_host
}
)
)
tv_path?.text = Utils.getEditable(
getString(
when (networks[position]) {
"kcp" -> R.string.server_lab_path_kcp
"ws" -> R.string.server_lab_path_ws
"httpupgrade" -> R.string.server_lab_path_httpupgrade
"splithttp" -> R.string.server_lab_path_splithttp
"h2" -> R.string.server_lab_path_h2
"quic" -> R.string.server_lab_path_quic
"grpc" -> R.string.server_lab_path_grpc
else -> R.string.server_lab_path
}
)
)
}
override fun onNothingSelected(parent: AdapterView<*>?) {
@@ -286,7 +322,7 @@ class ServerActivity : BaseActivity() {
tlsSetting.alpn?.let {
val alpnIndex = Utils.arrayFind(
alpns,
Utils.removeWhiteSpace(tlsSetting.alpn.joinToString())!!
Utils.removeWhiteSpace(tlsSetting.alpn.joinToString())?:""
)
sp_stream_alpn?.setSelection(alpnIndex)
}
@@ -414,7 +450,7 @@ class ServerActivity : BaseActivity() {
saveStreamSettings(it)
}
if (config.subscriptionId.isEmpty() && !subscriptionId.isNullOrEmpty()) {
config.subscriptionId = subscriptionId!!
config.subscriptionId = subscriptionId?:""
}
MmkvManager.encodeServerConfig(editGuid, config)

View File

@@ -1,19 +1,27 @@
package com.v2ray.ang.ui
import android.content.Intent
import androidx.recyclerview.widget.LinearLayoutManager
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import androidx.appcompat.app.AlertDialog
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import com.v2ray.ang.R
import android.os.Bundle
import com.v2ray.ang.databinding.ActivitySubSettingBinding
import com.v2ray.ang.databinding.LayoutProgressBinding
import com.v2ray.ang.dto.SubscriptionItem
import com.v2ray.ang.extension.toast
import com.v2ray.ang.util.AngConfigManager
import com.v2ray.ang.util.MmkvManager
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
class SubSettingActivity : BaseActivity() {
private lateinit var binding: ActivitySubSettingBinding
var subscriptions:List<Pair<String, SubscriptionItem>> = listOf()
var subscriptions: List<Pair<String, SubscriptionItem>> = listOf()
private val adapter by lazy { SubSettingRecyclerAdapter(this) }
override fun onCreate(savedInstanceState: Bundle?) {
@@ -37,9 +45,6 @@ class SubSettingActivity : BaseActivity() {
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.action_sub_setting, menu)
menu.findItem(R.id.del_config)?.isVisible = false
menu.findItem(R.id.save_config)?.isVisible = false
return super.onCreateOptionsMenu(menu)
}
@@ -48,6 +53,30 @@ class SubSettingActivity : BaseActivity() {
startActivity(Intent(this, SubEditActivity::class.java))
true
}
R.id.sub_update -> {
val dialog = AlertDialog.Builder(this)
.setView(LayoutProgressBinding.inflate(layoutInflater).root)
.setCancelable(false)
.show()
lifecycleScope.launch(Dispatchers.IO) {
val count = AngConfigManager.updateConfigViaSubAll()
delay(500L)
launch(Dispatchers.Main) {
if (count > 0) {
toast(R.string.toast_success)
} else {
toast(R.string.toast_failure)
}
dialog.dismiss()
}
}
true
}
else -> super.onOptionsItemSelected(item)
}
}

View File

@@ -44,7 +44,7 @@ class TaskerActivity : BaseActivity() {
val adapter = ArrayAdapter(this,
android.R.layout.simple_list_item_single_choice, lstData)
listview = findViewById<View>(R.id.listview) as ListView
listview!!.adapter = adapter
listview?.adapter = adapter
init()
}

View File

@@ -3,7 +3,7 @@ package com.v2ray.ang.ui
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import com.v2ray.ang.AppConfig
import android.util.Log
import com.v2ray.ang.R
import com.v2ray.ang.databinding.ActivityLogcatBinding
import com.v2ray.ang.extension.toast
@@ -24,32 +24,21 @@ class UrlSchemeActivity : BaseActivity() {
if (action == Intent.ACTION_SEND) {
if ("text/plain" == type) {
intent.getStringExtra(Intent.EXTRA_TEXT)?.let {
val uri = Uri.parse(it)
if (uri.scheme?.startsWith(AppConfig.PROTOCOL_HTTPS) == true || uri.scheme?.startsWith(
AppConfig.PROTOCOL_HTTP
) == true
) {
val name = uri.getQueryParameter("name") ?: "Subscription"
importSubscription(it, name)
} else {
importConfig(it)
}
parseUri(it)
}
}
} else if (action == Intent.ACTION_VIEW) {
when (data?.host) {
"install-config" -> {
val uri: Uri? = intent.data
val shareUrl: String = uri?.getQueryParameter("url")!!
toast(shareUrl)
importConfig(shareUrl)
val shareUrl = uri?.getQueryParameter("url") ?: ""
parseUri(shareUrl)
}
"install-sub" -> {
val uri: Uri? = intent.data
val url = uri?.getQueryParameter("url")!!
val name = uri.getQueryParameter("name") ?: "Subscription"
importSubscription(url, name)
val shareUrl = uri?.getQueryParameter("url") ?: ""
parseUri(shareUrl)
}
else -> {
@@ -57,10 +46,8 @@ class UrlSchemeActivity : BaseActivity() {
}
}
}
}
startActivity(Intent(this, MainActivity::class.java))
finish()
} catch (e: Exception) {
@@ -68,19 +55,21 @@ class UrlSchemeActivity : BaseActivity() {
}
}
private fun importSubscription(url: String, name: String) {
val decodedUrl = URLDecoder.decode(url, "UTF-8")
val check = AngConfigManager.importSubscription(name, decodedUrl)
if (check) toast(R.string.import_subscription_success) else toast(R.string.import_subscription_failure)
private fun parseUri(uriString: String?) {
if (uriString.isNullOrEmpty()) {
return
}
Log.d("UrlScheme", uriString)
private fun importConfig(shareUrl: String) {
val count = AngConfigManager.importBatchConfig(shareUrl, "", false)
val decodedUrl = URLDecoder.decode(uriString, "UTF-8")
val uri = Uri.parse(decodedUrl)
if (uri != null) {
val count = AngConfigManager.importBatchConfig(decodedUrl, "", false)
if (count > 0) {
toast(R.string.toast_success)
toast(R.string.import_subscription_success)
} else {
toast(R.string.toast_failure)
toast(R.string.import_subscription_failure)
}
}
}
}

View File

@@ -12,19 +12,16 @@ import com.google.gson.JsonSerializer
import com.google.gson.reflect.TypeToken
import com.tencent.mmkv.MMKV
import com.v2ray.ang.AppConfig
import com.v2ray.ang.AppConfig.PROTOCOL_HTTP
import com.v2ray.ang.AppConfig.PROTOCOL_HTTPS
import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_ADDRESS_V4
import com.v2ray.ang.AppConfig.WIREGUARD_LOCAL_MTU
import com.v2ray.ang.R
import com.v2ray.ang.dto.*
import com.v2ray.ang.dto.V2rayConfig.Companion.DEFAULT_SECURITY
import com.v2ray.ang.dto.V2rayConfig.Companion.TLS
import com.v2ray.ang.extension.idnHost
import com.v2ray.ang.extension.removeWhiteSpace
import com.v2ray.ang.util.MmkvManager.KEY_SELECTED_SERVER
import com.v2ray.ang.util.fmt.ShadowsocksFmt
import com.v2ray.ang.util.fmt.SocksFmt
import com.v2ray.ang.util.fmt.TrojanFmt
import com.v2ray.ang.util.fmt.VlessFmt
import com.v2ray.ang.util.fmt.VmessFmt
import com.v2ray.ang.util.fmt.WireguardFmt
import java.lang.reflect.Type
import java.net.URI
import java.util.*
object AngConfigManager {
@@ -205,9 +202,9 @@ object AngConfigManager {
// }
/**
* import config form qrcode or...
* parse config form qrcode or...
*/
private fun importConfig(
private fun parseConfig(
str: String?,
subid: String,
removedSelectedServer: ServerConfig?
@@ -217,254 +214,22 @@ object AngConfigManager {
return R.string.toast_none_data
}
//maybe sub
if (TextUtils.isEmpty(subid) && (str.startsWith(PROTOCOL_HTTP) || str.startsWith(
PROTOCOL_HTTPS
))
) {
MmkvManager.importUrlAsSubscription(str)
return 0
}
var config: ServerConfig? = null
val allowInsecure = settingsStorage?.decodeBool(AppConfig.PREF_ALLOW_INSECURE) ?: false
if (str.startsWith(EConfigType.VMESS.protocolScheme)) {
config = ServerConfig.create(EConfigType.VMESS)
val streamSetting = config.outboundBean?.streamSettings ?: return -1
if (!tryParseNewVmess(str, config, allowInsecure)) {
if (str.indexOf("?") > 0) {
if (!tryResolveVmess4Kitsunebi(str, config)) {
return R.string.toast_incorrect_protocol
}
} else {
var result = str.replace(EConfigType.VMESS.protocolScheme, "")
result = Utils.decode(result)
if (TextUtils.isEmpty(result)) {
return R.string.toast_decoding_failed
}
val vmessQRCode = Gson().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)
|| TextUtils.isEmpty(vmessQRCode.id)
|| TextUtils.isEmpty(vmessQRCode.net)
) {
return R.string.toast_incorrect_protocol
}
config.remarks = vmessQRCode.ps
config.outboundBean?.settings?.vnext?.get(0)?.let { vnext ->
vnext.address = vmessQRCode.add
vnext.port = Utils.parseInt(vmessQRCode.port)
vnext.users[0].id = vmessQRCode.id
vnext.users[0].security =
if (TextUtils.isEmpty(vmessQRCode.scy)) DEFAULT_SECURITY else vmessQRCode.scy
vnext.users[0].alterId = Utils.parseInt(vmessQRCode.aid)
}
val sni = streamSetting.populateTransportSettings(
vmessQRCode.net,
vmessQRCode.type,
vmessQRCode.host,
vmessQRCode.path,
vmessQRCode.path,
vmessQRCode.host,
vmessQRCode.path,
vmessQRCode.type,
vmessQRCode.path,
vmessQRCode.host
)
val fingerprint = vmessQRCode.fp ?: streamSetting.tlsSettings?.fingerprint
streamSetting.populateTlsSettings(
vmessQRCode.tls, allowInsecure,
if (TextUtils.isEmpty(vmessQRCode.sni)) sni else vmessQRCode.sni,
fingerprint, vmessQRCode.alpn, null, null, null
)
}
}
val config = if (str.startsWith(EConfigType.VMESS.protocolScheme)) {
VmessFmt.parseVmess(str)
} else if (str.startsWith(EConfigType.SHADOWSOCKS.protocolScheme)) {
config = ServerConfig.create(EConfigType.SHADOWSOCKS)
if (!tryResolveResolveSip002(str, config)) {
var result = str.replace(EConfigType.SHADOWSOCKS.protocolScheme, "")
val indexSplit = result.indexOf("#")
if (indexSplit > 0) {
try {
config.remarks =
Utils.urlDecode(result.substring(indexSplit + 1, result.length))
} catch (e: Exception) {
e.printStackTrace()
}
result = result.substring(0, indexSplit)
}
//part decode
val indexS = result.indexOf("@")
result = if (indexS > 0) {
Utils.decode(result.substring(0, indexS)) + result.substring(
indexS,
result.length
)
} else {
Utils.decode(result)
}
val legacyPattern = "^(.+?):(.*)@(.+?):(\\d+?)/?$".toRegex()
val match = legacyPattern.matchEntire(result)
?: return R.string.toast_incorrect_protocol
config.outboundBean?.settings?.servers?.get(0)?.let { server ->
server.address = match.groupValues[3].removeSurrounding("[", "]")
server.port = match.groupValues[4].toInt()
server.password = match.groupValues[2]
server.method = match.groupValues[1].lowercase()
}
}
ShadowsocksFmt.parseShadowsocks(str)
} else if (str.startsWith(EConfigType.SOCKS.protocolScheme)) {
var result = str.replace(EConfigType.SOCKS.protocolScheme, "")
val indexSplit = result.indexOf("#")
config = ServerConfig.create(EConfigType.SOCKS)
if (indexSplit > 0) {
try {
config.remarks =
Utils.urlDecode(result.substring(indexSplit + 1, result.length))
} catch (e: Exception) {
e.printStackTrace()
}
result = result.substring(0, indexSplit)
}
//part decode
val indexS = result.indexOf("@")
if (indexS > 0) {
result = Utils.decode(result.substring(0, indexS)) + result.substring(
indexS,
result.length
)
} else {
result = Utils.decode(result)
}
val legacyPattern = "^(.*):(.*)@(.+?):(\\d+?)$".toRegex()
val match =
legacyPattern.matchEntire(result) ?: return R.string.toast_incorrect_protocol
config.outboundBean?.settings?.servers?.get(0)?.let { server ->
server.address = match.groupValues[3].removeSurrounding("[", "]")
server.port = match.groupValues[4].toInt()
val socksUsersBean =
V2rayConfig.OutboundBean.OutSettingsBean.ServersBean.SocksUsersBean()
socksUsersBean.user = match.groupValues[1]
socksUsersBean.pass = match.groupValues[2]
server.users = listOf(socksUsersBean)
}
SocksFmt.parseSocks(str)
} else if (str.startsWith(EConfigType.TROJAN.protocolScheme)) {
val uri = URI(Utils.fixIllegalUrl(str))
config = ServerConfig.create(EConfigType.TROJAN)
config.remarks = Utils.urlDecode(uri.fragment ?: "")
var flow = ""
var fingerprint = config.outboundBean?.streamSettings?.tlsSettings?.fingerprint
if (uri.rawQuery != null) {
val queryParam = uri.rawQuery.split("&")
.associate { it.split("=").let { (k, v) -> k to Utils.urlDecode(v) } }
val sni = config.outboundBean?.streamSettings?.populateTransportSettings(
queryParam["type"] ?: "tcp",
queryParam["headerType"],
queryParam["host"],
queryParam["path"],
queryParam["seed"],
queryParam["quicSecurity"],
queryParam["key"],
queryParam["mode"],
queryParam["serviceName"],
queryParam["authority"]
)
fingerprint = queryParam["fp"] ?: ""
config.outboundBean?.streamSettings?.populateTlsSettings(
queryParam["security"] ?: TLS,
allowInsecure, queryParam["sni"] ?: sni!!, fingerprint, queryParam["alpn"],
null, null, null
)
flow = queryParam["flow"] ?: ""
} else {
config.outboundBean?.streamSettings?.populateTlsSettings(
TLS, allowInsecure, "",
fingerprint, null, null, null, null
)
}
config.outboundBean?.settings?.servers?.get(0)?.let { server ->
server.address = uri.idnHost
server.port = uri.port
server.password = uri.userInfo
server.flow = flow
}
TrojanFmt.parseTrojan(str)
} else if (str.startsWith(EConfigType.VLESS.protocolScheme)) {
val uri = URI(Utils.fixIllegalUrl(str))
val queryParam = uri.rawQuery.split("&")
.associate { it.split("=").let { (k, v) -> k to Utils.urlDecode(v) } }
config = ServerConfig.create(EConfigType.VLESS)
val streamSetting = config.outboundBean?.streamSettings ?: return -1
config.remarks = Utils.urlDecode(uri.fragment ?: "")
config.outboundBean?.settings?.vnext?.get(0)?.let { vnext ->
vnext.address = uri.idnHost
vnext.port = uri.port
vnext.users[0].id = uri.userInfo
vnext.users[0].encryption = queryParam["encryption"] ?: "none"
vnext.users[0].flow = queryParam["flow"] ?: ""
}
val sni = streamSetting.populateTransportSettings(
queryParam["type"] ?: "tcp",
queryParam["headerType"],
queryParam["host"],
queryParam["path"],
queryParam["seed"],
queryParam["quicSecurity"],
queryParam["key"],
queryParam["mode"],
queryParam["serviceName"],
queryParam["authority"]
)
streamSetting.populateTlsSettings(
queryParam["security"] ?: "",
allowInsecure,
queryParam["sni"] ?: sni,
queryParam["fp"] ?: "",
queryParam["alpn"],
queryParam["pbk"] ?: "",
queryParam["sid"] ?: "",
queryParam["spx"] ?: ""
)
VlessFmt.parseVless(str)
} else if (str.startsWith(EConfigType.WIREGUARD.protocolScheme)) {
val uri = URI(Utils.fixIllegalUrl(str))
config = ServerConfig.create(EConfigType.WIREGUARD)
config.remarks = Utils.urlDecode(uri.fragment ?: "")
WireguardFmt.parseWireguard(str)
} else {
null
}
if (uri.rawQuery != null) {
val queryParam = uri.rawQuery.split("&")
.associate { it.split("=").let { (k, v) -> k to Utils.urlDecode(v) } }
config.outboundBean?.settings?.let { wireguard ->
wireguard.secretKey = uri.userInfo
wireguard.address =
(queryParam["address"] ?: WIREGUARD_LOCAL_ADDRESS_V4).removeWhiteSpace()
.split(",")
wireguard.peers?.get(0)?.publicKey = queryParam["publickey"] ?: ""
wireguard.peers?.get(0)?.endpoint = "${uri.idnHost}:${uri.port}"
wireguard.mtu = Utils.parseInt(queryParam["mtu"] ?: WIREGUARD_LOCAL_MTU)
wireguard.reserved =
(queryParam["reserved"] ?: "0,0,0").removeWhiteSpace().split(",")
.map { it.toInt() }
}
}
}
if (config == null) {
return R.string.toast_incorrect_protocol
}
@@ -487,383 +252,21 @@ object AngConfigManager {
return 0
}
private fun tryParseNewVmess(
uriString: String,
config: ServerConfig,
allowInsecure: Boolean
): Boolean {
return runCatching {
val uri = URI(Utils.fixIllegalUrl(uriString))
check(uri.scheme == "vmess")
val (_, protocol, tlsStr, uuid, alterId) =
Regex("(tcp|http|ws|kcp|quic|grpc)(\\+tls)?:([0-9a-z]{8}-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{12})")
.matchEntire(uri.userInfo)?.groupValues
?: error("parse user info fail.")
val tls = tlsStr.isNotBlank()
val queryParam = uri.rawQuery.split("&")
.associate { it.split("=").let { (k, v) -> k to Utils.urlDecode(v) } }
val streamSetting = config.outboundBean?.streamSettings ?: return false
config.remarks = Utils.urlDecode(uri.fragment ?: "")
config.outboundBean.settings?.vnext?.get(0)?.let { vnext ->
vnext.address = uri.idnHost
vnext.port = uri.port
vnext.users[0].id = uuid
vnext.users[0].security = DEFAULT_SECURITY
vnext.users[0].alterId = alterId.toInt()
}
var fingerprint = streamSetting.tlsSettings?.fingerprint
val sni = streamSetting.populateTransportSettings(protocol,
queryParam["type"],
queryParam["host"]?.split("|")?.get(0) ?: "",
queryParam["path"]?.takeIf { it.trim() != "/" } ?: "",
queryParam["seed"],
queryParam["security"],
queryParam["key"],
queryParam["mode"],
queryParam["serviceName"],
queryParam["authority"])
streamSetting.populateTlsSettings(
if (tls) TLS else "", allowInsecure, sni, fingerprint, null,
null, null, null
)
true
}.getOrElse { false }
}
private fun tryResolveVmess4Kitsunebi(server: String, config: ServerConfig): Boolean {
var result = server.replace(EConfigType.VMESS.protocolScheme, "")
val indexSplit = result.indexOf("?")
if (indexSplit > 0) {
result = result.substring(0, indexSplit)
}
result = Utils.decode(result)
val arr1 = result.split('@')
if (arr1.count() != 2) {
return false
}
val arr21 = arr1[0].split(':')
val arr22 = arr1[1].split(':')
if (arr21.count() != 2) {
return false
}
config.remarks = "Alien"
config.outboundBean?.settings?.vnext?.get(0)?.let { vnext ->
vnext.address = arr22[0]
vnext.port = Utils.parseInt(arr22[1])
vnext.users[0].id = arr21[1]
vnext.users[0].security = arr21[0]
vnext.users[0].alterId = 0
}
return true
}
private fun tryResolveResolveSip002(str: String, config: ServerConfig): Boolean {
try {
val uri = URI(Utils.fixIllegalUrl(str))
config.remarks = Utils.urlDecode(uri.fragment ?: "")
val method: String
val password: String
if (uri.userInfo.contains(":")) {
val arrUserInfo = uri.userInfo.split(":").map { it.trim() }
if (arrUserInfo.count() != 2) {
return false
}
method = arrUserInfo[0]
password = Utils.urlDecode(arrUserInfo[1])
} else {
val base64Decode = Utils.decode(uri.userInfo)
val arrUserInfo = base64Decode.split(":").map { it.trim() }
if (arrUserInfo.count() < 2) {
return false
}
method = arrUserInfo[0]
password = base64Decode.substringAfter(":")
}
val query = Utils.urlDecode(uri.query ?: "")
if (query != "") {
val queryPairs = HashMap<String, String>()
val pairs = query.split(";")
Log.d(AppConfig.ANG_PACKAGE, pairs.toString())
for (pair in pairs) {
val idx = pair.indexOf("=")
if (idx == -1) {
queryPairs[Utils.urlDecode(pair)] = "";
} else {
queryPairs[Utils.urlDecode(pair.substring(0, idx))] =
Utils.urlDecode(pair.substring(idx + 1))
}
}
Log.d(AppConfig.ANG_PACKAGE, queryPairs.toString())
var sni: String? = ""
if (queryPairs["plugin"] == "obfs-local" && queryPairs["obfs"] == "http") {
sni = config.outboundBean?.streamSettings?.populateTransportSettings(
"tcp",
"http",
queryPairs["obfs-host"],
queryPairs["path"],
null,
null,
null,
null,
null,
null
)
} else if (queryPairs["plugin"] == "v2ray-plugin") {
var network = "ws";
if (queryPairs["mode"] == "quic") {
network = "quic";
}
sni = config.outboundBean?.streamSettings?.populateTransportSettings(
network,
null,
queryPairs["host"],
queryPairs["path"],
null,
null,
null,
null,
null,
null
)
}
if ("tls" in queryPairs) {
config.outboundBean?.streamSettings?.populateTlsSettings(
"tls", false, sni ?: "", null, null, null, null, null
)
}
}
config.outboundBean?.settings?.servers?.get(0)?.let { server ->
server.address = uri.idnHost
server.port = uri.port
server.password = password
server.method = method
}
return true
} catch (e: Exception) {
Log.d(AppConfig.ANG_PACKAGE, e.toString())
return false
}
}
/**
* share config
*/
private fun shareConfig(guid: String): String {
try {
val config = MmkvManager.decodeServerConfig(guid) ?: return ""
val outbound = config.getProxyOutbound() ?: return ""
val streamSetting =
outbound.streamSettings ?: V2rayConfig.OutboundBean.StreamSettingsBean()
if (config.configType != EConfigType.WIREGUARD) {
if (outbound.streamSettings == null) return ""
}
return config.configType.protocolScheme + when (config.configType) {
EConfigType.VMESS -> {
val vmessQRCode = VmessQRCode()
vmessQRCode.v = "2"
vmessQRCode.ps = config.remarks
vmessQRCode.add = outbound.getServerAddress().orEmpty()
vmessQRCode.port = outbound.getServerPort().toString()
vmessQRCode.id = outbound.getPassword().orEmpty()
vmessQRCode.aid =
outbound.settings?.vnext?.get(0)?.users?.get(0)?.alterId.toString()
vmessQRCode.scy =
outbound.settings?.vnext?.get(0)?.users?.get(0)?.security.toString()
vmessQRCode.net = streamSetting.network
vmessQRCode.tls = streamSetting.security
vmessQRCode.sni = streamSetting.tlsSettings?.serverName.orEmpty()
vmessQRCode.alpn =
Utils.removeWhiteSpace(streamSetting.tlsSettings?.alpn?.joinToString())
.orEmpty()
vmessQRCode.fp = streamSetting.tlsSettings?.fingerprint.orEmpty()
outbound.getTransportSettingDetails()?.let { transportDetails ->
vmessQRCode.type = transportDetails[0]
vmessQRCode.host = transportDetails[1]
vmessQRCode.path = transportDetails[2]
}
val json = Gson().toJson(vmessQRCode)
Utils.encode(json)
}
EConfigType.VMESS -> VmessFmt.toUri(config)
EConfigType.CUSTOM -> ""
EConfigType.SHADOWSOCKS -> {
val remark = "#" + Utils.urlEncode(config.remarks)
val pw =
Utils.encode("${outbound.getSecurityEncryption()}:${outbound.getPassword()}")
val url = String.format(
"%s@%s:%s",
pw,
Utils.getIpv6Address(outbound.getServerAddress()!!),
outbound.getServerPort()
)
url + remark
}
EConfigType.SOCKS -> {
val remark = "#" + Utils.urlEncode(config.remarks)
val pw =
if (outbound.settings?.servers?.get(0)?.users?.get(0)?.user != null)
"${outbound.settings?.servers?.get(0)?.users?.get(0)?.user}:${outbound.getPassword()}"
else
":"
val url = String.format(
"%s@%s:%s",
Utils.encode(pw),
Utils.getIpv6Address(outbound.getServerAddress()!!),
outbound.getServerPort()
)
url + remark
}
EConfigType.VLESS,
EConfigType.TROJAN -> {
val remark = "#" + Utils.urlEncode(config.remarks)
val dicQuery = HashMap<String, String>()
if (config.configType == EConfigType.VLESS) {
outbound.settings?.vnext?.get(0)?.users?.get(0)?.flow?.let {
if (!TextUtils.isEmpty(it)) {
dicQuery["flow"] = it
}
}
dicQuery["encryption"] =
if (outbound.getSecurityEncryption().isNullOrEmpty()) "none"
else outbound.getSecurityEncryption().orEmpty()
} else if (config.configType == EConfigType.TROJAN) {
config.outboundBean?.settings?.servers?.get(0)?.flow?.let {
if (!TextUtils.isEmpty(it)) {
dicQuery["flow"] = it
}
}
}
dicQuery["security"] = streamSetting.security.ifEmpty { "none" }
(streamSetting.tlsSettings
?: streamSetting.realitySettings)?.let { tlsSetting ->
if (!TextUtils.isEmpty(tlsSetting.serverName)) {
dicQuery["sni"] = tlsSetting.serverName
}
if (!tlsSetting.alpn.isNullOrEmpty() && tlsSetting.alpn.isNotEmpty()) {
dicQuery["alpn"] =
Utils.removeWhiteSpace(tlsSetting.alpn.joinToString()).orEmpty()
}
if (!TextUtils.isEmpty(tlsSetting.fingerprint)) {
dicQuery["fp"] = tlsSetting.fingerprint!!
}
if (!TextUtils.isEmpty(tlsSetting.publicKey)) {
dicQuery["pbk"] = tlsSetting.publicKey!!
}
if (!TextUtils.isEmpty(tlsSetting.shortId)) {
dicQuery["sid"] = tlsSetting.shortId!!
}
if (!TextUtils.isEmpty(tlsSetting.spiderX)) {
dicQuery["spx"] = Utils.urlEncode(tlsSetting.spiderX!!)
}
}
dicQuery["type"] =
streamSetting.network.ifEmpty { V2rayConfig.DEFAULT_NETWORK }
outbound.getTransportSettingDetails()?.let { transportDetails ->
when (streamSetting.network) {
"tcp" -> {
dicQuery["headerType"] = transportDetails[0].ifEmpty { "none" }
if (!TextUtils.isEmpty(transportDetails[1])) {
dicQuery["host"] = Utils.urlEncode(transportDetails[1])
}
}
"kcp" -> {
dicQuery["headerType"] = transportDetails[0].ifEmpty { "none" }
if (!TextUtils.isEmpty(transportDetails[2])) {
dicQuery["seed"] = Utils.urlEncode(transportDetails[2])
}
}
"ws", "httpupgrade" -> {
if (!TextUtils.isEmpty(transportDetails[1])) {
dicQuery["host"] = Utils.urlEncode(transportDetails[1])
}
if (!TextUtils.isEmpty(transportDetails[2])) {
dicQuery["path"] = Utils.urlEncode(transportDetails[2])
}
}
"http", "h2" -> {
dicQuery["type"] = "http"
if (!TextUtils.isEmpty(transportDetails[1])) {
dicQuery["host"] = Utils.urlEncode(transportDetails[1])
}
if (!TextUtils.isEmpty(transportDetails[2])) {
dicQuery["path"] = Utils.urlEncode(transportDetails[2])
}
}
"quic" -> {
dicQuery["headerType"] = transportDetails[0].ifEmpty { "none" }
dicQuery["quicSecurity"] = Utils.urlEncode(transportDetails[1])
dicQuery["key"] = Utils.urlEncode(transportDetails[2])
}
"grpc" -> {
dicQuery["mode"] = transportDetails[0]
dicQuery["authority"] = Utils.urlEncode(transportDetails[1])
dicQuery["serviceName"] = Utils.urlEncode(transportDetails[2])
}
}
}
val query = "?" + dicQuery.toList().joinToString(
separator = "&",
transform = { it.first + "=" + it.second })
val url = String.format(
"%s@%s:%s",
outbound.getPassword(),
Utils.getIpv6Address(outbound.getServerAddress()!!),
outbound.getServerPort()
)
url + query + remark
}
EConfigType.WIREGUARD -> {
val remark = "#" + Utils.urlEncode(config.remarks)
val dicQuery = HashMap<String, String>()
dicQuery["publickey"] =
Utils.urlEncode(outbound.settings?.peers?.get(0)?.publicKey.toString())
if (outbound.settings?.reserved != null) {
dicQuery["reserved"] = Utils.urlEncode(
Utils.removeWhiteSpace(outbound.settings?.reserved?.joinToString())
.toString()
)
}
dicQuery["address"] = Utils.urlEncode(
Utils.removeWhiteSpace((outbound.settings?.address as List<*>).joinToString())
.toString()
)
if (outbound.settings?.mtu != null) {
dicQuery["mtu"] = outbound.settings?.mtu.toString()
}
val query = "?" + dicQuery.toList().joinToString(
separator = "&",
transform = { it.first + "=" + it.second })
val url = String.format(
"%s@%s:%s",
Utils.urlEncode(outbound.getPassword().toString()),
Utils.getIpv6Address(outbound.getServerAddress()!!),
outbound.getServerPort()
)
url + query + remark
}
EConfigType.SHADOWSOCKS -> ShadowsocksFmt.toUri(config)
EConfigType.SOCKS -> SocksFmt.toUri(config)
EConfigType.VLESS -> VlessFmt.toUri(config)
EConfigType.TROJAN -> TrojanFmt.toUri(config)
EConfigType.WIREGUARD -> WireguardFmt.toUri(config)
}
} catch (e: Exception) {
e.printStackTrace()
@@ -983,7 +386,47 @@ object AngConfigManager {
// }
// }
fun importBatchConfig(servers: String?, subid: String, append: Boolean): Int {
fun importBatchConfig(server: String?, subid: String, append: Boolean): Int {
var count = parseBatchConfig(Utils.decode(server), subid, append)
if (count <= 0) {
count = parseBatchConfig(server, subid, append)
}
if (count <= 0) {
count = parseCustomConfigServer(server, subid)
}
var countSub = parseBatchSubscription(server)
if (countSub <= 0) {
countSub = parseBatchSubscription(Utils.decode(server))
}
if (countSub > 0) {
updateConfigViaSubAll()
}
return count + countSub
}
fun parseBatchSubscription(servers: String?): Int {
try {
if (servers == null) {
return 0
}
var count = 0
servers.lines()
.forEach { str ->
if (str.startsWith(AppConfig.PROTOCOL_HTTP) || str.startsWith(AppConfig.PROTOCOL_HTTPS)) {
count += MmkvManager.importUrlAsSubscription(str)
}
}
return count
} catch (e: Exception) {
e.printStackTrace()
}
return 0
}
fun parseBatchConfig(servers: String?, subid: String, append: Boolean): Int {
try {
if (servers == null) {
return 0
@@ -1004,16 +447,12 @@ object AngConfigManager {
if (!append) {
MmkvManager.removeServerViaSubid(subid)
}
// var servers = server
// if (server.indexOf("vmess") >= 0 && server.indexOf("vmess") == server.lastIndexOf("vmess")) {
// servers = server.replace("\n", "")
// }
var count = 0
servers.lines()
.reversed()
.forEach {
val resId = importConfig(it, subid, removedSelectedServer)
val resId = parseConfig(it, subid, removedSelectedServer)
if (resId == 0) {
count++
}
@@ -1025,24 +464,7 @@ object AngConfigManager {
return 0
}
fun importSubscription(remark: String, url: String, enabled: Boolean = true): Boolean {
val subId = Utils.getUuid()
val subItem = SubscriptionItem()
subItem.remarks = remark
subItem.url = url
subItem.enabled = enabled
if (TextUtils.isEmpty(subItem.remarks) || TextUtils.isEmpty(subItem.url)) {
return false
}
subStorage?.encode(subId, Gson().toJson(subItem))
return true
}
fun appendCustomConfigServer(server: String?, subid: String): Int {
fun parseCustomConfigServer(server: String?, subid: String): Int {
if (server == null) {
return 0
}
@@ -1069,9 +491,10 @@ object AngConfigManager {
if (serverList.isNotEmpty()) {
var count = 0
for (srv in serverList) {
for (srv in serverList.reversed()) {
val config = ServerConfig.create(EConfigType.CUSTOM)
config.fullConfig = Gson().fromJson(Gson().toJson(srv), V2rayConfig::class.java)
config.fullConfig =
Gson().fromJson(Gson().toJson(srv), V2rayConfig::class.java)
config.remarks = config.fullConfig?.remarks
?: ("%04d-".format(count + 1) + System.currentTimeMillis()
.toString())
@@ -1098,4 +521,72 @@ object AngConfigManager {
return 0
}
}
fun updateConfigViaSubAll(): Int {
var count = 0
try {
MmkvManager.decodeSubscriptions().forEach {
count += updateConfigViaSub(it)
}
} catch (e: Exception) {
e.printStackTrace()
return 0
}
return count
}
private fun updateConfigViaSub(it: Pair<String, SubscriptionItem>): Int {
try {
if (TextUtils.isEmpty(it.first)
|| TextUtils.isEmpty(it.second.remarks)
|| TextUtils.isEmpty(it.second.url)
) {
return 0
}
if (!it.second.enabled) {
return 0
}
val url = Utils.idnToASCII(it.second.url)
if (!Utils.isValidUrl(url)) {
return 0
}
Log.d(AppConfig.ANG_PACKAGE, url)
var configText = try {
Utils.getUrlContentWithCustomUserAgent(url)
} 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)
} catch (e: Exception) {
e.printStackTrace()
""
}
}
if (configText.isEmpty()) {
return 0
}
return parseConfigViaSub(configText, it.first, false)
} catch (e: Exception) {
e.printStackTrace()
return 0
}
}
private fun parseConfigViaSub(server: String?, subid: String, append: Boolean): Int {
var count = parseBatchConfig(Utils.decode(server), subid, append)
if (count <= 0) {
count = parseBatchConfig(server, subid, append)
}
if (count <= 0) {
count = parseCustomConfigServer(server, subid)
}
return count
}
}

View File

@@ -7,7 +7,6 @@ import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import com.v2ray.ang.dto.AppInfo
import rx.Observable
import java.util.*
object AppManagerUtil {
fun loadNetworkAppList(ctx: Context): ArrayList<AppInfo> {
@@ -31,7 +30,8 @@ object AppManagerUtil {
return apps
}
fun rxLoadNetworkAppList(ctx: Context): Observable<ArrayList<AppInfo>> = Observable.unsafeCreate {
fun rxLoadNetworkAppList(ctx: Context): Observable<ArrayList<AppInfo>> =
Observable.unsafeCreate {
it.onNext(loadNetworkAppList(ctx))
}

View File

@@ -179,7 +179,7 @@ object MmkvManager {
}
}
fun sortByTestResults( ) {
fun sortByTestResults() {
data class ServerDelay(var guid: String, var testDelayMillis: Long)
val serverDelays = mutableListOf<ServerDelay>()

View File

@@ -7,7 +7,7 @@ import android.content.res.Resources
import android.os.Build
import android.os.LocaleList
import androidx.annotation.RequiresApi
import java.util.*
import java.util.Locale
open class MyContextWrapper(base: Context?) : ContextWrapper(base) {
companion object {

View File

@@ -52,7 +52,8 @@ object SpeedtestUtil {
val allText = process.inputStream.bufferedReader().use { it.readText() }
if (!TextUtils.isEmpty(allText)) {
val tempInfo = allText.substring(allText.indexOf("min/avg/max/mdev") + 19)
val temps = tempInfo.split("/".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
val temps =
tempInfo.split("/".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
if (temps.count() > 0 && temps[0].length < 10) {
return temps[0].toFloat().toInt().toString() + "ms"
}
@@ -70,7 +71,7 @@ object SpeedtestUtil {
tcpTestingSockets.add(socket)
}
val start = System.currentTimeMillis()
socket.connect(InetSocketAddress(url, port),3000)
socket.connect(InetSocketAddress(url, port), 3000)
val time = System.currentTimeMillis() - start
synchronized(this) {
tcpTestingSockets.remove(socket)
@@ -105,8 +106,11 @@ object SpeedtestUtil {
val url = URL(Utils.getDelayTestUrl())
conn = url.openConnection(
Proxy(Proxy.Type.HTTP,
InetSocketAddress("127.0.0.1", port))) as HttpURLConnection
Proxy(
Proxy.Type.HTTP,
InetSocketAddress("127.0.0.1", port)
)
) as HttpURLConnection
conn.connectTimeout = 30000
conn.readTimeout = 30000
conn.setRequestProperty("Connection", "close")
@@ -120,11 +124,19 @@ object SpeedtestUtil {
if (code == 204 || code == 200 && conn.responseLength == 0L) {
result = context.getString(R.string.connection_test_available, elapsed)
} else {
throw IOException(context.getString(R.string.connection_test_error_status_code, code))
throw IOException(
context.getString(
R.string.connection_test_error_status_code,
code
)
)
}
} 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

View File

@@ -39,8 +39,8 @@ object Utils {
* @param text
* @return
*/
fun getEditable(text: String): Editable {
return Editable.Factory.getInstance().newEditable(text)
fun getEditable(text: String?): Editable {
return Editable.Factory.getInstance().newEditable(text?:"")
}
/**
@@ -101,16 +101,16 @@ object Utils {
/**
* base64 decode
*/
fun decode(text: String): String {
fun decode(text: String?): String {
tryDecodeBase64(text)?.let { return it }
if (text.endsWith('=')) {
if (text?.endsWith('=')==true) {
// try again for some loosely formatted base64
tryDecodeBase64(text.trimEnd('='))?.let { return it }
}
return ""
}
fun tryDecodeBase64(text: String): String? {
fun tryDecodeBase64(text: String?): String? {
try {
return Base64.decode(text, Base64.NO_WRAP).toString(charset("UTF-8"))
} catch (e: Exception) {
@@ -236,7 +236,13 @@ object Utils {
*/
fun isValidUrl(value: String?): Boolean {
try {
if (value != null && Patterns.WEB_URL.matcher(value).matches() || URLUtil.isValidUrl(value)) {
if (value.isNullOrEmpty()) {
return false
}
if (Patterns.WEB_URL.matcher(value).matches()
|| Patterns.DOMAIN_NAME.matcher(value).matches()
|| URLUtil.isValidUrl(value)
) {
return true
}
} catch (e: Exception) {
@@ -302,7 +308,11 @@ object Utils {
/**
* readTextFromAssets
*/
fun readTextFromAssets(context: Context, fileName: String): String {
fun readTextFromAssets(context: Context?, fileName: String): String {
if(context == null)
{
return ""
}
val content = context.assets.open(fileName).bufferedReader().use {
it.readText()
}
@@ -352,7 +362,7 @@ object Utils {
}
@Throws(IOException::class)
fun getUrlContentWithCustomUserAgent(urlStr: String?, httpPort: Int = 0): String {
fun getUrlContentWithCustomUserAgent(urlStr: String?, timeout: Int = 30000, httpPort: Int = 0): String {
val url = URL(urlStr)
val conn = if (httpPort == 0) {
url.openConnection()
@@ -364,6 +374,8 @@ object Utils {
)
)
}
conn.connectTimeout = timeout
conn.readTimeout = timeout
conn.setRequestProperty("Connection", "close")
conn.setRequestProperty("User-agent", "v2rayNG/${BuildConfig.VERSION_NAME}")
url.userInfo?.let {
@@ -389,7 +401,10 @@ object Utils {
}
}
fun getIpv6Address(address: String): String {
fun getIpv6Address(address: String?): String {
if(address == null){
return ""
}
return if (isIpv6Address(address) && !address.contains('[') && !address.contains(']')) {
String.format("[%s]", address)
} else {
@@ -438,5 +453,14 @@ object Utils {
val url = settingsStorage.decodeString(AppConfig.PREF_DELAY_TEST_URL)
return if (url.isNullOrEmpty()) AppConfig.DelayTestUrl else url
}
fun getDelayTestUrl(second: Boolean = false): String {
return if (second) {
AppConfig.DelayTestUrl2
} else {
val url = settingsStorage.decodeString(AppConfig.PREF_DELAY_TEST_URL)
if (url.isNullOrEmpty()) AppConfig.DelayTestUrl else url
}
}
}

View File

@@ -2,9 +2,11 @@ package com.v2ray.ang.util
import android.content.Context
import android.text.TextUtils
import android.util.Log
import com.google.gson.Gson
import com.tencent.mmkv.MMKV
import com.v2ray.ang.AppConfig
import com.v2ray.ang.AppConfig.ANG_PACKAGE
import com.v2ray.ang.AppConfig.PROTOCOL_FREEDOM
import com.v2ray.ang.AppConfig.TAG_DIRECT
import com.v2ray.ang.AppConfig.TAG_FRAGMENT
@@ -49,6 +51,14 @@ object V2rayConfigUtil {
return Result(true, customConfig)
}
val outbound = config.getProxyOutbound() ?: return Result(false, "")
val address = outbound.getServerAddress() ?: return Result(false, "")
if (!Utils.isIpAddress(address)) {
if (!Utils.isValidUrl(address)) {
Log.d(ANG_PACKAGE, "$address is an invalid ip or domain")
return Result(false, "")
}
}
val result = getV2rayNonCustomConfig(context, outbound, config.remarks)
//Log.d(ANG_PACKAGE, result.content)
return result
@@ -134,7 +144,8 @@ object V2rayConfigUtil {
settingsStorage?.decodeBool(AppConfig.PREF_SNIFFING_ENABLED, true)
?: true
v2rayConfig.inbounds[0].sniffing?.enabled = fakedns || sniffAllTlsAndHttp
v2rayConfig.inbounds[0].sniffing?.routeOnly = settingsStorage?.decodeBool(AppConfig.PREF_ROUTE_ONLY_ENABLED, false)
v2rayConfig.inbounds[0].sniffing?.routeOnly =
settingsStorage?.decodeBool(AppConfig.PREF_ROUTE_ONLY_ENABLED, false)
if (!sniffAllTlsAndHttp) {
v2rayConfig.inbounds[0].sniffing?.destOverride?.clear()
}
@@ -162,10 +173,6 @@ object V2rayConfigUtil {
&& settingsStorage?.decodeBool(AppConfig.PREF_FAKE_DNS_ENABLED) == true
) {
v2rayConfig.fakedns = listOf(V2rayConfig.FakednsBean())
v2rayConfig.outbounds.filter { it.protocol == PROTOCOL_FREEDOM && it.tag == TAG_DIRECT }
.forEach {
it.settings?.domainStrategy = "UseIP"
}
}
}
@@ -180,7 +187,7 @@ object V2rayConfigUtil {
)
routingUserRule(
settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_DIRECT)
?: "", AppConfig.TAG_DIRECT, v2rayConfig
?: "", TAG_DIRECT, v2rayConfig
)
routingUserRule(
settingsStorage?.decodeString(AppConfig.PREF_V2RAY_ROUTING_BLOCKED)
@@ -194,45 +201,46 @@ object V2rayConfigUtil {
val routingMode = settingsStorage?.decodeString(AppConfig.PREF_ROUTING_MODE)
?: ERoutingMode.BYPASS_LAN_MAINLAND.value
// Hardcode googleapis.cn
// Hardcode googleapis.cn gstatic.com
val googleapisRoute = V2rayConfig.RoutingBean.RulesBean(
outboundTag = AppConfig.TAG_PROXY,
domain = arrayListOf("domain:googleapis.cn")
domain = arrayListOf("domain:googleapis.cn", "domain:gstatic.com")
)
when (routingMode) {
ERoutingMode.BYPASS_LAN.value -> {
routingGeo("ip", "private", AppConfig.TAG_DIRECT, v2rayConfig)
routingGeo("ip", "private", TAG_DIRECT, v2rayConfig)
}
ERoutingMode.BYPASS_MAINLAND.value -> {
routingGeo("", "cn", AppConfig.TAG_DIRECT, v2rayConfig)
routingGeo("domain", "geolocation-cn", AppConfig.TAG_DIRECT, v2rayConfig)
routingGeo("", "cn", TAG_DIRECT, v2rayConfig)
routingGeo("domain", "geolocation-cn", TAG_DIRECT, v2rayConfig)
v2rayConfig.routing.rules.add(0, googleapisRoute)
}
ERoutingMode.BYPASS_LAN_MAINLAND.value -> {
routingGeo("ip", "private", AppConfig.TAG_DIRECT, v2rayConfig)
routingGeo("", "cn", AppConfig.TAG_DIRECT, v2rayConfig)
routingGeo("domain", "geolocation-cn", AppConfig.TAG_DIRECT, v2rayConfig)
routingGeo("ip", "private", TAG_DIRECT, v2rayConfig)
routingGeo("", "cn", TAG_DIRECT, v2rayConfig)
routingGeo("domain", "geolocation-cn", TAG_DIRECT, v2rayConfig)
v2rayConfig.routing.rules.add(0, googleapisRoute)
}
ERoutingMode.GLOBAL_DIRECT.value -> {
val globalDirect = V2rayConfig.RoutingBean.RulesBean(
outboundTag = AppConfig.TAG_DIRECT,
outboundTag = TAG_DIRECT,
port = "0-65535"
)
v2rayConfig.routing.rules.add(globalDirect)
}
}
if(routingMode != ERoutingMode.GLOBAL_DIRECT.value) {
if (routingMode != ERoutingMode.GLOBAL_DIRECT.value) {
v2rayConfig.routing.rules.add(
V2rayConfig.RoutingBean.RulesBean(
outboundTag = AppConfig.TAG_PROXY,
port = "0-65535"
))
)
)
}
} catch (e: Exception) {
@@ -295,10 +303,10 @@ object V2rayConfigUtil {
rulesDomain.domain?.add(it)
}
}
if (rulesDomain.domain?.size!! > 0) {
if ((rulesDomain.domain?.size ?: 0) > 0) {
v2rayConfig.routing.rules.add(rulesDomain)
}
if (rulesIP.ip?.size!! > 0) {
if ((rulesIP.ip?.size ?: 0) > 0) {
v2rayConfig.routing.rules.add(rulesIP)
}
}
@@ -457,7 +465,7 @@ object V2rayConfigUtil {
if (Utils.isPureIpAddress(domesticDns.first())) {
v2rayConfig.routing.rules.add(
0, V2rayConfig.RoutingBean.RulesBean(
outboundTag = AppConfig.TAG_DIRECT,
outboundTag = TAG_DIRECT,
port = "53",
ip = arrayListOf(domesticDns.first()),
domain = null
@@ -560,7 +568,7 @@ object V2rayConfigUtil {
} else {
path
}
outbound.streamSettings?.tcpSettings?.header?.request?.headers?.Host = host!!
outbound.streamSettings?.tcpSettings?.header?.request?.headers?.Host = host
}
@@ -589,7 +597,8 @@ object V2rayConfigUtil {
mux = null
)
var packets = settingsStorage?.decodeString(AppConfig.PREF_FRAGMENT_PACKETS) ?: "tlshello"
var packets =
settingsStorage?.decodeString(AppConfig.PREF_FRAGMENT_PACKETS) ?: "tlshello"
if (v2rayConfig.outbounds[0].streamSettings?.security == V2rayConfig.REALITY
&& packets == "tlshello"
) {

View File

@@ -0,0 +1,159 @@
package com.v2ray.ang.util.fmt
import android.util.Log
import com.v2ray.ang.AppConfig
import com.v2ray.ang.dto.EConfigType
import com.v2ray.ang.dto.ServerConfig
import com.v2ray.ang.extension.idnHost
import com.v2ray.ang.util.Utils
import java.net.URI
object ShadowsocksFmt {
fun parseShadowsocks(str: String): ServerConfig? {
val config = ServerConfig.create(EConfigType.SHADOWSOCKS)
if (!tryResolveResolveSip002(str, config)) {
var result = str.replace(EConfigType.SHADOWSOCKS.protocolScheme, "")
val indexSplit = result.indexOf("#")
if (indexSplit > 0) {
try {
config.remarks =
Utils.urlDecode(result.substring(indexSplit + 1, result.length))
} catch (e: Exception) {
e.printStackTrace()
}
result = result.substring(0, indexSplit)
}
//part decode
val indexS = result.indexOf("@")
result = if (indexS > 0) {
Utils.decode(result.substring(0, indexS)) + result.substring(
indexS,
result.length
)
} else {
Utils.decode(result)
}
val legacyPattern = "^(.+?):(.*)@(.+?):(\\d+?)/?$".toRegex()
val match = legacyPattern.matchEntire(result)
?: return null
config.outboundBean?.settings?.servers?.get(0)?.let { server ->
server.address = match.groupValues[3].removeSurrounding("[", "]")
server.port = match.groupValues[4].toInt()
server.password = match.groupValues[2]
server.method = match.groupValues[1].lowercase()
}
}
return config
}
fun toUri(config: ServerConfig): String {
val outbound = config.getProxyOutbound() ?: return ""
val remark = "#" + Utils.urlEncode(config.remarks)
val pw =
Utils.encode("${outbound.getSecurityEncryption()}:${outbound.getPassword()}")
val url = String.format(
"%s@%s:%s",
pw,
Utils.getIpv6Address(outbound.getServerAddress()),
outbound.getServerPort()
)
return url + remark
}
private fun tryResolveResolveSip002(str: String, config: ServerConfig): Boolean {
try {
val uri = URI(Utils.fixIllegalUrl(str))
config.remarks = Utils.urlDecode(uri.fragment ?: "")
val method: String
val password: String
if (uri.userInfo.contains(":")) {
val arrUserInfo = uri.userInfo.split(":").map { it.trim() }
if (arrUserInfo.count() != 2) {
return false
}
method = arrUserInfo[0]
password = Utils.urlDecode(arrUserInfo[1])
} else {
val base64Decode = Utils.decode(uri.userInfo)
val arrUserInfo = base64Decode.split(":").map { it.trim() }
if (arrUserInfo.count() < 2) {
return false
}
method = arrUserInfo[0]
password = base64Decode.substringAfter(":")
}
val query = Utils.urlDecode(uri.query ?: "")
if (query != "") {
val queryPairs = HashMap<String, String>()
val pairs = query.split(";")
Log.d(AppConfig.ANG_PACKAGE, pairs.toString())
for (pair in pairs) {
val idx = pair.indexOf("=")
if (idx == -1) {
queryPairs[Utils.urlDecode(pair)] = ""
} else {
queryPairs[Utils.urlDecode(pair.substring(0, idx))] =
Utils.urlDecode(pair.substring(idx + 1))
}
}
Log.d(AppConfig.ANG_PACKAGE, queryPairs.toString())
var sni: String? = ""
if (queryPairs["plugin"] == "obfs-local" && queryPairs["obfs"] == "http") {
sni = config.outboundBean?.streamSettings?.populateTransportSettings(
"tcp",
"http",
queryPairs["obfs-host"],
queryPairs["path"],
null,
null,
null,
null,
null,
null
)
} else if (queryPairs["plugin"] == "v2ray-plugin") {
var network = "ws"
if (queryPairs["mode"] == "quic") {
network = "quic"
}
sni = config.outboundBean?.streamSettings?.populateTransportSettings(
network,
null,
queryPairs["host"],
queryPairs["path"],
null,
null,
null,
null,
null,
null
)
}
if ("tls" in queryPairs) {
config.outboundBean?.streamSettings?.populateTlsSettings(
"tls", false, sni ?: "", null, null, null, null, null
)
}
}
config.outboundBean?.settings?.servers?.get(0)?.let { server ->
server.address = uri.idnHost
server.port = uri.port
server.password = password
server.method = method
}
return true
} catch (e: Exception) {
Log.d(AppConfig.ANG_PACKAGE, e.toString())
return false
}
}
}

View File

@@ -0,0 +1,69 @@
package com.v2ray.ang.util.fmt
import com.v2ray.ang.dto.EConfigType
import com.v2ray.ang.dto.ServerConfig
import com.v2ray.ang.dto.V2rayConfig
import com.v2ray.ang.util.Utils
object SocksFmt {
fun parseSocks(str: String): ServerConfig? {
val config = ServerConfig.create(EConfigType.SOCKS)
var result = str.replace(EConfigType.SOCKS.protocolScheme, "")
val indexSplit = result.indexOf("#")
if (indexSplit > 0) {
try {
config.remarks =
Utils.urlDecode(result.substring(indexSplit + 1, result.length))
} catch (e: Exception) {
e.printStackTrace()
}
result = result.substring(0, indexSplit)
}
//part decode
val indexS = result.indexOf("@")
if (indexS > 0) {
result = Utils.decode(result.substring(0, indexS)) + result.substring(
indexS,
result.length
)
} else {
result = Utils.decode(result)
}
val legacyPattern = "^(.*):(.*)@(.+?):(\\d+?)$".toRegex()
val match =
legacyPattern.matchEntire(result) ?: return null
config.outboundBean?.settings?.servers?.get(0)?.let { server ->
server.address = match.groupValues[3].removeSurrounding("[", "]")
server.port = match.groupValues[4].toInt()
val socksUsersBean =
V2rayConfig.OutboundBean.OutSettingsBean.ServersBean.SocksUsersBean()
socksUsersBean.user = match.groupValues[1]
socksUsersBean.pass = match.groupValues[2]
server.users = listOf(socksUsersBean)
}
return config
}
fun toUri(config: ServerConfig): String {
val outbound = config.getProxyOutbound() ?: return ""
val remark = "#" + Utils.urlEncode(config.remarks)
val pw =
if (outbound.settings?.servers?.get(0)?.users?.get(0)?.user != null)
"${outbound.settings?.servers?.get(0)?.users?.get(0)?.user}:${outbound.getPassword()}"
else
":"
val url = String.format(
"%s@%s:%s",
Utils.encode(pw),
Utils.getIpv6Address(outbound.getServerAddress()),
outbound.getServerPort()
)
return url + remark
}
}

View File

@@ -0,0 +1,180 @@
package com.v2ray.ang.util.fmt
import android.text.TextUtils
import com.tencent.mmkv.MMKV
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.extension.idnHost
import com.v2ray.ang.util.MmkvManager
import com.v2ray.ang.util.Utils
import java.net.URI
object TrojanFmt {
private val settingsStorage by lazy {
MMKV.mmkvWithID(
MmkvManager.ID_SETTING,
MMKV.MULTI_PROCESS_MODE
)
}
fun parseTrojan(str: String): ServerConfig? {
var allowInsecure = settingsStorage?.decodeBool(AppConfig.PREF_ALLOW_INSECURE) ?: false
val config = ServerConfig.create(EConfigType.TROJAN)
val uri = URI(Utils.fixIllegalUrl(str))
config.remarks = Utils.urlDecode(uri.fragment ?: "")
var flow = ""
var fingerprint = config.outboundBean?.streamSettings?.tlsSettings?.fingerprint
if (uri.rawQuery.isNullOrEmpty()) {
config.outboundBean?.streamSettings?.populateTlsSettings(
V2rayConfig.TLS,
allowInsecure,
"",
fingerprint,
null,
null,
null,
null
)
} else {
val queryParam = uri.rawQuery.split("&")
.associate { it.split("=").let { (k, v) -> k to Utils.urlDecode(v) } }
val sni = config.outboundBean?.streamSettings?.populateTransportSettings(
queryParam["type"] ?: "tcp",
queryParam["headerType"],
queryParam["host"],
queryParam["path"],
queryParam["seed"],
queryParam["quicSecurity"],
queryParam["key"],
queryParam["mode"],
queryParam["serviceName"],
queryParam["authority"]
)
fingerprint = queryParam["fp"] ?: ""
allowInsecure = if ((queryParam["allowInsecure"] ?: "") == "1") true else allowInsecure
config.outboundBean?.streamSettings?.populateTlsSettings(
queryParam["security"] ?: V2rayConfig.TLS,
allowInsecure,
queryParam["sni"] ?: sni ?: "",
fingerprint,
queryParam["alpn"],
null,
null,
null
)
flow = queryParam["flow"] ?: ""
}
config.outboundBean?.settings?.servers?.get(0)?.let { server ->
server.address = uri.idnHost
server.port = uri.port
server.password = uri.userInfo
server.flow = flow
}
return config
}
fun toUri(config: ServerConfig): String {
val outbound = config.getProxyOutbound() ?: return ""
val streamSetting = outbound.streamSettings ?: V2rayConfig.OutboundBean.StreamSettingsBean()
val remark = "#" + Utils.urlEncode(config.remarks)
val dicQuery = HashMap<String, String>()
config.outboundBean?.settings?.servers?.get(0)?.flow?.let {
if (!TextUtils.isEmpty(it)) {
dicQuery["flow"] = it
}
}
dicQuery["security"] = streamSetting.security.ifEmpty { "none" }
(streamSetting.tlsSettings
?: streamSetting.realitySettings)?.let { tlsSetting ->
if (!TextUtils.isEmpty(tlsSetting.serverName)) {
dicQuery["sni"] = tlsSetting.serverName
}
if (!tlsSetting.alpn.isNullOrEmpty() && tlsSetting.alpn.isNotEmpty()) {
dicQuery["alpn"] =
Utils.removeWhiteSpace(tlsSetting.alpn.joinToString()).orEmpty()
}
if (!TextUtils.isEmpty(tlsSetting.fingerprint)) {
dicQuery["fp"] = tlsSetting.fingerprint ?: ""
}
if (!TextUtils.isEmpty(tlsSetting.publicKey)) {
dicQuery["pbk"] = tlsSetting.publicKey ?: ""
}
if (!TextUtils.isEmpty(tlsSetting.shortId)) {
dicQuery["sid"] = tlsSetting.shortId ?: ""
}
if (!TextUtils.isEmpty(tlsSetting.spiderX)) {
dicQuery["spx"] = Utils.urlEncode(tlsSetting.spiderX ?: "")
}
}
dicQuery["type"] =
streamSetting.network.ifEmpty { V2rayConfig.DEFAULT_NETWORK }
outbound.getTransportSettingDetails()?.let { transportDetails ->
when (streamSetting.network) {
"tcp" -> {
dicQuery["headerType"] = transportDetails[0].ifEmpty { "none" }
if (!TextUtils.isEmpty(transportDetails[1])) {
dicQuery["host"] = Utils.urlEncode(transportDetails[1])
}
}
"kcp" -> {
dicQuery["headerType"] = transportDetails[0].ifEmpty { "none" }
if (!TextUtils.isEmpty(transportDetails[2])) {
dicQuery["seed"] = Utils.urlEncode(transportDetails[2])
}
}
"ws", "httpupgrade", "splithttp" -> {
if (!TextUtils.isEmpty(transportDetails[1])) {
dicQuery["host"] = Utils.urlEncode(transportDetails[1])
}
if (!TextUtils.isEmpty(transportDetails[2])) {
dicQuery["path"] = Utils.urlEncode(transportDetails[2])
}
}
"http", "h2" -> {
dicQuery["type"] = "http"
if (!TextUtils.isEmpty(transportDetails[1])) {
dicQuery["host"] = Utils.urlEncode(transportDetails[1])
}
if (!TextUtils.isEmpty(transportDetails[2])) {
dicQuery["path"] = Utils.urlEncode(transportDetails[2])
}
}
"quic" -> {
dicQuery["headerType"] = transportDetails[0].ifEmpty { "none" }
dicQuery["quicSecurity"] = Utils.urlEncode(transportDetails[1])
dicQuery["key"] = Utils.urlEncode(transportDetails[2])
}
"grpc" -> {
dicQuery["mode"] = transportDetails[0]
dicQuery["authority"] = Utils.urlEncode(transportDetails[1])
dicQuery["serviceName"] = Utils.urlEncode(transportDetails[2])
}
}
}
val query = "?" + dicQuery.toList().joinToString(
separator = "&",
transform = { it.first + "=" + it.second })
val url = String.format(
"%s@%s:%s",
outbound.getPassword(),
Utils.getIpv6Address(outbound.getServerAddress()),
outbound.getServerPort()
)
return url + query + remark
}
}

View File

@@ -0,0 +1,171 @@
package com.v2ray.ang.util.fmt
import android.text.TextUtils
import com.tencent.mmkv.MMKV
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.extension.idnHost
import com.v2ray.ang.util.MmkvManager
import com.v2ray.ang.util.Utils
import java.net.URI
object VlessFmt {
private val settingsStorage by lazy {
MMKV.mmkvWithID(
MmkvManager.ID_SETTING,
MMKV.MULTI_PROCESS_MODE
)
}
fun parseVless(str: String): ServerConfig? {
var allowInsecure = settingsStorage?.decodeBool(AppConfig.PREF_ALLOW_INSECURE) ?: false
val config = ServerConfig.create(EConfigType.VLESS)
val uri = URI(Utils.fixIllegalUrl(str))
if (uri.rawQuery.isNullOrEmpty()) return null
val queryParam = uri.rawQuery.split("&")
.associate { it.split("=").let { (k, v) -> k to Utils.urlDecode(v) } }
val streamSetting = config.outboundBean?.streamSettings ?: return null
config.remarks = Utils.urlDecode(uri.fragment ?: "")
config.outboundBean.settings?.vnext?.get(0)?.let { vnext ->
vnext.address = uri.idnHost
vnext.port = uri.port
vnext.users[0].id = uri.userInfo
vnext.users[0].encryption = queryParam["encryption"] ?: "none"
vnext.users[0].flow = queryParam["flow"] ?: ""
}
val sni = streamSetting.populateTransportSettings(
queryParam["type"] ?: "tcp",
queryParam["headerType"],
queryParam["host"],
queryParam["path"],
queryParam["seed"],
queryParam["quicSecurity"],
queryParam["key"],
queryParam["mode"],
queryParam["serviceName"],
queryParam["authority"]
)
allowInsecure = if ((queryParam["allowInsecure"] ?: "") == "1") true else allowInsecure
streamSetting.populateTlsSettings(
queryParam["security"] ?: "",
allowInsecure,
queryParam["sni"] ?: sni,
queryParam["fp"] ?: "",
queryParam["alpn"],
queryParam["pbk"] ?: "",
queryParam["sid"] ?: "",
queryParam["spx"] ?: ""
)
return config
}
fun toUri(config: ServerConfig): String {
val outbound = config.getProxyOutbound() ?: return ""
val streamSetting = outbound.streamSettings ?: V2rayConfig.OutboundBean.StreamSettingsBean()
val remark = "#" + Utils.urlEncode(config.remarks)
val dicQuery = HashMap<String, String>()
outbound.settings?.vnext?.get(0)?.users?.get(0)?.flow?.let {
if (!TextUtils.isEmpty(it)) {
dicQuery["flow"] = it
}
}
dicQuery["encryption"] =
if (outbound.getSecurityEncryption().isNullOrEmpty()) "none"
else outbound.getSecurityEncryption().orEmpty()
dicQuery["security"] = streamSetting.security.ifEmpty { "none" }
(streamSetting.tlsSettings
?: streamSetting.realitySettings)?.let { tlsSetting ->
if (!TextUtils.isEmpty(tlsSetting.serverName)) {
dicQuery["sni"] = tlsSetting.serverName
}
if (!tlsSetting.alpn.isNullOrEmpty() && tlsSetting.alpn.isNotEmpty()) {
dicQuery["alpn"] =
Utils.removeWhiteSpace(tlsSetting.alpn.joinToString()).orEmpty()
}
if (!TextUtils.isEmpty(tlsSetting.fingerprint)) {
dicQuery["fp"] = tlsSetting.fingerprint ?: ""
}
if (!TextUtils.isEmpty(tlsSetting.publicKey)) {
dicQuery["pbk"] = tlsSetting.publicKey ?: ""
}
if (!TextUtils.isEmpty(tlsSetting.shortId)) {
dicQuery["sid"] = tlsSetting.shortId ?: ""
}
if (!TextUtils.isEmpty(tlsSetting.spiderX)) {
dicQuery["spx"] = Utils.urlEncode(tlsSetting.spiderX ?: "")
}
}
dicQuery["type"] =
streamSetting.network.ifEmpty { V2rayConfig.DEFAULT_NETWORK }
outbound.getTransportSettingDetails()?.let { transportDetails ->
when (streamSetting.network) {
"tcp" -> {
dicQuery["headerType"] = transportDetails[0].ifEmpty { "none" }
if (!TextUtils.isEmpty(transportDetails[1])) {
dicQuery["host"] = Utils.urlEncode(transportDetails[1])
}
}
"kcp" -> {
dicQuery["headerType"] = transportDetails[0].ifEmpty { "none" }
if (!TextUtils.isEmpty(transportDetails[2])) {
dicQuery["seed"] = Utils.urlEncode(transportDetails[2])
}
}
"ws", "httpupgrade", "splithttp" -> {
if (!TextUtils.isEmpty(transportDetails[1])) {
dicQuery["host"] = Utils.urlEncode(transportDetails[1])
}
if (!TextUtils.isEmpty(transportDetails[2])) {
dicQuery["path"] = Utils.urlEncode(transportDetails[2])
}
}
"http", "h2" -> {
dicQuery["type"] = "http"
if (!TextUtils.isEmpty(transportDetails[1])) {
dicQuery["host"] = Utils.urlEncode(transportDetails[1])
}
if (!TextUtils.isEmpty(transportDetails[2])) {
dicQuery["path"] = Utils.urlEncode(transportDetails[2])
}
}
"quic" -> {
dicQuery["headerType"] = transportDetails[0].ifEmpty { "none" }
dicQuery["quicSecurity"] = Utils.urlEncode(transportDetails[1])
dicQuery["key"] = Utils.urlEncode(transportDetails[2])
}
"grpc" -> {
dicQuery["mode"] = transportDetails[0]
dicQuery["authority"] = Utils.urlEncode(transportDetails[1])
dicQuery["serviceName"] = Utils.urlEncode(transportDetails[2])
}
}
}
val query = "?" + dicQuery.toList().joinToString(
separator = "&",
transform = { it.first + "=" + it.second })
val url = String.format(
"%s@%s:%s",
outbound.getPassword(),
Utils.getIpv6Address(outbound.getServerAddress()),
outbound.getServerPort()
)
return url + query + remark
}
}

View File

@@ -0,0 +1,160 @@
package com.v2ray.ang.util.fmt
import android.text.TextUtils
import android.util.Log
import com.google.gson.Gson
import com.tencent.mmkv.MMKV
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.MmkvManager
import com.v2ray.ang.util.Utils
import java.net.URI
object VmessFmt {
private val settingsStorage by lazy {
MMKV.mmkvWithID(
MmkvManager.ID_SETTING, MMKV.MULTI_PROCESS_MODE
)
}
fun parseVmess(str: String): ServerConfig? {
if (str.indexOf('?') > 0 && str.indexOf('&') > 0) {
return parseVmessStd(str)
}
val allowInsecure = settingsStorage?.decodeBool(AppConfig.PREF_ALLOW_INSECURE) ?: false
val config = ServerConfig.create(EConfigType.VMESS)
val streamSetting = config.outboundBean?.streamSettings ?: return null
var result = str.replace(EConfigType.VMESS.protocolScheme, "")
result = Utils.decode(result)
if (TextUtils.isEmpty(result)) {
Log.d(AppConfig.ANG_PACKAGE, "R.string.toast_decoding_failed")
return null
}
val vmessQRCode = Gson().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)
|| TextUtils.isEmpty(vmessQRCode.id)
|| TextUtils.isEmpty(vmessQRCode.net)
) {
Log.d(AppConfig.ANG_PACKAGE, "R.string.toast_incorrect_protocol")
return null
}
config.remarks = vmessQRCode.ps
config.outboundBean.settings?.vnext?.get(0)?.let { vnext ->
vnext.address = vmessQRCode.add
vnext.port = Utils.parseInt(vmessQRCode.port)
vnext.users[0].id = vmessQRCode.id
vnext.users[0].security =
if (TextUtils.isEmpty(vmessQRCode.scy)) V2rayConfig.DEFAULT_SECURITY else vmessQRCode.scy
vnext.users[0].alterId = Utils.parseInt(vmessQRCode.aid)
}
val sni = streamSetting.populateTransportSettings(
vmessQRCode.net,
vmessQRCode.type,
vmessQRCode.host,
vmessQRCode.path,
vmessQRCode.path,
vmessQRCode.host,
vmessQRCode.path,
vmessQRCode.type,
vmessQRCode.path,
vmessQRCode.host
)
val fingerprint = vmessQRCode.fp
streamSetting.populateTlsSettings(
vmessQRCode.tls,
allowInsecure,
if (TextUtils.isEmpty(vmessQRCode.sni)) sni else vmessQRCode.sni,
fingerprint,
vmessQRCode.alpn,
null,
null,
null
)
return config
}
fun toUri(config: ServerConfig): String {
val outbound = config.getProxyOutbound() ?: return ""
val streamSetting = outbound.streamSettings ?: V2rayConfig.OutboundBean.StreamSettingsBean()
val vmessQRCode = VmessQRCode()
vmessQRCode.v = "2"
vmessQRCode.ps = config.remarks
vmessQRCode.add = outbound.getServerAddress().orEmpty()
vmessQRCode.port = outbound.getServerPort().toString()
vmessQRCode.id = outbound.getPassword().orEmpty()
vmessQRCode.aid = outbound.settings?.vnext?.get(0)?.users?.get(0)?.alterId.toString()
vmessQRCode.scy = outbound.settings?.vnext?.get(0)?.users?.get(0)?.security.toString()
vmessQRCode.net = streamSetting.network
vmessQRCode.tls = streamSetting.security
vmessQRCode.sni = streamSetting.tlsSettings?.serverName.orEmpty()
vmessQRCode.alpn =
Utils.removeWhiteSpace(streamSetting.tlsSettings?.alpn?.joinToString()).orEmpty()
vmessQRCode.fp = streamSetting.tlsSettings?.fingerprint.orEmpty()
outbound.getTransportSettingDetails()?.let { transportDetails ->
vmessQRCode.type = transportDetails[0]
vmessQRCode.host = transportDetails[1]
vmessQRCode.path = transportDetails[2]
}
val json = Gson().toJson(vmessQRCode)
return Utils.encode(json)
}
fun parseVmessStd(str: String): ServerConfig? {
var allowInsecure = settingsStorage?.decodeBool(AppConfig.PREF_ALLOW_INSECURE) ?: false
val config = ServerConfig.create(EConfigType.VMESS)
val uri = URI(Utils.fixIllegalUrl(str))
if (uri.rawQuery.isNullOrEmpty()) return null
val queryParam = uri.rawQuery.split("&")
.associate { it.split("=").let { (k, v) -> k to Utils.urlDecode(v) } }
val streamSetting = config.outboundBean?.streamSettings ?: return null
config.remarks = Utils.urlDecode(uri.fragment ?: "")
config.outboundBean.settings?.vnext?.get(0)?.let { vnext ->
vnext.address = uri.idnHost
vnext.port = uri.port
vnext.users[0].id = uri.userInfo
vnext.users[0].security = V2rayConfig.DEFAULT_SECURITY
vnext.users[0].alterId = 0
}
val sni = streamSetting.populateTransportSettings(
queryParam["type"] ?: "tcp",
queryParam["headerType"],
queryParam["host"],
queryParam["path"],
queryParam["seed"],
queryParam["quicSecurity"],
queryParam["key"],
queryParam["mode"],
queryParam["serviceName"],
queryParam["authority"]
)
allowInsecure = if ((queryParam["allowInsecure"] ?: "") == "1") true else allowInsecure
streamSetting.populateTlsSettings(
queryParam["security"] ?: "",
allowInsecure,
queryParam["sni"] ?: sni,
queryParam["fp"] ?: "",
queryParam["alpn"],
null,
null,
null
)
return config
}
}

View File

@@ -0,0 +1,73 @@
package com.v2ray.ang.util.fmt
import com.v2ray.ang.AppConfig
import com.v2ray.ang.dto.EConfigType
import com.v2ray.ang.dto.ServerConfig
import com.v2ray.ang.extension.idnHost
import com.v2ray.ang.extension.removeWhiteSpace
import com.v2ray.ang.util.Utils
import java.net.URI
object WireguardFmt {
fun parseWireguard(str: String): ServerConfig? {
val uri = URI(Utils.fixIllegalUrl(str))
if (uri.rawQuery != null) {
val config = ServerConfig.create(EConfigType.WIREGUARD)
config.remarks = Utils.urlDecode(uri.fragment ?: "")
val queryParam = uri.rawQuery.split("&")
.associate { it.split("=").let { (k, v) -> k to Utils.urlDecode(v) } }
config.outboundBean?.settings?.let { wireguard ->
wireguard.secretKey = uri.userInfo
wireguard.address =
(queryParam["address"]
?: AppConfig.WIREGUARD_LOCAL_ADDRESS_V4).removeWhiteSpace()
.split(",")
wireguard.peers?.get(0)?.publicKey = queryParam["publickey"] ?: ""
wireguard.peers?.get(0)?.endpoint =
Utils.getIpv6Address(uri.idnHost) + ":${uri.port}"
wireguard.mtu = Utils.parseInt(queryParam["mtu"] ?: AppConfig.WIREGUARD_LOCAL_MTU)
wireguard.reserved =
(queryParam["reserved"] ?: "0,0,0").removeWhiteSpace().split(",")
.map { it.toInt() }
}
return config
} else {
return null
}
}
fun toUri(config: ServerConfig): String {
val outbound = config.getProxyOutbound() ?: return ""
val remark = "#" + Utils.urlEncode(config.remarks)
val dicQuery = HashMap<String, String>()
dicQuery["publickey"] =
Utils.urlEncode(outbound.settings?.peers?.get(0)?.publicKey.toString())
if (outbound.settings?.reserved != null) {
dicQuery["reserved"] = Utils.urlEncode(
Utils.removeWhiteSpace(outbound.settings?.reserved?.joinToString())
.toString()
)
}
dicQuery["address"] = Utils.urlEncode(
Utils.removeWhiteSpace((outbound.settings?.address as List<*>).joinToString())
.toString()
)
if (outbound.settings?.mtu != null) {
dicQuery["mtu"] = outbound.settings?.mtu.toString()
}
val query = "?" + dicQuery.toList().joinToString(
separator = "&",
transform = { it.first + "=" + it.second })
val url = String.format(
"%s@%s:%s",
Utils.urlEncode(outbound.getPassword().toString()),
Utils.getIpv6Address(outbound.getServerAddress()),
outbound.getServerPort()
)
return url + query + remark
}
}

View File

@@ -6,6 +6,7 @@ import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.content.IntentFilter
import android.content.res.AssetManager
import android.os.Build
import android.util.Log
import android.view.LayoutInflater
@@ -37,6 +38,8 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancelChildren
import kotlinx.coroutines.launch
import java.io.File
import java.io.FileOutputStream
import java.util.Collections
class MainViewModel(application: Application) : AndroidViewModel(application) {
@@ -60,8 +63,8 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
}
var serverList = MmkvManager.decodeServerList()
var subscriptionId: String = settingsStorage.decodeString(AppConfig.CACHE_SUBSCRIPTION_ID, "")!!
var keywordFilter: String = settingsStorage.decodeString(AppConfig.CACHE_KEYWORD_FILTER, "")!!
var subscriptionId: String = settingsStorage.decodeString(AppConfig.CACHE_SUBSCRIPTION_ID, "")?:""
var keywordFilter: String = settingsStorage.decodeString(AppConfig.CACHE_KEYWORD_FILTER, "")?:""
private set
val serversCache = mutableListOf<ServersCache>()
val isRunning by lazy { MutableLiveData<Boolean>() }
@@ -110,7 +113,12 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
}
}
fun appendCustomConfigServer(server: String) {
fun appendCustomConfigServer(server: String): Boolean {
if (server.contains("inbounds")
&& server.contains("outbounds")
&& server.contains("routing")
) {
try {
val config = ServerConfig.create(EConfigType.CUSTOM)
config.subscriptionId = subscriptionId
config.fullConfig = Gson().fromJson(server, V2rayConfig::class.java)
@@ -119,6 +127,12 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
serverRawStorage?.encode(key, server)
serverList.add(0, key)
serversCache.add(0, ServersCache(key, config))
return true
} catch (e: Exception) {
e.printStackTrace()
}
}
return false
}
fun swapServer(fromPosition: Int, toPosition: Int) {
@@ -272,7 +286,6 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
for (it in deleteServer) {
MmkvManager.removeServer(it)
}
reloadServerList()
getApplication<AngApplication>().toast(
getApplication<AngApplication>().getString(
R.string.title_del_duplicate_config_count,
@@ -281,6 +294,32 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
)
}
fun copyAssets(assets: AssetManager) {
val extFolder = Utils.userAssetPath(getApplication<AngApplication>())
viewModelScope.launch(Dispatchers.Default) {
try {
val geo = arrayOf("geosite.dat", "geoip.dat")
assets.list("")
?.filter { geo.contains(it) }
?.filter { !File(extFolder, it).exists() }
?.forEach {
val target = File(extFolder, it)
assets.open(it).use { input ->
FileOutputStream(target).use { output ->
input.copyTo(output)
}
}
Log.i(
ANG_PACKAGE,
"Copied from apk assets folder to ${target.absolutePath}"
)
}
} catch (e: Exception) {
Log.e(ANG_PACKAGE, "asset copy failed", e)
}
}
}
private val mMsgReceiver = object : BroadcastReceiver() {
override fun onReceive(ctx: Context?, intent: Intent?) {
when (intent?.getIntExtra("key", 0)) {

View File

@@ -1,8 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_height="match_parent">
<LinearLayout xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
@@ -17,7 +20,7 @@
android:layout_height="wrap_content"
android:clickable="true"
android:focusable="true"
android:foreground="?attr/selectableItemBackground"
android:background="?attr/selectableItemBackground"
android:gravity="center|start"
android:orientation="horizontal"
android:padding="16dp">
@@ -57,7 +60,7 @@
android:layout_height="@dimen/server_height"
android:clickable="true"
android:focusable="true"
android:foreground="?attr/selectableItemBackground"
android:background="?attr/selectableItemBackground"
android:gravity="center|start"
android:orientation="horizontal"
android:padding="16dp">
@@ -81,7 +84,7 @@
android:layout_height="@dimen/server_height"
android:clickable="true"
android:focusable="true"
android:foreground="?attr/selectableItemBackground"
android:background="?attr/selectableItemBackground"
android:gravity="center|start"
android:orientation="horizontal"
android:padding="16dp">
@@ -113,7 +116,7 @@
android:layout_height="@dimen/server_height"
android:clickable="true"
android:focusable="true"
android:foreground="?attr/selectableItemBackground"
android:background="?attr/selectableItemBackground"
android:gravity="center|start"
android:orientation="horizontal"
android:padding="16dp">
@@ -137,7 +140,7 @@
android:layout_height="@dimen/server_height"
android:clickable="true"
android:focusable="true"
android:foreground="?attr/selectableItemBackground"
android:background="?attr/selectableItemBackground"
android:gravity="center|start"
android:orientation="horizontal"
android:padding="16dp">
@@ -162,7 +165,7 @@
android:layout_height="@dimen/server_height"
android:clickable="true"
android:focusable="true"
android:foreground="?attr/selectableItemBackground"
android:background="?attr/selectableItemBackground"
android:gravity="center|start"
android:orientation="horizontal"
android:padding="16dp">
@@ -186,7 +189,7 @@
android:layout_height="@dimen/server_height"
android:clickable="true"
android:focusable="true"
android:foreground="?attr/selectableItemBackground"
android:background="?attr/selectableItemBackground"
android:gravity="center|start"
android:orientation="horizontal"
android:padding="16dp">
@@ -221,6 +224,7 @@
</LinearLayout>
</LinearLayout>
</LinearLayout>
</ScrollView>

View File

@@ -173,6 +173,7 @@
android:orientation="vertical">
<TextView
android:id="@+id/tv_request_host"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/server_lab_request_host" />
@@ -192,6 +193,7 @@
android:orientation="vertical">
<TextView
android:id="@+id/tv_path"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/server_lab_path" />

View File

@@ -154,6 +154,7 @@
android:orientation="vertical">
<TextView
android:id="@+id/tv_request_host"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/server_lab_request_host" />
@@ -173,6 +174,7 @@
android:orientation="vertical">
<TextView
android:id="@+id/tv_path"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/server_lab_path" />

View File

@@ -193,6 +193,7 @@
android:orientation="vertical">
<TextView
android:id="@+id/tv_request_host"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/server_lab_request_host" />
@@ -212,6 +213,7 @@
android:orientation="vertical">
<TextView
android:id="@+id/tv_path"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/server_lab_path" />

View File

@@ -192,6 +192,7 @@
android:orientation="vertical">
<TextView
android:id="@+id/tv_request_host"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/server_lab_request_host" />
@@ -211,6 +212,7 @@
android:orientation="vertical">
<TextView
android:id="@+id/tv_path"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/server_lab_path" />

View File

@@ -3,7 +3,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:foreground="?attr/selectableItemBackground"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:focusable="true">

View File

@@ -21,7 +21,7 @@
android:layout_gravity="center"
android:clickable="true"
android:focusable="true"
android:foreground="?attr/selectableItemBackground"
android:background="?attr/selectableItemBackground"
android:gravity="center"
android:nextFocusRight="@+id/layout_share"
android:orientation="horizontal">
@@ -115,7 +115,7 @@
android:id="@+id/layout_share"
android:layout_width="wrap_content"
android:layout_height="@dimen/server_height"
android:background="?attr/selectableItemBackground"
android:background="?attr/selectableItemBackgroundBorderless"
android:clickable="true"
android:focusable="true"
android:gravity="center"
@@ -134,7 +134,7 @@
android:id="@+id/layout_edit"
android:layout_width="wrap_content"
android:layout_height="@dimen/server_height"
android:background="?attr/selectableItemBackground"
android:background="?attr/selectableItemBackgroundBorderless"
android:clickable="true"
android:focusable="true"
android:gravity="center"
@@ -152,7 +152,7 @@
android:id="@+id/layout_remove"
android:layout_width="wrap_content"
android:layout_height="@dimen/server_height"
android:background="?attr/selectableItemBackground"
android:background="?attr/selectableItemBackgroundBorderless"
android:clickable="true"
android:focusable="true"
android:gravity="center"

View File

@@ -21,7 +21,7 @@
android:layout_gravity="center"
android:clickable="true"
android:focusable="true"
android:foreground="?attr/selectableItemBackground"
android:background="?attr/selectableItemBackground"
android:gravity="center"
android:nextFocusRight="@+id/layout_edit"
android:orientation="horizontal">
@@ -67,7 +67,7 @@
android:id="@+id/layout_share"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:background="?attr/selectableItemBackgroundBorderless"
android:clickable="true"
android:focusable="true"
android:gravity="center"
@@ -85,7 +85,7 @@
android:id="@+id/layout_edit"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:background="?attr/selectableItemBackgroundBorderless"
android:clickable="true"
android:focusable="true"
android:gravity="center"

View File

@@ -14,7 +14,7 @@
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:foreground="?attr/selectableItemBackground"
android:background="?attr/selectableItemBackground"
android:gravity="center"
android:orientation="horizontal"
android:padding="@dimen/nav_header_vertical_spacing">
@@ -64,7 +64,7 @@
android:id="@+id/layout_edit"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:background="?attr/selectableItemBackgroundBorderless"
android:clickable="true"
android:focusable="true"
android:padding="@dimen/nav_header_vertical_spacing">
@@ -79,7 +79,7 @@
android:id="@+id/layout_remove"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:background="?attr/selectableItemBackgroundBorderless"
android:clickable="true"
android:focusable="true"
android:padding="@dimen/nav_header_vertical_spacing">

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_vertical"
android:orientation="horizontal"
android:padding="16dp">
<ProgressBar
android:id="@+id/pb_waiting"
style="@android:style/Widget.DeviceDefault.ProgressBar"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>

View File

@@ -7,13 +7,8 @@
android:title="@string/menu_item_add_config"
app:showAsAction="always" />
<item
android:id="@+id/del_config"
android:icon="@drawable/ic_delete_24dp"
android:title="@string/menu_item_del_config"
app:showAsAction="always" />
<item
android:id="@+id/save_config"
android:icon="@drawable/ic_action_done"
android:title="@string/menu_item_save_config"
android:id="@+id/sub_update"
android:icon="@drawable/ic_restore_24dp"
android:title="@string/title_sub_update"
app:showAsAction="always" />
</menu>

View File

@@ -48,8 +48,22 @@
<string name="server_lab_more_function">النقل</string>
<string name="server_lab_head_type">نوع الرأس</string>
<string name="server_lab_mode_type">وضع gRPC</string>
<string name="server_lab_request_host">مضيف الطلب (مضيف/مضيف ws/مضيف httpupgrade/مضيف h2)/أمان QUIC/جهة gRPC</string>
<string name="server_lab_path">المسار (مسار ws/مسار httpupgrade/مسار h2)/مفتاح QUIC/بذرة kcp/خدمة gRPC</string>
<string name="server_lab_request_host">host</string>
<string name="server_lab_request_host_http">http host</string>
<string name="server_lab_request_host_ws">ws host</string>
<string name="server_lab_request_host_httpupgrade">httpupgrade host</string>
<string name="server_lab_request_host_splithttp">splithttp host</string>
<string name="server_lab_request_host_h2">h2 host</string>
<string name="server_lab_request_host_quic">QUIC security</string>
<string name="server_lab_request_host_grpc">gRPC Authority</string>
<string name="server_lab_path">path</string>
<string name="server_lab_path_ws">ws path</string>
<string name="server_lab_path_httpupgrade">httpupgrade path</string>
<string name="server_lab_path_splithttp">splithttp path</string>
<string name="server_lab_path_h2">h2 path</string>
<string name="server_lab_path_quic">QUIC key</string>
<string name="server_lab_path_kcp">kcp seed</string>
<string name="server_lab_path_grpc">gRPC serviceName</string>
<string name="server_lab_stream_security">TLS</string>
<string name="server_lab_stream_fingerprint" translatable="false">البصمة</string>
<string name="server_lab_stream_alpn" translatable="false">بروتوكول الطبقة الآخرى التالية (ALPN)</string>

View File

@@ -46,8 +46,22 @@
<string name="server_lab_more_function">انتقال</string>
<string name="server_lab_head_type">نوع head</string>
<string name="server_lab_mode_type">حالت gRPC</string>
<string name="server_lab_request_host">gRPC Authority/میزبان درخواست (host/host ws/host h2)/امنیت QUIC</string>
<string name="server_lab_path">مسیر (مسیر ws/ مسیر h2) کلید QUIC/دانه kcp/نام‌خدمات gRPC</string>
<string name="server_lab_request_host">host</string>
<string name="server_lab_request_host_http">http host</string>
<string name="server_lab_request_host_ws">ws host</string>
<string name="server_lab_request_host_httpupgrade">httpupgrade host</string>
<string name="server_lab_request_host_splithttp">splithttp host</string>
<string name="server_lab_request_host_h2">h2 host</string>
<string name="server_lab_request_host_quic">QUIC security</string>
<string name="server_lab_request_host_grpc">gRPC Authority</string>
<string name="server_lab_path">path</string>
<string name="server_lab_path_ws">ws path</string>
<string name="server_lab_path_httpupgrade">httpupgrade path</string>
<string name="server_lab_path_splithttp">splithttp path</string>
<string name="server_lab_path_h2">h2 path</string>
<string name="server_lab_path_quic">QUIC key</string>
<string name="server_lab_path_kcp">kcp seed</string>
<string name="server_lab_path_grpc">gRPC serviceName</string>
<string name="server_lab_stream_security">TLS</string>
<string name="server_lab_allow_insecure">مجوز ناامن</string>
<string name="server_lab_sni">SNI</string>

View File

@@ -46,8 +46,22 @@
<string name="server_lab_more_function">Другие параметры</string>
<string name="server_lab_head_type">Тип заголовка</string>
<string name="server_lab_mode_type">Режим gRPC</string>
<string name="server_lab_request_host">Запрос узла (WS/H2)/HTTPUpgrade/Шифрование QUIC/Полномочия gRPC</string>
<string name="server_lab_path">Путь (WS/H2)/HTTPUpgrade/Ключ QUIC/Сид KCP/Сервис gRPC</string>
<string name="server_lab_request_host">Узел</string>
<string name="server_lab_request_host_http">Узел HTTP</string>
<string name="server_lab_request_host_ws">Узел WS</string>
<string name="server_lab_request_host_httpupgrade">Узел HTTPUpgrade</string>
<string name="server_lab_request_host_splithttp">Узел SplitHTTP</string>
<string name="server_lab_request_host_h2">Узел H2</string>
<string name="server_lab_request_host_quic">Шифрование QUIC</string>
<string name="server_lab_request_host_grpc">Полномочия gRPC</string>
<string name="server_lab_path">Путь</string>
<string name="server_lab_path_ws">Путь WS</string>
<string name="server_lab_path_httpupgrade">Путь HTTPUpgrade</string>
<string name="server_lab_path_splithttp">Путь SplitHTTP</string>
<string name="server_lab_path_h2">Путь H2</string>
<string name="server_lab_path_quic">Ключ QUIC</string>
<string name="server_lab_path_kcp">Сид KCP</string>
<string name="server_lab_path_grpc">Служба gRPC</string>
<string name="server_lab_stream_security">TLS</string>
<string name="server_lab_allow_insecure">Разрешать небезопасные</string>
<string name="server_lab_sni">SNI</string>
@@ -87,6 +101,7 @@
<string name="msg_remark_is_duplicate">Описание уже существует</string>
<string name="toast_action_not_allowed">Это действие запрещено</string>
<!-- PerAppProxyActivity -->
<string name="msg_dialog_progress">Загрузка…</string>
<string name="menu_item_search">Поиск</string>
@@ -126,7 +141,6 @@
<string name="title_pref_route_only_enabled">Домен только для маршрутизации</string>
<string name="summary_pref_route_only_enabled">Использовать определённое доменное имя только для маршрутизации и сохранять целевой адрес в виде IP.</string>
<string name="title_pref_local_dns_enabled">Использовать локальную DNS</string>
<string name="summary_pref_local_dns_enabled">Обслуживание выполняется DNS-модулем ядра (в настройках маршрутизации рекомендуется выбрать режим «Все, кроме LAN и Китая»)</string>
@@ -149,8 +163,8 @@
<string name="title_pref_domestic_dns">Внутренняя DNS (необязательно)</string>
<string name="summary_pref_domestic_dns">DNS</string>
<string name="title_pref_delay_test_url">True delay test url (http/https)</string>
<string name="summary_pref_delay_test_url">Url</string>
<string name="title_pref_delay_test_url">Сервис проверки времени отклика (HTTP/HTTPS)</string>
<string name="summary_pref_delay_test_url">URL</string>
<string name="title_pref_proxy_sharing_enabled">Разрешать подключения из LAN</string>
<string name="summary_pref_proxy_sharing_enabled">Другие устройства могут подключаться, используя ваш IP-адрес, чтобы использовать прокси по протоколам SOCKS/HTTP. Используйте только в доверенной сети, чтобы избежать несанкционированного подключения.</string>
@@ -178,7 +192,7 @@
<string name="summary_pref_feedback">Предложить улучшение или сообщить об ошибке на GitHub</string>
<string name="summary_pref_tg_group">Присоединиться к группе в Telegram</string>
<string name="toast_tg_app_not_found">Приложение Telegram не найдено</string>
<string name="title_privacy_policy">Конфиденциальность</string>
<string name="title_privacy_policy">Политика конфиденциальности</string>
<string name="title_about">О приложении</string>
<string name="title_source_code">Исходный код</string>
<string name="title_tg_channel">Telegram-канал</string>

View File

@@ -11,11 +11,11 @@
<!-- Notifications -->
<string name="notification_action_stop_v2ray">Ngắt kết nối v2rayNG</string>
<string name="toast_permission_denied">Vui lòng cấp quyền cần thiết cho v2rayNG! Bạn đã từ chối các quyền cần thiết như Camera hay Bộ nhớ?</string>
<string name="notification_action_more">Nhấn để biết thêm</string>
<string name="notification_action_more">Nhấn để biết thêm...</string>
<string name="toast_services_start">Đang khởi động v2rayNG...</string>
<string name="toast_services_stop">Đã dừng v2rayNG!</string>
<string name="toast_services_success">Đã khởi động v2rayNG!</string>
<string name="toast_services_failure">Không thể khởi động v2rayNG, kiểm tra lại cấu hình.</string>
<string name="toast_services_failure">Không thể khởi động v2rayNG, kiểm tra lại cấu hình!</string>
<!--ServerActivity-->
<string name="title_server">v2rayNG</string>
@@ -24,12 +24,12 @@
<string name="menu_item_del_config">Xoá cấu hình</string>
<string name="menu_item_import_config_qrcode">Nhập cấu hình từ mã QR</string>
<string name="menu_item_import_config_clipboard">Nhập cấu hình từ Clipboard</string>
<string name="menu_item_import_config_manually_vmess">Nhập thủ công [Vmess]</string>
<string name="menu_item_import_config_manually_vmess">Nhập thủ công [VMESS]</string>
<string name="menu_item_import_config_manually_vless">Nhập thủ công [VLESS]</string>
<string name="menu_item_import_config_manually_ss">Nhập thủ công [Shadowsocks]</string>
<string name="menu_item_import_config_manually_ss">Nhập thủ công [ShadowSocks]</string>
<string name="menu_item_import_config_manually_socks">Nhập thủ công [Socks]</string>
<string name="menu_item_import_config_manually_trojan">Nhập thủ công [Trojan]</string>
<string name="menu_item_import_config_manually_wireguard">Nhập thủ công [Wireguard]</string>
<string name="menu_item_import_config_manually_wireguard">Nhập thủ công [WireGuard]</string>
<string name="menu_item_import_config_custom">Nâng cao / Cấu hình tùy chỉnh</string>
<string name="menu_item_import_config_custom_clipboard">Nhập cấu hình tùy chỉnh từ Clipboard</string>
<string name="menu_item_import_config_custom_local">Nhập cấu hình tùy chỉnh từ Tệp</string>
@@ -40,33 +40,47 @@
<string name="server_lab_address">Địa chỉ</string>
<string name="server_lab_port">Cổng</string>
<string name="server_lab_id">ID</string>
<string name="server_lab_alterid">alterId</string>
<string name="server_lab_alterid">ID bổ sung (AlterID)</string>
<string name="server_lab_security">Thuật toán mã hóa</string>
<string name="server_lab_network">Giao thức truyền tải (network)</string>
<string name="server_lab_network">Giao thức truyền tải (Network)</string>
<string name="server_lab_more_function">Nâng cao</string>
<string name="server_lab_head_type">Kiểu ngụy trang (type)</string>
<string name="server_lab_head_type">Kiểu ngụy trang (Type)</string>
<string name="server_lab_mode_type">Chế độ gRPC</string>
<string name="server_lab_request_host">Yêu cầu host(host/ws host/h2 host)/QUIC security/gRPC Authority</string>
<string name="server_lab_path">path(ws path/h2 path)/QUIC key/kcp seed/gRPC serviceName</string>
<string name="server_lab_request_host">host</string>
<string name="server_lab_request_host_http">http host</string>
<string name="server_lab_request_host_ws">ws host</string>
<string name="server_lab_request_host_httpupgrade">httpupgrade host</string>
<string name="server_lab_request_host_splithttp">splithttp host</string>
<string name="server_lab_request_host_h2">h2 host</string>
<string name="server_lab_request_host_quic">QUIC security</string>
<string name="server_lab_request_host_grpc">gRPC Authority</string>
<string name="server_lab_path">path</string>
<string name="server_lab_path_ws">ws path</string>
<string name="server_lab_path_httpupgrade">httpupgrade path</string>
<string name="server_lab_path_splithttp">splithttp path</string>
<string name="server_lab_path_h2">h2 path</string>
<string name="server_lab_path_quic">QUIC key</string>
<string name="server_lab_path_kcp">kcp seed</string>
<string name="server_lab_path_grpc">gRPC serviceName</string>
<string name="server_lab_stream_security">TLS</string>
<string name="server_lab_allow_insecure">allowInsecure</string>
<string name="server_lab_allow_insecure">Bỏ qua xác minh chứng chỉ</string>
<string name="server_lab_sni">SNI</string>
<string name="server_lab_address3">Địa chỉ</string>
<string name="server_lab_port3">Cổng</string>
<string name="server_lab_id3">Mật khẩu</string>
<string name="server_lab_security3">Thuật toán mã hóa</string>
<string name="server_lab_id4">Mật khẩu (không bắt buộc)</string>
<string name="server_lab_security4">Tên người dùng (không bắt buộc)</string>
<string name="server_lab_id4">Mật khẩu (Không bắt buộc)</string>
<string name="server_lab_security4">Tên người dùng (Không bắt buộc)</string>
<string name="server_lab_encryption">Mã hóa</string>
<string name="server_lab_flow">Kiểm soát lưu lượng (flow)</string>
<string name="server_lab_reserved">Reserved (không bắt buộc)</string>
<string name="server_lab_local_address">Địa chỉ cục bộ (IPv4/IPv6, phân cách bằng dấu phẩy)</string>
<string name="server_lab_local_mtu">MTU (không bắt buộc, mặc định 1420)</string>
<string name="server_lab_flow">Kiểm soát lưu lượng (Flow)</string>
<string name="server_lab_reserved">Reserved (Không bắt buộc)</string>
<string name="server_lab_local_address">Địa chỉ cục bộ (IPv4 / IPv6, phân cách bằng dấu phẩy)</string>
<string name="server_lab_local_mtu">MTU (Không bắt buộc, mặc định 1420)</string>
<string name="toast_success">Thành công!</string>
<string name="toast_failure">Đã xảy ra lỗi, vui lòng thử lại!</string>
<string name="toast_none_data">Không có gì ở đây</string>
<string name="toast_incorrect_protocol">Không đúng Protocol</string>
<string name="toast_decoding_failed">Không thể giải mã</string>
<string name="toast_none_data">Không có gì ở đây!</string>
<string name="toast_incorrect_protocol">Protocol không đúng!</string>
<string name="toast_decoding_failed">Không thể giải mã!</string>
<string name="title_file_chooser">Vui lòng chọn tệp cấu hình!</string>
<string name="toast_require_file_manager">Vui lòng cài đặt trình quản lý tệp để tiếp tục!</string>
<string name="server_customize_config">Cấu hình tùy chỉnh</string>
@@ -76,16 +90,16 @@
<string name="toast_invalid_url">URL không hợp lệ hoặc trống!</string>
<string name="server_lab_need_inbound">Vui lòng đảm bảo cấu hình tùy chỉnh này không bị lỗi trước khi sử dụng!</string>
<string name="toast_malformed_josn">Cấu hình không hợp lệ!</string>
<string name="server_lab_request_host6">Host (SNI) (không bắt buộc)</string>
<string name="server_lab_request_host6">Host (SNI) (Không bắt buộc)</string>
<string name="toast_asset_copy_failed">Không thể sao chép tệp tin, hãy dùng trình quản lý tệp!</string>
<string name="menu_item_add_file">Thêm tệp</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="toast_action_not_allowed">Hành động này bị cấm!</string>
<!-- PerAppProxyActivity -->
<string name="title_user_asset_add_url">Thêm URL nội dung</string>
<string name="msg_file_not_found">Không tìm thấy tập tin</string>
<string name="msg_remark_is_duplicate">Nhận xét đã tồn tại</string>
<string name="msg_file_not_found">Không tìm thấy tập tin!</string>
<string name="msg_remark_is_duplicate">Nhận xét đã tồn tại!</string>
<string name="msg_dialog_progress">Đang tải...</string>
<string name="menu_item_search">Tìm kiếm</string>
<string name="menu_item_select_all">Chọn tất cả</string>
@@ -93,7 +107,7 @@
<string name="switch_bypass_apps_mode">Chế độ Bypass</string>
<string name="menu_item_select_proxy_app">Tự động chọn ứng dụng Proxy</string>
<string name="msg_downloading_content">Đang tải xuống nội dung...</string>
<string name="menu_item_export_proxy_app">Xuất và sao chép</string>
<string name="menu_item_export_proxy_app">Xuất và Sao chép</string>
<string name="menu_item_import_proxy_app">Nhập từ Clipboard</string>
@@ -101,15 +115,15 @@
<string name="title_settings">Cài đặt</string>
<string name="title_advanced">Cài đặt nâng cao</string>
<string name="title_vpn_settings">Cài đặt VPN</string>
<string name="title_pref_per_app_proxy">Proxy Theo Ứng Dụng</string>
<string name="title_pref_per_app_proxy">Proxy theo Ứng dụng</string>
<string name="summary_pref_per_app_proxy">- Bình thường: Ứng dụng đã chọn sẽ kết nối thông qua Proxy, chưa chọn sẽ kết nối trực tiếp. \n- Chế độ Bypass: Ứng dụng đã chọn sẽ kết nối trực tiếp, chưa chọn sẽ kết nối qua Proxy. \n- Nếu bạn đang ở Trung Quốc thì vào Menu, chọn Tự động chọn ứng dụng Proxy.</string>
<string name="title_mux_settings">Cài đặt Mux</string>
<string name="title_pref_mux_enabled">Bật Mux</string>
<string name="summary_pref_mux_enabled">Giảm độ trễ trong bước bắt tay của kết nối TCP. Mux phân phối dữ liệu từ nhiều kết nối TCP trên một kết nối TCP duy nhất. Không nên sử dụng Mux để xem video, download file hoặc chạy speedtest vì thường không hiệu quả.</string>
<string name="title_pref_mux_concurency">TCP connections (từ 1 đến 1024)</string>
<string name="title_pref_mux_xudp_concurency">XUDP connections (từ 1 đến 1024)</string>
<string name="title_pref_mux_xudp_quic">Handling of QUIC traffic in mux tunnel.</string>
<string name="summary_pref_mux_enabled">Giảm độ trễ nhưng có thể làm gián đoạn luồng, vì vậy không nên kích hoạt nó. \nCác phương pháp xử lý lưu lượng truy cập TCP, UDP và QUIC có sẵn bên dưới.</string>
<string name="title_pref_mux_concurency">Số lượng liên kết con tái sử dụng TCP (-1 đến 1024)</string>
<string name="title_pref_mux_xudp_concurency">Số lượng liên kết con tái sử dụng XUDP (-1 đến 1024)</string>
<string name="title_pref_mux_xudp_quic">Phương pháp xử lý lưu lượng QUIC</string>
<string-array name="mux_xudp_quic_entries">
<item>Từ chối</item>
<item>Cho phép</item>
@@ -117,46 +131,46 @@
</string-array>
<string name="title_pref_speed_enabled">Bật Hiển thị tốc độ mạng</string>
<string name="title_pref_speed_enabled">Hiển thị tốc độ mạng</string>
<string name="summary_pref_speed_enabled">Hiển thị tốc độ mạng hiện tại trên thanh thông báo. \nBiểu tượng trên thanh trạng thái có thể thay đổi tùy vào mức sử dụng.</string>
<string name="title_pref_sniffing_enabled">Bật Sniffing</string>
<string name="summary_pref_sniffing_enabled">Nhận diện tên miền từ gói tin để phục vụ định tuyến. \n(phải tắt để xài Zalo)</string>
<string name="title_pref_route_only_enabled">Enable routeOnly</string>
<string name="summary_pref_route_only_enabled">Use the sniffed domain name for routing only, and keep the target address as the IP address.</string>
<string name="summary_pref_sniffing_enabled">Phát hiện tên miền từ lưu lượng truy cập (Được bật theo mặc định). \nỞ Việt Nam: tắt để sử dụng Zalo.</string>
<string name="title_pref_route_only_enabled">Bật RouteOnly</string>
<string name="summary_pref_route_only_enabled">Chỉ sử dụng tên miền được đánh dấu để định tuyến và giữ địa chỉ đích làm địa chỉ IP.</string>
<string name="title_pref_local_dns_enabled">Bật Local DNS</string>
<string name="summary_pref_local_dns_enabled">DNS được xử lý bởi mô-đun DNS của xray-core (dùng nếu cần định tuyến bypass cho mạng LAN và địa chỉ nội địa)</string>
<string name="summary_pref_local_dns_enabled">DNS được xử lý bởi module DNS của Xray-Core (Dùng nếu cần định tuyến Bypass cho mạng LAN và Địa chỉ nội địa).</string>
<string name="title_pref_fake_dns_enabled">Bật FakeDNS</string>
<string name="summary_pref_fake_dns_enabled">FakeDNS lấy tên miền mục tiêu bằng cách giả mạo DNS (nhanh hơn, nhưng có thể không hoạt động cho một số ứng dụng)</string>
<string name="summary_pref_fake_dns_enabled">FakeDNS lấy tên miền mục tiêu bằng cách giả mạo DNS (Nhanh hơn, nhưng có thể không hoạt động cho một số ứng dụng).</string>
<string name="title_pref_prefer_ipv6">Ưu tiên IPv6</string>
<string name="summary_pref_prefer_ipv6">Ưu tiên sử dụng địa chỉ IPv6 cho kết nối và định tuyến.</string>
<string name="title_pref_routing">Định tuyến</string>
<string name="title_pref_routing_domain_strategy">Chiến lược tên miền (domainStrategy)</string>
<string name="title_pref_routing_domain_strategy">Chiến lược tên miền (DomainStrategy)</string>
<string name="title_pref_routing_mode">Quy tắc được định nghĩa trước</string>
<string name="title_pref_routing_custom">Quy tắc tùy chỉnh</string>
<string name="title_pref_remote_dns">DNS ngoại quốc (không bắt buộc)</string>
<string name="title_pref_remote_dns">DNS ngoại quốc (UDP / TCP / HTTPS / QUIC) (Không bắt buộc)</string>
<string name="summary_pref_remote_dns">DNS</string>
<string name="title_pref_vpn_dns">VPN DNS (Chỉ IPv4/IPv6)</string>
<string name="title_pref_vpn_dns">VPN DNS (Chỉ IPv4 / IPv6)</string>
<string name="title_pref_domestic_dns">DNS nội địa (không bắt buộc)</string>
<string name="title_pref_domestic_dns">DNS nội địa (Không bắt buộc)</string>
<string name="summary_pref_domestic_dns">DNS</string>
<string name="title_pref_delay_test_url">True delay test url (http/https)</string>
<string name="summary_pref_delay_test_url">Url</string>
<string name="title_pref_delay_test_url">URL kiểm tra độ trễ thực (HTTP / HTTPS)</string>
<string name="summary_pref_delay_test_url">URL</string>
<string name="title_pref_proxy_sharing_enabled">Cho phép kết nối từ mạng LAN</string>
<string name="summary_pref_proxy_sharing_enabled">Các thiết bị khác trong cùng mạng LAN có thể kết nối đến SOCKS/HTTP proxy trên thiết bị của bạn. \nChỉ bật tính năng này trong các mạng đáng tin cậy để tránh kết nối trái phép.</string>
<string name="summary_pref_proxy_sharing_enabled">Các thiết bị khác trong cùng mạng LAN có thể kết nối đến SOCKS / HTTP proxy trên thiết bị của bạn. \nChỉ bật tính năng này trong các mạng đáng tin cậy để tránh kết nối trái phép.</string>
<string name="toast_warning_pref_proxysharing_short">Đang bật cho phép kết nối từ mạng LAN</string>
<string name="title_pref_allow_insecure">allowInsecure</string>
<string name="summary_pref_allow_insecure">Khi nhập những cấu hình có bảo mật TLS, mặc định sẽ không xác minh chứng chỉ (allowInsecure: true).</string>
<string name="title_pref_allow_insecure">Bỏ qua xác minh chứng chỉ</string>
<string name="summary_pref_allow_insecure">Khi nhập những cấu hình có bảo mật TLS, mặc định sẽ không xác minh chứng chỉ.</string>
<string name="title_pref_socks_port">Cổng Proxy SOCKS5</string>
<string name="summary_pref_socks_port">Cổng Proxy SOCKS5</string>
@@ -176,29 +190,29 @@
<string name="title_pref_feedback">Phản hồi lỗi</string>
<string name="summary_pref_feedback">Phản hồi cải tiến hoặc lỗi lên GitHub</string>
<string name="summary_pref_tg_group">Tham gia nhóm Telegram</string>
<string name="toast_tg_app_not_found">Không tìm thấy ứng dụng Telegram</string>
<string name="toast_tg_app_not_found">Không tìm thấy ứng dụng Telegram!</string>
<string name="title_privacy_policy">Chính sách bảo mật</string>
<string name="title_about">About</string>
<string name="title_source_code">Source code</string>
<string name="title_tg_channel">Telegram channel</string>
<string name="title_configuration_backup">Backup configuration</string>
<string name="summary_configuration_backup">Storage location: [%s], The backup will be cleared after uninstalling the app or clearing the storage</string>
<string name="title_configuration_restore">Restore configuration</string>
<string name="title_configuration_share">Share configuration</string>
<string name="title_about">Giới thiệu</string>
<string name="title_source_code">Mã nguồn</string>
<string name="title_tg_channel">Kênh Telegram</string>
<string name="title_configuration_backup">Sao lưu cấu hình</string>
<string name="summary_configuration_backup">Nơi lưu trữ: [%s], bản backup sẽ được dọn dẹp sau khi xóa ứng dụng hoặc xóa bộ nhớ.</string>
<string name="title_configuration_restore">Khôi phục cấu hình</string>
<string name="title_configuration_share">Chia sẻ cấu hình</string>
<string name="title_pref_promotion">Quảng bá server</string>
<string name="summary_pref_promotion">Quảng cáo, nhấn để biết thêm (Ủng hộ có thể được gỡ bỏ)</string>
<string name="title_pref_auto_update_subscription">Tự động cập nhật các gói đăng ký</string>
<string name="summary_pref_auto_update_subscription">Tự động cập nhật các gói đăng ký của bạn ở trong nền với khoảng thời gian cố định. Tùy thiết bị, tính năng này có thể không luôn hoạt động đúng như mong đợi.</string>
<string name="title_pref_auto_update_interval">Thời gian Cập nhật tự động (Phút, Giá trị tối thiểu 15)</string>
<string name="title_pref_auto_update_interval">Thời gian cập nhật tự động (Phút, giá trị tối thiểu 15)</string>
<string name="title_core_loglevel">Log level</string>
<string name="title_core_loglevel">Cấp độ nhật ký</string>
<string name="title_mode">Chế độ kết nối</string>
<string name="title_mode_help">Nhấn vào đây nếu bạn cần trợ giúp!</string>
<string name="title_language">Ngôn ngữ</string>
<string name="title_ui_settings">Cài đặt giao diện</string>
<string name="title_pref_ui_mode_night">UI mode settings</string>
<string name="title_ui_settings">Cài đặt UI</string>
<string name="title_pref_ui_mode_night">Cài đặt chế độ UI</string>
<string name="title_logcat">Logcat</string>
<string name="logcat_copy">Sao chép</string>
@@ -207,7 +221,7 @@
<string name="title_del_all_config">Xoá tất cả cấu hình</string>
<string name="title_del_duplicate_config">Xoá cấu hình trùng lặp</string>
<string name="title_del_invalid_config">Xoá cấu hình lỗi</string>
<string name="title_export_all">Xuất và sao chép tất cả cấu hình</string>
<string name="title_export_all">Xuất và Sao chép tất cả cấu hình</string>
<string name="title_sub_setting">Các gói đăng ký</string>
<string name="sub_setting_remarks">Tên gói đăng ký</string>
<string name="sub_setting_url">URL gói đăng ký</string>
@@ -215,8 +229,8 @@
<string name="sub_auto_update">Bật tự động cập nhật</string>
<string name="title_sub_update">Cập nhật các gói đăng ký</string>
<string name="title_ping_all_server">Ping tất cả máy chủ</string>
<string name="title_real_ping_all_server">Test HTTP tất cả máy chủ</string>
<string name="title_user_asset_setting">Tệp Geo asset</string>
<string name="title_real_ping_all_server">Kiểm tra HTTP tất cả máy chủ</string>
<string name="title_user_asset_setting">Tệp Geo Asset</string>
<string name="title_sort_by_test_results">Sắp xếp lại theo lần kiểm tra cuối cùng</string>
<string name="title_filter_config">Lọc cấu hình theo các gói đăng ký</string>
<string name="filter_config_all">Hiển thị tất cả các gói đăng ký</string>
@@ -226,20 +240,20 @@
<string name="tasker_setting_confirm">Xác nhận</string>
<string name="routing_settings_title">Cài đặt định tuyến</string>
<string name="routing_settings_tips">Phân cách bằng dấu phẩy (,). Có thể tải xuống rules mặc định để tham khảo ở menu ba chấm</string>
<string name="routing_settings_tips">Phân cách bằng dấu phẩy (,). Có thể tải xuống Rules mặc định để tham khảo ở menu ba chấm.</string>
<string name="routing_settings_save">Lưu lại</string>
<string name="routing_settings_delete">Xoá</string>
<string name="routing_settings_scan_replace">Quét QR và thay thế</string>
<string name="routing_settings_scan_append">Quét QR và nối thêm</string>
<string name="routing_settings_default_rules">Tải xuống rules mặc định cho China</string>
<string name="routing_settings_scan_replace">Quét QR và Thay thế</string>
<string name="routing_settings_scan_append">Quét QR và Nối thêm</string>
<string name="routing_settings_default_rules">Tải xuống Rules mặc định cho Trung Quốc</string>
<string name="connection_test_pending">Kiểm tra kết nối</string>
<string name="connection_test_testing">Đang kiểm tra kết nối mạng...</string>
<string name="connection_test_available">Test thành công: Mất %d ms để truy cập Google.com</string>
<string name="connection_test_available">Kiểm tra thành công: thời gian truy cập Google là %d ms</string>
<string name="connection_test_error">Lỗi kết nối mạng, hãy thử đổi cấu hình hoặc kiểm tra lại! Mã lỗi: %s</string>
<string name="connection_test_fail">Không có kết nối mạng!</string>
<string name="connection_test_error_status_code">Mã lỗi: #%d</string>
<string name="connection_connected">Đã kết nối, nhấn vào đây để kiểm tra kết nối mạng!</string>
<string name="connection_connected">Đã kết nối, nhấn để kiểm tra kết nối mạng!</string>
<string name="connection_not_connected">Chưa kết nối</string>
<string name="import_subscription_success">Nhập gói đăng ký thành công!</string>
@@ -247,13 +261,13 @@
<string-array name="share_method">
<item>Xuất ra mã QR (Chụp màn hình để lưu)</item>
<item>Sao chép vào bảng nhớ tạm</item>
<item>Sao chép vào Clipboard</item>
<item>Sao chép thành cấu hình tùy chỉnh</item>
</string-array>
<string-array name="share_sub_method">
<item>Xuất gói ra mã QR (Chụp màn hình để lưu)</item>
<item>Xuất gói vào bảng nhớ tạm</item>
<item>Xuất gói vào Clipboard</item>
</string-array>
<string-array name="routing_tag">
@@ -278,9 +292,9 @@
<string name="menu_item_add_url">Thêm liên kết</string>
<string-array name="ui_mode_night">
<item>Follow system</item>
<item>Light</item>
<item>Dark</item>
<item>Theo hệ thống</item>
<item>Sáng</item>
<item>Tối</item>
</string-array>
</resources>

View File

@@ -46,8 +46,22 @@
<string name="server_lab_more_function">底层传输方式(transport)</string>
<string name="server_lab_head_type">伪装类型(type)</string>
<string name="server_lab_mode_type">gRPC 传输模式(mode)</string>
<string name="server_lab_request_host">伪装域名(host)(host/ws host/httpupgrade host/h2 host)/QUIC 加密方式/gRPC Authority</string>
<string name="server_lab_path">path(ws path/httpupgrade path/h2 path)/QUIC 加密密钥/kcp seed/gRPC serviceName</string>
<string name="server_lab_request_host">伪装域名(host)</string>
<string name="server_lab_request_host_http">http host</string>
<string name="server_lab_request_host_ws">ws host</string>
<string name="server_lab_request_host_httpupgrade">httpupgrade host</string>
<string name="server_lab_request_host_splithttp">splithttp host</string>
<string name="server_lab_request_host_h2">h2 host</string>
<string name="server_lab_request_host_quic">QUIC 加密方式</string>
<string name="server_lab_request_host_grpc">gRPC Authority</string>
<string name="server_lab_path">path</string>
<string name="server_lab_path_ws">ws path</string>
<string name="server_lab_path_httpupgrade">httpupgrade path</string>
<string name="server_lab_path_splithttp">splithttp path</string>
<string name="server_lab_path_h2">h2 path</string>
<string name="server_lab_path_quic">QUIC 加密密钥</string>
<string name="server_lab_path_kcp">kcp seed</string>
<string name="server_lab_path_grpc">gRPC serviceName</string>
<string name="server_lab_stream_security">传输层安全(TLS)</string>
<string name="server_lab_allow_insecure">跳过证书验证(allowInsecure)</string>
<string name="server_lab_sni">SNI</string>

View File

@@ -46,8 +46,22 @@
<string name="server_lab_more_function">底層傳輸方式 (transport)</string>
<string name="server_lab_head_type">標頭類型</string>
<string name="server_lab_mode_type">gRPC 傳輸模式 (mode)</string>
<string name="server_lab_request_host">要求主機 (host)(host/ws host/httpupgrade host/h2 host)/QUIC 加密方式/gRPC Authority</string>
<string name="server_lab_path">path(ws path/httpupgrade path/h2 path)/QUIC 加密金鑰/kcp seed/gRPC serviceName</string>
<string name="server_lab_request_host">要求主機 (host)</string>
<string name="server_lab_request_host_http">http host</string>
<string name="server_lab_request_host_ws">ws host</string>
<string name="server_lab_request_host_httpupgrade">httpupgrade host</string>
<string name="server_lab_request_host_splithttp">splithttp host</string>
<string name="server_lab_request_host_h2">h2 host</string>
<string name="server_lab_request_host_quic">QUIC 加密方式</string>
<string name="server_lab_request_host_grpc">gRPC Authority</string>
<string name="server_lab_path">path</string>
<string name="server_lab_path_ws">ws path</string>
<string name="server_lab_path_httpupgrade">httpupgrade path</string>
<string name="server_lab_path_splithttp">splithttp path</string>
<string name="server_lab_path_h2">h2 path</string>
<string name="server_lab_path_quic">QUIC 加密金鑰</string>
<string name="server_lab_path_kcp">kcp seed</string>
<string name="server_lab_path_grpc">gRPC serviceName</string>
<string name="server_lab_stream_security">傳輸層安全 (TLS)</string>
<string name="server_lab_allow_insecure">跳過憑證驗證 (allowInsecure)</string>
<string name="server_lab_sni">SNI</string>

View File

@@ -26,6 +26,7 @@
<item>kcp</item>
<item>ws</item>
<item>httpupgrade</item>
<item>splithttp</item>
<item>h2</item>
<item>quic</item>
<item>grpc</item>

View File

@@ -47,8 +47,22 @@
<string name="server_lab_more_function">Transport</string>
<string name="server_lab_head_type">head type</string>
<string name="server_lab_mode_type">gRPC mode</string>
<string name="server_lab_request_host">request host(host/ws host/httpupgrade host/h2 host)/QUIC security/gRPC Authority</string>
<string name="server_lab_path">path(ws path/httpupgrade path/h2 path)/QUIC key/kcp seed/gRPC serviceName</string>
<string name="server_lab_request_host">host</string>
<string name="server_lab_request_host_http">http host</string>
<string name="server_lab_request_host_ws">ws host</string>
<string name="server_lab_request_host_httpupgrade">httpupgrade host</string>
<string name="server_lab_request_host_splithttp">splithttp host</string>
<string name="server_lab_request_host_h2">h2 host</string>
<string name="server_lab_request_host_quic">QUIC security</string>
<string name="server_lab_request_host_grpc">gRPC Authority</string>
<string name="server_lab_path">path</string>
<string name="server_lab_path_ws">ws path</string>
<string name="server_lab_path_httpupgrade">httpupgrade path</string>
<string name="server_lab_path_splithttp">splithttp path</string>
<string name="server_lab_path_h2">h2 path</string>
<string name="server_lab_path_quic">QUIC key</string>
<string name="server_lab_path_kcp">kcp seed</string>
<string name="server_lab_path_grpc">gRPC serviceName</string>
<string name="server_lab_stream_security">TLS</string>
<string name="server_lab_stream_fingerprint" translatable="false">Fingerprint</string>
<string name="server_lab_stream_alpn" translatable="false">Alpn</string>

View File

@@ -1,6 +1,6 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
id("com.android.application") version "8.2.2" apply false
id("com.android.library") version "8.2.2" apply false
id("com.android.application") version "8.4.2" apply false
id("com.android.library") version "8.4.2" apply false
id("org.jetbrains.kotlin.android") version "1.9.23" apply false
}

View File

@@ -0,0 +1,63 @@
[versions]
activityKtx = "1.9.1"
appcompat = "1.7.0"
cardview = "1.0.0"
constraintlayout = "2.1.4"
core = "3.5.3"
editorkit = "2.9.0"
flexbox = "3.0.0"
fragmentKtx = "1.8.2"
gson = "2.11.0"
junit = "4.13.2"
kotlinReflect = "2.0.0"
kotlinxCoroutinesCore = "1.8.1"
legacySupportV4 = "1.0.0"
lifecycleViewmodelKtx = "2.8.4"
material = "1.12.0"
mmkvStatic = "1.3.4"
multidex = "2.0.1"
preferenceKtx = "1.2.1"
quickieBundled = "1.9.0"
recyclerview = "1.3.2"
rxandroid = "1.2.1"
rxjava = "1.3.8"
rxpermissions = "0.9.4"
toastcompat = "1.1.0"
viewpager2 = "1.1.0"
workRuntimeKtx = "2.8.1"
[libraries]
activity-ktx = { module = "androidx.activity:activity-ktx", version.ref = "activityKtx" }
appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" }
cardview = { module = "androidx.cardview:cardview", version.ref = "cardview" }
constraintlayout = { module = "androidx.constraintlayout:constraintlayout", version.ref = "constraintlayout" }
core = { module = "com.google.zxing:core", version.ref = "core" }
editorkit = { module = "com.blacksquircle.ui:editorkit", version.ref = "editorkit" }
flexbox = { module = "com.google.android.flexbox:flexbox", version.ref = "flexbox" }
fragment-ktx = { module = "androidx.fragment:fragment-ktx", version.ref = "fragmentKtx" }
gson = { module = "com.google.code.gson:gson", version.ref = "gson" }
junit = { module = "junit:junit", version.ref = "junit" }
kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlinReflect" }
kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinxCoroutinesCore" }
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinxCoroutinesCore" }
language-base = { module = "com.blacksquircle.ui:language-base", version.ref = "editorkit" }
language-json = { module = "com.blacksquircle.ui:language-json", version.ref = "editorkit" }
legacy-support-v4 = { module = "androidx.legacy:legacy-support-v4", version.ref = "legacySupportV4" }
lifecycle-livedata-ktx = { module = "androidx.lifecycle:lifecycle-livedata-ktx", version.ref = "lifecycleViewmodelKtx" }
lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycleViewmodelKtx" }
lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "lifecycleViewmodelKtx" }
material = { module = "com.google.android.material:material", version.ref = "material" }
mmkv-static = { module = "com.tencent:mmkv-static", version.ref = "mmkvStatic" }
multidex = { module = "androidx.multidex:multidex", version.ref = "multidex" }
quickie-bundled = { module = "io.github.g00fy2.quickie:quickie-bundled", version.ref = "quickieBundled" }
recyclerview = { module = "androidx.recyclerview:recyclerview", version.ref = "recyclerview" }
preference-ktx = { module = "androidx.preference:preference-ktx", version.ref = "preferenceKtx" }
rxandroid = { module = "io.reactivex:rxandroid", version.ref = "rxandroid" }
rxjava = { module = "io.reactivex:rxjava", version.ref = "rxjava" }
rxpermissions = { module = "com.tbruyelle.rxpermissions:rxpermissions", version.ref = "rxpermissions" }
toastcompat = { module = "me.drakeet.support:toastcompat", version.ref = "toastcompat" }
viewpager2 = { module = "androidx.viewpager2:viewpager2", version.ref = "viewpager2" }
work-multiprocess = { module = "androidx.work:work-multiprocess", version.ref = "workRuntimeKtx" }
work-runtime-ktx = { module = "androidx.work:work-runtime-ktx", version.ref = "workRuntimeKtx" }
[plugins]

View File

@@ -1,6 +1,6 @@
#Sun Jun 21 12:33:19 CST 2020
#Sun Jul 28 13:40:50 CST 2024
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip